dev
dart 178 lines 4.74 KB
Raw
1 import "dart:io";
2
3 import "package:flutter/material.dart";
4 import "package:flutter/services.dart";
5
6 class DigitInputController {
7 String _text = "";
8
9 String get text => _text;
10
11 set text(String newText) {
12 _text = newText;
13 for (final listener in _listeners) {
14 listener.call();
15 }
16 }
17
18 final List<void Function()> _listeners = [];
19
20 void addListener(void Function() listener) => _listeners.add(listener);
21 }
22
23 class DigitInputPill extends StatelessWidget {
24 const DigitInputPill({required this.highlighted, super.key, this.digit});
25
26 final String? digit;
27 final bool highlighted;
28
29 @override
30 Widget build(BuildContext context) => Container(
31 width: 51,
32 height: 84,
33 decoration: BoxDecoration(
34 color: Theme.of(context).colorScheme.surfaceContainerHigh,
35 borderRadius: BorderRadius.circular(16),
36 border: highlighted
37 ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
38 : null,
39 ),
40 child: Center(
41 child: Padding(
42 padding: const EdgeInsets.symmetric(vertical: 16),
43 child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)),
44 ),
45 ),
46 );
47 }
48
49 class DigitInput extends StatefulWidget {
50 const DigitInput({
51 required this.controller,
52 required this.desiredLength,
53 this.breakAt = 3,
54 super.key,
55 });
56
57 final DigitInputController controller;
58 final int desiredLength;
59 final int breakAt;
60
61 @override
62 State<DigitInput> createState() => _DigitInputState();
63 }
64
65 class _DigitInputState extends State<DigitInput> implements TextInputClient {
66 TextInputConnection? _textInputConnection;
67
68 @override
69 void initState() {
70 super.initState();
71 _openVirtualKeyboard();
72 }
73
74 void _openVirtualKeyboard() {
75 if (_textInputConnection == null || !_textInputConnection!.attached) {
76 _textInputConnection = TextInput.attach(
77 this,
78 TextInputConfiguration(
79 inputType: TextInputType.number,
80 inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
81 ),
82 );
83 }
84
85 _textInputConnection!.setEditingState(
86 TextEditingValue(
87 text: widget.controller.text,
88 selection: TextSelection.collapsed(offset: widget.controller.text.length),
89 ),
90 );
91 _textInputConnection!.show();
92 }
93
94 void _closeVirtualKeyboard() => _textInputConnection?.close();
95
96 @override
97 void dispose() {
98 _closeVirtualKeyboard();
99 super.dispose();
100 }
101
102 @override
103 Widget build(BuildContext context) => GestureDetector(
104 onTap: _openVirtualKeyboard,
105 child: SizedBox(
106 height: 84,
107 child: ListView.separated(
108 scrollDirection: Axis.horizontal,
109 shrinkWrap: true,
110 physics: const NeverScrollableScrollPhysics(),
111 itemBuilder: (context, index) => DigitInputPill(
112 digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
113 highlighted: widget.controller.text.length == index,
114 ),
115 separatorBuilder: (context, index) =>
116 SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
117 itemCount: widget.desiredLength,
118 ),
119 ),
120 );
121
122 @override
123 void updateEditingValue(TextEditingValue value) {
124 final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
125
126 if (numberRegExp.hasMatch(value.text)) {
127 setState(() => widget.controller.text = value.text);
128 } else {
129 // flutter keeps the text editing state regardless of whether you actually used it or not.
130 // so, if validation fails, we gotta set it back to what it was explicitly
131 _textInputConnection?.setEditingState(
132 TextEditingValue(
133 text: widget.controller.text,
134 selection: TextSelection.collapsed(offset: widget.controller.text.length),
135 ),
136 );
137 }
138 }
139
140 @override
141 void performAction(TextInputAction action) {}
142
143 @override
144 TextEditingValue? get currentTextEditingValue => TextEditingValue(text: widget.controller.text);
145
146 @override
147 void connectionClosed() => _textInputConnection = null;
148
149 @override
150 AutofillScope? get currentAutofillScope => null;
151
152 @override
153 void performPrivateCommand(String action, Map<String, dynamic> data) {}
154
155 @override
156 void showAutocorrectionPromptRect(int start, int end) {}
157
158 @override
159 void updateFloatingCursor(RawFloatingCursorPoint point) {}
160
161 @override
162 void insertTextPlaceholder(Size size) {}
163
164 @override
165 void removeTextPlaceholder() {}
166
167 @override
168 void showToolbar() {}
169
170 @override
171 void performSelector(String selectorName) {}
172
173 @override
174 void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
175
176 @override
177 void insertContent(KeyboardInsertedContent content) {}
178 }