Skip to content

Commit 28e0f08

Browse files
authored
Reland "[text_input] introduce TextInputControl" (#113758)
1 parent a25c86c commit 28e0f08

File tree

8 files changed

+902
-83
lines changed

8 files changed

+902
-83
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Hidenori Matsubayashi <[email protected]>
7777
Perqin Xie <[email protected]>
7878
Seongyun Kim <[email protected]>
7979
Ludwik Trammer <[email protected]>
80+
J-P Nurmi <[email protected]>
8081
Marian Triebe <[email protected]>
8182
Alexis Rouillard <[email protected]>
8283
Mirko Mucaria <[email protected]>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Flutter code sample for TextInputControl
6+
7+
import 'package:flutter/foundation.dart';
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter/services.dart';
10+
11+
void main() => runApp(const MyApp());
12+
13+
class MyApp extends StatelessWidget {
14+
const MyApp({super.key});
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return const MaterialApp(
19+
home: MyStatefulWidget(),
20+
);
21+
}
22+
}
23+
24+
class MyStatefulWidget extends StatefulWidget {
25+
const MyStatefulWidget({super.key});
26+
27+
@override
28+
MyStatefulWidgetState createState() => MyStatefulWidgetState();
29+
}
30+
31+
class MyStatefulWidgetState extends State<MyStatefulWidget> {
32+
final TextEditingController _controller = TextEditingController();
33+
final FocusNode _focusNode = FocusNode();
34+
35+
@override
36+
void dispose() {
37+
super.dispose();
38+
_controller.dispose();
39+
_focusNode.dispose();
40+
}
41+
42+
@override
43+
Widget build(BuildContext context) {
44+
return Scaffold(
45+
body: Center(
46+
child: TextField(
47+
autofocus: true,
48+
controller: _controller,
49+
focusNode: _focusNode,
50+
decoration: InputDecoration(
51+
suffix: IconButton(
52+
icon: const Icon(Icons.clear),
53+
tooltip: 'Clear and unfocus',
54+
onPressed: () {
55+
_controller.clear();
56+
_focusNode.unfocus();
57+
},
58+
),
59+
),
60+
),
61+
),
62+
bottomSheet: const MyVirtualKeyboard(),
63+
);
64+
}
65+
}
66+
67+
class MyVirtualKeyboard extends StatefulWidget {
68+
const MyVirtualKeyboard({super.key});
69+
70+
@override
71+
MyVirtualKeyboardState createState() => MyVirtualKeyboardState();
72+
}
73+
74+
class MyVirtualKeyboardState extends State<MyVirtualKeyboard> {
75+
final MyTextInputControl _inputControl = MyTextInputControl();
76+
77+
@override
78+
void initState() {
79+
super.initState();
80+
_inputControl.register();
81+
}
82+
83+
@override
84+
void dispose() {
85+
super.dispose();
86+
_inputControl.unregister();
87+
}
88+
89+
void _handleKeyPress(String key) {
90+
_inputControl.processUserInput(key);
91+
}
92+
93+
@override
94+
Widget build(BuildContext context) {
95+
return ValueListenableBuilder<bool>(
96+
valueListenable: _inputControl.visible,
97+
builder: (_, bool visible, __) {
98+
return Visibility(
99+
visible: visible,
100+
child: FocusScope(
101+
canRequestFocus: false,
102+
child: TextFieldTapRegion(
103+
child: Row(
104+
mainAxisAlignment: MainAxisAlignment.center,
105+
children: <Widget>[
106+
for (final String key in <String>['A', 'B', 'C'])
107+
ElevatedButton(
108+
child: Text(key),
109+
onPressed: () => _handleKeyPress(key),
110+
),
111+
],
112+
),
113+
),
114+
),
115+
);
116+
},
117+
);
118+
}
119+
}
120+
121+
class MyTextInputControl with TextInputControl {
122+
TextEditingValue _editingState = TextEditingValue.empty;
123+
final ValueNotifier<bool> _visible = ValueNotifier<bool>(false);
124+
125+
/// The input control's visibility state for updating the visual presentation.
126+
ValueListenable<bool> get visible => _visible;
127+
128+
/// Register the input control.
129+
void register() => TextInput.setInputControl(this);
130+
131+
/// Restore the original platform input control.
132+
void unregister() => TextInput.restorePlatformInputControl();
133+
134+
@override
135+
void show() => _visible.value = true;
136+
137+
@override
138+
void hide() => _visible.value = false;
139+
140+
@override
141+
void setEditingState(TextEditingValue value) => _editingState = value;
142+
143+
/// Process user input.
144+
///
145+
/// Updates the internal editing state by inserting the input text,
146+
/// and by replacing the current selection if any.
147+
void processUserInput(String input) {
148+
_editingState = _editingState.copyWith(
149+
text: _insertText(input),
150+
selection: _replaceSelection(input),
151+
);
152+
153+
// Request the attached client to update accordingly.
154+
TextInput.updateEditingValue(_editingState);
155+
}
156+
157+
String _insertText(String input) {
158+
final String text = _editingState.text;
159+
final TextSelection selection = _editingState.selection;
160+
return text.replaceRange(selection.start, selection.end, input);
161+
}
162+
163+
TextSelection _replaceSelection(String input) {
164+
final TextSelection selection = _editingState.selection;
165+
return TextSelection.collapsed(offset: selection.start + input.length);
166+
}
167+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter/services.dart';
7+
import 'package:flutter_api_samples/services/text_input/text_input_control.0.dart'
8+
as example;
9+
import 'package:flutter_test/flutter_test.dart';
10+
11+
void main() {
12+
testWidgets('Enter text using the VKB', (WidgetTester tester) async {
13+
await tester.pumpWidget(const example.MyApp());
14+
await tester.pumpAndSettle();
15+
16+
await tester.tap(find.descendant(
17+
of: find.byType(example.MyVirtualKeyboard),
18+
matching: find.widgetWithText(ElevatedButton, 'A'),
19+
));
20+
await tester.pumpAndSettle();
21+
expect(find.widgetWithText(TextField, 'A'), findsOneWidget);
22+
23+
await tester.tap(find.descendant(
24+
of: find.byType(example.MyVirtualKeyboard),
25+
matching: find.widgetWithText(ElevatedButton, 'B'),
26+
));
27+
await tester.pumpAndSettle();
28+
expect(find.widgetWithText(TextField, 'AB'), findsOneWidget);
29+
30+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
31+
await tester.pumpAndSettle();
32+
33+
await tester.tap(find.descendant(
34+
of: find.byType(example.MyVirtualKeyboard),
35+
matching: find.widgetWithText(ElevatedButton, 'C'),
36+
));
37+
await tester.pumpAndSettle();
38+
expect(find.widgetWithText(TextField, 'ACB'), findsOneWidget);
39+
});
40+
}

0 commit comments

Comments
 (0)