diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1a1ee1e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## [0.0.1] - 12 October 2022
+
+- Initial release.
diff --git a/README.md b/README.md
index 5d983ce..2a7ffd3 100644
--- a/README.md
+++ b/README.md
@@ -2,34 +2,83 @@
[](https://github.com/tenhobi/effective_dart)
-Short description of what your package is, why you created it. What issues it fixes and how it works. Also mention the available platforms
+Custom widget that allows for an inputfield spilt over multiple fields
+Ported from the appshell.
## Setup
-What setup steps are neccesarry and why
+To use this package, add `flutter_single_character_input` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels).
-
-PLATFORM
-
-specific platform steps
-
-
+```dart
+ flutter_single_character_input:
+ git:
+ url: https://github.com/Iconica-Development/flutter_single_character_input.git
+ ref: master
+```
## How to use
-How can we use the package descibe the most common ways with examples in
-
```dart
- codeblocks
+ SingleCharacterInput(
+ characters: [
+ InputCharacter(
+ hint: '1',
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: true,
+ decimal: true,
+ ),
+ formatter: (value) {
+ if (RegExp('[0-9]').hasMatch(value)) {
+ return value;
+ }
+ return '';
+ },
+ ),
+ InputCharacter(
+ hint: 'B',
+ keyboardType: TextInputType.name,
+ formatter: (value) {
+ if (RegExp('[A-Za-z]').hasMatch(value)) {
+ return value.toUpperCase();
+ }
+ return '';
+ },
+ ),
+ ],
+ textStyle: Theme.of(context).textTheme.headline1?.copyWith(
+ fontWeight: FontWeight.w400,
+ fontSize: 28,
+ ),
+ inputDecoration: InputDecoration(
+ hintStyle: Theme.of(context).textTheme.bodyText1?.copyWith(
+ fontWeight: FontWeight.w400,
+ color: const Color(0xFFBBBBBB),
+ fontSize: 28,
+ ),
+ isDense: true,
+ isCollapsed: true,
+ ),
+ buildDecoration: (context, input) {
+ return Container(
+ margin: const EdgeInsets.all(5),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 15),
+ width: 32,
+ child: input,
+ ),
+ );
+ },
+ onChanged: (value, finished) {},
+ ),
```
## Issues
-Please file any issues, bugs or feature request as an issue on our [GitHub](REPO URL) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
+Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_single_character_input) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
-If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](URL TO PULL REQUEST TAB IN REPO).
+If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_single_character_input/pulls).
## Author
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0530d9c..eaf2f3e 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -3,212 +3,3 @@ analyzer:
errors:
todo: ignore
exclude: [lib/generated_plugin_registrant.dart]
-linter:
- # https://dart.dev/tools/linter-rules#lints
- rules:
- # error rules
- always_use_package_imports: false
- avoid_dynamic_calls: true
- avoid_empty_else: true
- avoid_print: true
- avoid_relative_lib_imports: true
- avoid_returning_null_for_future: true
- avoid_slow_async_io: true
- avoid_type_to_string: true
- avoid_types_as_parameter_names: true
- avoid_web_libraries_in_flutter: true
- cancel_subscriptions: true
- close_sinks: true
- comment_references: false
- control_flow_in_finally: true
- diagnostic_describe_all_properties: false
- empty_statements: true
- hash_and_equals: true
- invariant_booleans: true
- iterable_contains_unrelated_type: true
- list_remove_unrelated_type: true
- literal_only_boolean_expressions: true
- no_adjacent_strings_in_list: true
- no_duplicate_case_values: true
- no_logic_in_create_state: true
- prefer_relative_imports: false
- prefer_void_to_null: true
- test_types_in_equals: true
- throw_in_finally: true
- unnecessary_statements: true
- unrelated_type_equality_checks: true
- unsafe_html: true
- use_build_context_synchronously: true
- use_key_in_widget_constructors: true
- valid_regexps: true
- # style rules
- always_declare_return_types: true
- always_put_control_body_on_new_line: true
- always_put_required_named_parameters_first: true
- always_require_non_null_named_parameters: true
- always_specify_types: false
- annotate_overrides: true
- avoid_annotating_with_dynamic: false
- avoid_bool_literals_in_conditional_expressions: true
- avoid_catches_without_on_clauses: false
- avoid_catching_errors: false
- avoid_classes_with_only_static_members: true
- avoid_double_and_int_checks: true
- avoid_equals_and_hash_code_on_mutable_classes: false
- avoid_escaping_inner_quotes: false
- avoid_field_initializers_in_const_classes: true
- avoid_final_parameters: true
- avoid_function_literals_in_foreach_calls: true
- avoid_implementing_value_types: true
- avoid_init_to_null: true
- avoid_js_rounded_ints: true
- avoid_multiple_declarations_per_line: true
- avoid_null_checks_in_equality_operators: true
- avoid_positional_boolean_parameters: true
- avoid_private_typedef_functions: true
- avoid_redundant_argument_values: false
- avoid_renaming_method_parameters: true
- avoid_return_types_on_setters: true
- avoid_returning_null: true
- avoid_returning_null_for_void: true
- avoid_returning_this: true
- avoid_setters_without_getters: true
- avoid_shadowing_type_parameters: true
- avoid_single_cascade_in_expression_statements: true
- avoid_types_on_closure_parameters: false
- avoid_unnecessary_containers: false
- avoid_unused_constructor_parameters: true
- avoid_void_async: true
- await_only_futures: true
- camel_case_extensions: true
- camel_case_types: true
- cascade_invocations: true
- cast_nullable_to_non_nullable: true
- conditional_uri_does_not_exist: true
- constant_identifier_names: true
- curly_braces_in_flow_control_structures: true
- deprecated_consistency: true
- directives_ordering: true
- do_not_use_environment: true
- empty_catches: true
- empty_constructor_bodies: true
- eol_at_end_of_file: true
- exhaustive_cases: true
- file_names: true
- flutter_style_todos: true
- implementation_imports: true
- join_return_with_assignment: true
- leading_newlines_in_multiline_strings: true
- library_names: true
- library_prefixes: true
- library_private_types_in_public_api: true
- lines_longer_than_80_chars: true
- missing_whitespace_between_adjacent_strings: true
- no_default_cases: true
- no_leading_underscores_for_library_prefixes: true
- no_leading_underscores_for_local_identifiers: true
- no_runtimeType_toString: true
- non_constant_identifier_names: true
- noop_primitive_operations: true
- null_check_on_nullable_type_parameter: true
- null_closures: true
- omit_local_variable_types: true
- one_member_abstracts: true
- only_throw_errors: true
- overridden_fields: true
- package_api_docs: true
- package_prefixed_library_names: true
- parameter_assignments: true
- prefer_adjacent_string_concatenation: true
- prefer_asserts_in_initializer_lists: true
- prefer_asserts_with_message: true
- prefer_collection_literals: true
- prefer_conditional_assignment: true
- prefer_const_constructors: true
- prefer_const_constructors_in_immutables: true
- prefer_const_declarations: false
- prefer_const_literals_to_create_immutables: false
- prefer_constructors_over_static_methods: true
- prefer_contains: true
- prefer_double_quotes: false
- prefer_equal_for_default_values: true
- prefer_expression_function_bodies: false
- prefer_final_fields: true
- prefer_final_in_for_each: false
- prefer_final_locals: false
- prefer_final_parameters: false
- prefer_for_elements_to_map_fromIterable: true
- prefer_foreach: true
- prefer_function_declarations_over_variables: true
- prefer_generic_function_type_aliases: true
- prefer_if_elements_to_conditional_expressions: true
- prefer_if_null_operators: true
- prefer_initializing_formals: true
- prefer_inlined_adds: true
- prefer_int_literals: false
- prefer_interpolation_to_compose_strings: true
- prefer_is_empty: true
- prefer_is_not_empty: true
- prefer_is_not_operator: true
- prefer_iterable_whereType: true
- prefer_mixin: true
- prefer_null_aware_method_calls: true
- prefer_null_aware_operators: true
- prefer_single_quotes: true
- prefer_spread_collections: true
- prefer_typing_uninitialized_variables: true
- provide_deprecation_message: true
- public_member_api_docs: false
- recursive_getters: true
- require_trailing_commas: true
- sized_box_for_whitespace: true
- sized_box_shrink_expand: true
- slash_for_doc_comments: true
- sort_child_properties_last: true
- sort_constructors_first: true
- sort_unnamed_constructors_first: true
- tighten_type_of_initializing_formals: true
- type_annotate_public_apis: true
- type_init_formals: true
- unawaited_futures: true
- unnecessary_await_in_return: true
- unnecessary_brace_in_string_interps: true
- unnecessary_const: false
- unnecessary_constructor_name: true
- unnecessary_final: true
- unnecessary_getters_setters: true
- unnecessary_lambdas: true
- unnecessary_late: true
- unnecessary_new: true
- unnecessary_null_aware_assignments: true
- unnecessary_null_checks: true
- unnecessary_null_in_if_null_operators: true
- unnecessary_nullable_for_final_variable_declarations: true
- unnecessary_overrides: true
- unnecessary_parenthesis: true
- unnecessary_raw_strings: true
- unnecessary_string_escapes: true
- unnecessary_string_interpolations: true
- unnecessary_this: true
- use_decorated_box: true
- use_full_hex_values_for_flutter_colors: true
- use_function_type_syntax_for_parameters: true
- use_if_null_to_convert_nulls_to_bools: true
- use_is_even_rather_than_modulo: true
- use_late_for_private_fields_and_variables: true
- use_named_constants: true
- use_raw_strings: false
- use_rethrow_when_possible: true
- use_setters_to_change_properties: true
- use_string_buffers: true
- use_test_throws_matchers: true
- use_to_and_as_if_applicable: true
- void_checks: true
- # pub rules
- depend_on_referenced_packages: true
- lowercase_with_underscores: true
- secure_pubspec_urls: false
- sort_pub_dependencies: false
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 8273dd2..33bd3a6 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,17 +1,111 @@
import 'package:flutter/material.dart';
-import 'package:flutter_single_character_input/flutter_single_character_input.dart';
+import 'package:flutter_single_character_input/single_character_input.dart';
void main() {
runApp(const MaterialApp(home: FlutterSingleCharacterInputDemo()));
}
-class FlutterSingleCharacterInputDemo extends StatelessWidget {
+class FlutterSingleCharacterInputDemo extends StatefulWidget {
const FlutterSingleCharacterInputDemo({Key? key}) : super(key: key);
+ @override
+ State createState() =>
+ _FlutterSingleCharacterInputDemoState();
+}
+
+class _FlutterSingleCharacterInputDemoState
+ extends State {
+ CharacterFormatter get _numberFormatter => (String value) {
+ if (RegExp('[0-9]').hasMatch(value)) {
+ return value;
+ }
+ return '';
+ };
+
+ CharacterFormatter get _textFormatter => (String value) {
+ if (RegExp('[A-Za-z]').hasMatch(value)) {
+ return value.toUpperCase();
+ }
+ return '';
+ };
+
@override
Widget build(BuildContext context) {
- return const Scaffold(
- body: SingleCharacterInput(),
+ return Scaffold(
+ body: Center(
+ child: SingleCharacterInput(
+ characters: [
+ InputCharacter(
+ hint: '1',
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: true,
+ decimal: true,
+ ),
+ formatter: _numberFormatter,
+ ),
+ InputCharacter(
+ hint: '2',
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: true,
+ decimal: true,
+ ),
+ formatter: _numberFormatter,
+ ),
+ InputCharacter(
+ hint: '3',
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: true,
+ decimal: true,
+ ),
+ formatter: _numberFormatter,
+ ),
+ InputCharacter(
+ hint: '4',
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: true,
+ decimal: true,
+ ),
+ formatter: _numberFormatter,
+ ),
+ InputCharacter(
+ hint: 'A',
+ keyboardType: TextInputType.name,
+ formatter: _textFormatter,
+ ),
+ InputCharacter(
+ hint: 'B',
+ keyboardType: TextInputType.name,
+ formatter: _textFormatter,
+ ),
+ ],
+ textStyle: Theme.of(context).textTheme.headline1?.copyWith(
+ fontWeight: FontWeight.w400,
+ fontSize: 28,
+ ),
+ inputDecoration: InputDecoration(
+ hintStyle: Theme.of(context).textTheme.bodyText1?.copyWith(
+ fontWeight: FontWeight.w400,
+ color: const Color(0xFFBBBBBB),
+ fontSize: 28,
+ ),
+ isDense: true,
+ isCollapsed: true,
+ ),
+ buildDecoration: (context, input) {
+ return Container(
+ margin: const EdgeInsets.all(5),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 15),
+ width: 32,
+ child: input,
+ ),
+ );
+ },
+ onChanged: (value, finished) {
+ // setState(() {});
+ },
+ ),
+ ),
);
}
}
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
index 233546e..8b13789 100644
--- a/example/test/widget_test.dart
+++ b/example/test/widget_test.dart
@@ -1,29 +1 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-import 'package:example/main.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(const FlutterSingleCharacterInputDemo());
-
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
- });
-}
diff --git a/lib/flutter_single_character_input.dart b/lib/flutter_single_character_input.dart
deleted file mode 100644
index 98280b2..0000000
--- a/lib/flutter_single_character_input.dart
+++ /dev/null
@@ -1,3 +0,0 @@
-library flutter_single_character_input;
-
-export 'src/flutter_single_character_input.dart';
diff --git a/lib/single_character_input.dart b/lib/single_character_input.dart
new file mode 100644
index 0000000..619d3b8
--- /dev/null
+++ b/lib/single_character_input.dart
@@ -0,0 +1,4 @@
+library single_character_input;
+
+export 'src/input_character.dart';
+export 'src/single_character_input.dart';
diff --git a/lib/src/flutter_single_character_input.dart b/lib/src/flutter_single_character_input.dart
deleted file mode 100644
index b1a9f11..0000000
--- a/lib/src/flutter_single_character_input.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/src/widgets/container.dart';
-import 'package:flutter/src/widgets/framework.dart';
-
-class SingleCharacterInput extends StatefulWidget {
- const SingleCharacterInput({super.key});
-
- @override
- State createState() => _SingleCharacterInputState();
-}
-
-class _SingleCharacterInputState extends State {
- @override
- Widget build(BuildContext context) {
- return const Text('HELLO THERE');
- }
-}
diff --git a/lib/src/input_character.dart b/lib/src/input_character.dart
new file mode 100644
index 0000000..8d51953
--- /dev/null
+++ b/lib/src/input_character.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+
+class InputCharacter {
+ const InputCharacter({
+ required this.keyboardType,
+ this.readOnly = false,
+ this.hint = '',
+ this.formatter,
+ });
+
+ final TextInputType keyboardType;
+ final CharacterFormatter? formatter;
+ final bool readOnly;
+ final String hint;
+
+ String format(String value) {
+ return formatter?.call(value) ?? value;
+ }
+}
+
+typedef CharacterFormatter = String Function(String);
diff --git a/lib/src/single_character_input.dart b/lib/src/single_character_input.dart
new file mode 100644
index 0000000..65f0e19
--- /dev/null
+++ b/lib/src/single_character_input.dart
@@ -0,0 +1,306 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_single_character_input/src/input_character.dart';
+
+class SingleCharacterInput extends StatefulWidget {
+ const SingleCharacterInput({
+ required this.characters,
+ required this.onChanged,
+ this.textAlign = TextAlign.center,
+ this.cursorTextAlign = TextAlign.center,
+ this.buildCustomInput,
+ this.buildDecoration,
+ this.inputDecoration,
+ this.spacing = 0,
+ this.textStyle,
+ Key? key,
+ }) : super(key: key);
+
+ final Widget Function(BuildContext context, List inputs)?
+ buildCustomInput;
+
+ /// Called when building a single input. Can be used to wrap the input.
+ final Widget Function(BuildContext context, Widget input)? buildDecoration;
+
+ /// Gets called when the value is changed.
+ /// Passes the changed value and if all inputs are filled in.
+ final void Function(String value, bool isComplete) onChanged;
+
+ final InputDecoration? inputDecoration;
+
+ /// List of all character fields which are used to create inputs.
+ final List characters;
+
+ final TextAlign textAlign;
+ final TextAlign cursorTextAlign;
+ final double spacing;
+ final TextStyle? textStyle;
+
+ @override
+ State createState() => _SingleCharacterInputState();
+}
+
+class _SingleCharacterInputState extends State
+ with SingleTickerProviderStateMixin {
+ late final TextEditingController _mainController;
+ late final Map _mainNodes;
+ String _currentValue = '';
+ int _currentIndex = 0;
+ late TextInputType _currentKeyboard;
+ late Animation _cursorAnimation;
+ late AnimationController _cursorAnimationController;
+
+ @override
+ void initState() {
+ super.initState();
+ _mainController = TextEditingController();
+ _mainNodes = widget.characters
+ .asMap()
+ .map((key, value) => MapEntry(value.keyboardType, FocusNode()));
+ _currentKeyboard = widget.characters.first.keyboardType;
+ _cursorAnimationController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 500),
+ );
+ _cursorAnimation = Tween(begin: 0.0, end: 0.8).animate(
+ CurvedAnimation(
+ curve: Curves.linear,
+ parent: _cursorAnimationController,
+ ),
+ );
+
+ _cursorAnimationController
+ ..addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.completed) {
+ _cursorAnimationController.repeat(reverse: true);
+ }
+ })
+ ..forward();
+
+ _mainNodes.forEach((key, value) {
+ value.addListener(() {
+ setState(() {});
+ });
+ });
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _mainNodes[_currentKeyboard]?.requestFocus();
+ setState(() {});
+ });
+ }
+
+ @override
+ void dispose() {
+ _mainController.dispose();
+ for (var element in _mainNodes.values) {
+ element.dispose();
+ }
+ _cursorAnimationController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _mainNodes[_currentKeyboard]?.unfocus();
+ _mainNodes[_currentKeyboard]?.requestFocus();
+ });
+ setState(() {});
+ },
+ child: Wrap(
+ direction: Axis.horizontal,
+ children: [
+ Offstage(
+ child: Column(
+ children: _mainNodes
+ .map((key, value) {
+ return MapEntry(
+ key,
+ TextField(
+ focusNode: value,
+ controller: _mainController,
+ textCapitalization: TextCapitalization.characters,
+ keyboardType: key,
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(
+ RegExp('[a-zA-Z0-9]'),
+ ),
+ LengthLimitingTextInputFormatter(
+ widget.characters.length,
+ ),
+ ],
+ onChanged: (String value) {
+ if (value.length > _currentIndex) {
+ var result = widget.characters[_currentIndex]
+ .format(value[_currentIndex]);
+ if (value[_currentIndex] != result) {
+ value = value.replaceRange(
+ _currentIndex,
+ _currentIndex + 1,
+ result,
+ );
+ _mainController.value =
+ _mainController.value.copyWith(
+ text: value,
+ selection: TextSelection.collapsed(
+ offset: value.length,
+ ),
+ );
+ }
+ }
+ _onChanged(value);
+ },
+ ),
+ );
+ })
+ .values
+ .toList(),
+ ),
+ ),
+ if (widget.buildCustomInput != null) ...[
+ widget.buildCustomInput!.call(
+ context,
+ widget.characters
+ .asMap()
+ .map(
+ (key, value) => MapEntry(key, _createCharacter(key)),
+ )
+ .values
+ .toList(),
+ ),
+ ] else ...[
+ for (var i = 0; i < widget.characters.length; i++) ...[
+ _createCharacter(i),
+ if (i < widget.characters.length - 1 && widget.spacing > 0) ...[
+ SizedBox(
+ height: widget.spacing,
+ width: widget.spacing,
+ ),
+ ]
+ ],
+ ],
+ ],
+ ),
+ );
+ }
+
+ void _onChanged(String value) {
+ widget.onChanged.call(value, value.length == widget.characters.length);
+ setState(() {
+ _currentValue = value;
+ });
+
+ var nextIndex = min(_currentValue.length, widget.characters.length - 1);
+ if (_currentIndex != nextIndex) {
+ setState(() {
+ _currentIndex = nextIndex;
+ });
+ }
+
+ var nextKeyboard = widget.characters[_currentIndex].keyboardType;
+ if (_currentKeyboard != nextKeyboard) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _mainNodes[nextKeyboard]?.requestFocus();
+ });
+ setState(() {
+ _currentKeyboard = nextKeyboard;
+ });
+ }
+ }
+
+ Widget _createCharacter(int index) {
+ var char = widget.characters[index];
+ if (char.readOnly) {
+ return Text(
+ char.hint,
+ style: widget.textStyle,
+ );
+ }
+ Widget input = TextField(
+ textCapitalization: TextCapitalization.characters,
+ decoration: widget.inputDecoration?.copyWith(
+ label: _createLabel(index),
+ floatingLabelBehavior: FloatingLabelBehavior.never,
+ ),
+ textAlign: widget.textAlign,
+ style: widget.textStyle,
+ readOnly: true,
+ onTap: () {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _mainNodes[_currentKeyboard]?.unfocus();
+ _mainNodes[_currentKeyboard]?.requestFocus();
+ });
+ setState(() {});
+ },
+ controller: TextEditingController.fromValue(
+ TextEditingValue(text: _getCurrentInputValue(index)),
+ ),
+ );
+ if (widget.buildDecoration != null) {
+ return widget.buildDecoration!.call(context, input);
+ }
+ return input;
+ }
+
+ String _getCurrentInputValue(index) {
+ if (_currentValue.length > index) {
+ return _currentValue[index];
+ }
+ return '';
+ }
+
+ bool _hasFocus() {
+ return _mainNodes.values.any((element) => element.hasFocus);
+ }
+
+ Widget _createLabel(int index) {
+ if (index < _currentValue.length) {
+ return const SizedBox.shrink();
+ }
+ if (index == _currentValue.length && _hasFocus()) {
+ return AnimatedBuilder(
+ animation: _cursorAnimation,
+ builder: (context, _) {
+ return Opacity(
+ opacity: _cursorAnimation.value,
+ child: Container(
+ alignment: _getAlignment(widget.cursorTextAlign),
+ child: Text(
+ '|',
+ style: widget.textStyle,
+ textAlign: widget.cursorTextAlign,
+ ),
+ ),
+ );
+ },
+ );
+ } else {
+ return Container(
+ alignment: _getAlignment(widget.textAlign),
+ child: Text(
+ widget.characters[index].hint,
+ style: widget.inputDecoration?.hintStyle ?? widget.textStyle,
+ textAlign: widget.textAlign,
+ ),
+ );
+ }
+ }
+
+ Alignment _getAlignment(TextAlign textAlign) {
+ switch (textAlign) {
+ case TextAlign.left:
+ case TextAlign.start:
+ return Alignment.centerLeft;
+ case TextAlign.right:
+ case TextAlign.end:
+ return Alignment.centerRight;
+ case TextAlign.center:
+ case TextAlign.justify:
+ return Alignment.center;
+ }
+ }
+}
diff --git a/test/flutter-single-character-input_test.dart b/test/single_character_input_test.dart
similarity index 100%
rename from test/flutter-single-character-input_test.dart
rename to test/single_character_input_test.dart