Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active August 25, 2023 14:51
Show Gist options
  • Save slightfoot/732a57b7d168d936bbf8e4287b59273e to your computer and use it in GitHub Desktop.
Save slightfoot/732a57b7d168d936bbf8e4287b59273e to your computer and use it in GitHub Desktop.
Number Input Widget - For entering pins and other such things - #HumpDayQandA - 24th April 2019
// MIT License
//
// Copyright (c) 2023 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
bool _loading = false;
void _onSubmit(value) {
print('entered: $value');
setState(() => _loading = true);
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() => _loading = false);
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: _loading
? const CircularProgressIndicator()
: NumberInput(onSubmit: _onSubmit),
),
),
);
}
}
class NumberInput extends StatefulWidget {
const NumberInput({
super.key,
this.initialValue,
required this.onSubmit,
this.autoFocus = true,
this.maxLength = 6,
});
final String? initialValue;
final ValueChanged<String> onSubmit;
final bool autoFocus;
final int maxLength;
@override
State<NumberInput> createState() => _NumberInputState();
}
class _NumberInputState extends State<NumberInput>
with TextInputClient
implements AutofillClient {
late TextEditingValue _editingValue;
late FocusNode _focusNode;
TextInputConnection? _connection;
AutofillGroupState? _currentAutofillScope;
bool get _hasInputConnection => _connection?.attached ?? false;
@override
TextEditingValue? get currentTextEditingValue => _editingValue;
@override
AutofillScope? get currentAutofillScope => _currentAutofillScope;
@override
String get autofillId => 'NumberInput-$hashCode';
@override
TextInputConfiguration get textInputConfiguration {
return TextInputConfiguration(
autofillConfiguration: AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: const [AutofillHints.oneTimeCode],
currentEditingValue: _editingValue,
),
inputType: TextInputType.number,
inputAction: TextInputAction.done,
autocorrect: false,
);
}
@override
void initState() {
super.initState();
if (widget.initialValue case String initalValue) {
_editingValue = TextEditingValue(
text: initalValue,
selection: TextSelection.collapsed(offset: initalValue.length),
);
} else {
_editingValue = const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
composing: TextRange(start: 0, end: 0),
);
}
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChanged);
if (widget.autoFocus) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => requestKeyboard(),
);
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final AutofillGroupState? newAutofillGroup = AutofillGroup.maybeOf(context);
if (currentAutofillScope != newAutofillGroup) {
_currentAutofillScope?.unregister(autofillId);
_currentAutofillScope = newAutofillGroup;
_currentAutofillScope?.register(this);
}
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
_closeInputConnectionIfNeeded();
}
setState(() {});
}
@override
void dispose() {
_currentAutofillScope?.unregister(autofillId);
_focusNode.dispose();
_closeInputConnectionIfNeeded();
super.dispose();
}
@override
void autofill(TextEditingValue newEditingValue) {
final value = int.tryParse(newEditingValue.text)?.toString();
if (value != null) {
setState(() {
_editingValue = TextEditingValue(
text: value,
selection: TextSelection.collapsed(offset: value.length),
);
});
}
}
@override
Widget build(BuildContext context) {
final text = _editingValue.text;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(widget.maxLength, (int index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade500),
),
child: SizedBox(
width: 45.0,
height: 60.0,
child: Align(
alignment: Alignment.center,
child: Text(
(index < text.length) ? text[index] : '',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 32,
),
),
),
),
),
);
}).toList(),
),
);
}
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(this, textInputConfiguration);
_connection!.setEditingState(_editingValue);
}
_connection!.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_connection!.close();
_connection = null;
}
}
@override
void performAction(TextInputAction action) {
_focusNode.unfocus();
}
@override
void updateEditingValue(TextEditingValue value) {
print('update ${_editingValue}');
setState(() {
_editingValue = value;
if (value.text.length == widget.maxLength) {
widget.onSubmit.call(value.text);
}
});
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
// Not required
}
@override
void connectionClosed() {
// Not required
}
@override
void didChangeInputControl(
TextInputControl? oldControl, TextInputControl? newControl) {
// Not required
}
@override
void insertContent(KeyboardInsertedContent content) {
// Not required
}
@override
void insertTextPlaceholder(Size size) {
// Not required
}
@override
void performPrivateCommand(String action, Map<String, dynamic> data) {
// Not required
}
@override
void performSelector(String selectorName) {
// Not required
}
@override
void removeTextPlaceholder() {
// Not required
}
@override
void showAutocorrectionPromptRect(int start, int end) {
// Not required
}
@override
void showToolbar() {
// Not required
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment