mirror of
https://github.com/Iconica-Development/flutter_single_character_input.git
synced 2025-05-19 12:13:45 +02:00
ported component from appshell
This commit is contained in:
parent
8199bddb28
commit
f096fae94f
11 changed files with 494 additions and 274 deletions
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## [0.0.1] - 12 October 2022
|
||||
|
||||
- Initial release.
|
75
README.md
75
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).
|
||||
|
||||
<details>
|
||||
<summary>PLATFORM</summary>
|
||||
|
||||
specific platform steps
|
||||
|
||||
</details>
|
||||
```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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<FlutterSingleCharacterInputDemo> createState() =>
|
||||
_FlutterSingleCharacterInputDemoState();
|
||||
}
|
||||
|
||||
class _FlutterSingleCharacterInputDemoState
|
||||
extends State<FlutterSingleCharacterInputDemo> {
|
||||
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(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
library flutter_single_character_input;
|
||||
|
||||
export 'src/flutter_single_character_input.dart';
|
4
lib/single_character_input.dart
Normal file
4
lib/single_character_input.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
library single_character_input;
|
||||
|
||||
export 'src/input_character.dart';
|
||||
export 'src/single_character_input.dart';
|
|
@ -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<SingleCharacterInput> createState() => _SingleCharacterInputState();
|
||||
}
|
||||
|
||||
class _SingleCharacterInputState extends State<SingleCharacterInput> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Text('HELLO THERE');
|
||||
}
|
||||
}
|
21
lib/src/input_character.dart
Normal file
21
lib/src/input_character.dart
Normal file
|
@ -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);
|
306
lib/src/single_character_input.dart
Normal file
306
lib/src/single_character_input.dart
Normal file
|
@ -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<Widget> 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<InputCharacter> characters;
|
||||
|
||||
final TextAlign textAlign;
|
||||
final TextAlign cursorTextAlign;
|
||||
final double spacing;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
State<SingleCharacterInput> createState() => _SingleCharacterInputState();
|
||||
}
|
||||
|
||||
class _SingleCharacterInputState extends State<SingleCharacterInput>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TextEditingController _mainController;
|
||||
late final Map<TextInputType, FocusNode> _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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue