mirror of
https://github.com/Iconica-Development/flutter_single_character_input.git
synced 2025-05-19 20:23: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)
|
[](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
|
## 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>
|
```dart
|
||||||
<summary>PLATFORM</summary>
|
flutter_single_character_input:
|
||||||
|
git:
|
||||||
specific platform steps
|
url: https://github.com/Iconica-Development/flutter_single_character_input.git
|
||||||
|
ref: master
|
||||||
</details>
|
```
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
How can we use the package descibe the most common ways with examples in
|
|
||||||
|
|
||||||
```dart
|
```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
|
## 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
|
## 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
|
## Author
|
||||||
|
|
||||||
|
|
|
@ -3,212 +3,3 @@ analyzer:
|
||||||
errors:
|
errors:
|
||||||
todo: ignore
|
todo: ignore
|
||||||
exclude: [lib/generated_plugin_registrant.dart]
|
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/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() {
|
void main() {
|
||||||
runApp(const MaterialApp(home: FlutterSingleCharacterInputDemo()));
|
runApp(const MaterialApp(home: FlutterSingleCharacterInputDemo()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlutterSingleCharacterInputDemo extends StatelessWidget {
|
class FlutterSingleCharacterInputDemo extends StatefulWidget {
|
||||||
const FlutterSingleCharacterInputDemo({Key? key}) : super(key: key);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Scaffold(
|
return Scaffold(
|
||||||
body: SingleCharacterInput(),
|
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