From 257a1aee5a26a5e8197fc66f6c0acb8f984065a4 Mon Sep 17 00:00:00 2001 From: Vitaliy Date: Mon, 30 May 2022 12:14:51 +0300 Subject: [PATCH] Cleanup. --- lib/bloc/group_field.dart | 2 + lib/bloc/groups_field_bloc.dart | 58 +++---- lib/bloc/groups_field_event.dart | 2 + lib/bloc/groups_field_state.dart | 8 + lib/groups_field.dart | 267 +++++++++++++------------------ lib/ui/default_text_field.dart | 31 ++-- 6 files changed, 169 insertions(+), 199 deletions(-) diff --git a/lib/bloc/group_field.dart b/lib/bloc/group_field.dart index 03df09e..aa8e56f 100644 --- a/lib/bloc/group_field.dart +++ b/lib/bloc/group_field.dart @@ -4,6 +4,8 @@ class GroupField { final String text; final Group group; final Widget widget; + + // ignore: no-object-declaration final Object field; GroupField({ diff --git a/lib/bloc/groups_field_bloc.dart b/lib/bloc/groups_field_bloc.dart index 591e982..4155417 100644 --- a/lib/bloc/groups_field_bloc.dart +++ b/lib/bloc/groups_field_bloc.dart @@ -1,3 +1,5 @@ +// ignore_for_file: no-magic-number + import 'dart:async'; import 'package:pedantic/pedantic.dart'; @@ -6,7 +8,7 @@ import 'package:flutter/widgets.dart'; import 'package:groups_field/group.dart'; -part 'interface.dart'; +part 'groups_field_bloc_interface.dart'; part 'group_field.dart'; part 'groups_field_event.dart'; part 'groups_field_state.dart'; @@ -26,7 +28,7 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { @visibleForTesting List fields; - String _textFieldValue; + String textFieldValue; late final StreamController stateController; @@ -43,8 +45,8 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { required this.delimiters, required this.isFieldCanBeDeleted, this.isScrollable = false, - }) : fields = [], - _textFieldValue = "" { + this.textFieldValue = "", + }) : fields = [] { stateController = StreamController(); stateStream = stateController.stream.asBroadcastStream().asBroadcastStream(); @@ -71,7 +73,7 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { @visibleForTesting Future prepareExistedGroupsFieldsWidgets( - PrepareExistedGroupsFieldsWidgets event, + PrepareExistedGroupsFieldsWidgets _, ) async { fields = prepareExistedGroupsFields(groups: groups); final widgets = fields.map((field) => field.widget).toList(); @@ -110,15 +112,9 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { isScrollable: isScrollable, ); - Size fieldsSize; - if (isScrollable) { - fieldsSize = event.parentLayoutElement.size; - } else { - fieldsSize = Size( - cursorPosition.dx, - event.parentLayoutElement.size.height, - ); - } + final fieldsSize = isScrollable + ? event.parentLayoutElement.size + : Size(cursorPosition.dx, event.parentLayoutElement.size.height); final lastChildElement = event.lastChildElement; final lastFieldSize = @@ -192,7 +188,7 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { } } - final previousTextFieldValue = _textFieldValue; + final previousTextFieldValue = textFieldValue; /// latest field must be removed. if (event.isRemovedFieldKeyPressed && @@ -221,7 +217,7 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { stateController.add(state); - _textFieldValue = event.textFieldValue; + textFieldValue = event.textFieldValue; } else { // Check is new text must be a field. @@ -260,9 +256,9 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { stateController.add(state); - _textFieldValue = ""; + textFieldValue = ""; } else { - _textFieldValue = event.textFieldValue; + textFieldValue = event.textFieldValue; } } } @@ -416,21 +412,17 @@ class GroupsFieldBloc implements GroupsFieldBlocInterface { Offset cursorPosition; if (isScrollable) { - if (parentElement == null) { - cursorPosition = const Offset(0, 0); - } else { - if (parentElement.size.width < parentLayoutElement.size.width) { - cursorPosition = Offset( - parentElement.size.width, - parentElement.size.height / 2, - ); - } else { - cursorPosition = Offset( - parentLayoutElement.size.width, - parentLayoutElement.size.height / 2, - ); - } - } + cursorPosition = parentElement == null + ? const Offset(0, 0) + : parentElement.size.width < parentLayoutElement.size.width + ? Offset( + parentElement.size.width, + parentElement.size.height / 2, + ) + : Offset( + parentLayoutElement.size.width, + parentLayoutElement.size.height / 2, + ); } else { Offset offset; diff --git a/lib/bloc/groups_field_event.dart b/lib/bloc/groups_field_event.dart index e8dbe12..c4cc61d 100644 --- a/lib/bloc/groups_field_event.dart +++ b/lib/bloc/groups_field_event.dart @@ -41,6 +41,8 @@ class TextFieldChanged extends GroupsFieldEvent { class SuggestionSelected extends GroupsFieldEvent { final Group group; + + // ignore: no-object-declaration final Object suggestion; SuggestionSelected({ diff --git a/lib/bloc/groups_field_state.dart b/lib/bloc/groups_field_state.dart index 8c400fb..4009f21 100644 --- a/lib/bloc/groups_field_state.dart +++ b/lib/bloc/groups_field_state.dart @@ -35,7 +35,9 @@ class GroupFieldRemove extends GroupsFieldState { final List widgets; final String removedFieldText; + // ignore: no-object-declaration final Object removedField; + final Group removedFieldGroup; GroupFieldRemove({ @@ -52,7 +54,10 @@ class NewFieldAdd extends GroupsFieldState { final String newFieldText; final Widget addedFieldWidget; + + // ignore: no-object-declaration final Object addedField; + final Group addedFieldGroup; NewFieldAdd({ @@ -82,7 +87,10 @@ class SuggestionsReady extends GroupsFieldState { class SuggestionSelect extends GroupsFieldState { /// widgets - already build fields in groups. final List widgets; + + // ignore: no-object-declaration final Object field; + final Group group; final String text; diff --git a/lib/groups_field.dart b/lib/groups_field.dart index 2c25dbe..b65ed53 100644 --- a/lib/groups_field.dart +++ b/lib/groups_field.dart @@ -1,3 +1,5 @@ +// ignore_for_file: no-magic-number + import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -6,6 +8,7 @@ import 'ui/fields_not_scrollable.dart'; import 'ui/fields_scrollable.dart'; import 'ui/group_suggestions.dart'; import 'ui/overlay_container.dart'; +import 'ui/default_text_field.dart'; import 'bloc/groups_field_bloc.dart'; import 'group.dart'; @@ -81,7 +84,6 @@ class _GroupsFieldState extends State { late final FocusNode _focusNode; late final FocusNode _textFieldFocusNode; - late bool _isRemovedFieldKeyPressed; OverlayEntry? _overlayEntry; @@ -97,17 +99,6 @@ class _GroupsFieldState extends State { _textFieldKey = GlobalKey(); _textEditingController = TextEditingController(); - _textEditingController.addListener(() { - if (!_isRemovedFieldKeyPressed) { - textFieldOnChangeHandler( - context: context, - fieldText: _textEditingController.text, - isRemoved: _isRemovedFieldKeyPressed, - ); - } - }); - - _isRemovedFieldKeyPressed = false; _focusNode = FocusNode(); _textFieldFocusNode = FocusNode(); @@ -164,9 +155,12 @@ class _GroupsFieldState extends State { SchedulerBinding.instance.addPostFrameCallback( (timeStamp) { + _overlayEntry?.remove(); + _overlayEntry = null; + final lastChildElement = _lastFieldKey.currentContext == null ? null - : _lastFieldKey.currentContext!.findRenderObject() as RenderBox; + : _lastFieldKey.currentContext?.findRenderObject() as RenderBox; final parentElement = _fieldsKey.currentContext?.findRenderObject() as RenderBox; @@ -198,14 +192,17 @@ class _GroupsFieldState extends State { SchedulerBinding.instance.addPostFrameCallback( (timeStamp) { + _overlayEntry?.remove(); + _overlayEntry = null; + final lastChildElement = - _lastFieldKey.currentContext!.findRenderObject() as RenderBox; + _lastFieldKey.currentContext?.findRenderObject() as RenderBox; final parentElement = - _fieldsKey.currentContext!.findRenderObject() as RenderBox; + _fieldsKey.currentContext?.findRenderObject() as RenderBox; - final parentLayoutElement = _fieldsLayoutKey.currentContext! - .findRenderObject() as RenderBox; + final parentLayoutElement = _fieldsLayoutKey.currentContext + ?.findRenderObject() as RenderBox; final event = GroupFieldsRendered( lastChildElement: lastChildElement, @@ -220,13 +217,9 @@ class _GroupsFieldState extends State { if (state is SuggestionsReady) { SchedulerBinding.instance.addPostFrameCallback((_) { - if (_overlayEntry != null) { - _overlayEntry!.remove(); - _overlayEntry = null; - } - if (state.widgets.isNotEmpty) { - final size = _textFieldKey.currentContext!.size!; + final size = _textFieldKey.currentContext?.size; + if (size == null) return; final constrains = BoxConstraints( minWidth: size.width, @@ -236,7 +229,7 @@ class _GroupsFieldState extends State { ); final textFieldElement = - _textFieldKey.currentContext!.findRenderObject() as RenderBox; + _textFieldKey.currentContext?.findRenderObject() as RenderBox; final offsetOfTextField = textFieldElement.localToGlobal(Offset.zero); @@ -246,34 +239,34 @@ class _GroupsFieldState extends State { offsetOfTextField.dy + textFieldElement.size.height, ); - Widget suggestionsContainer; - final suggestionsAreaBuilder = widget.suggestionsAreaBuilder; - if (suggestionsAreaBuilder == null) { - suggestionsContainer = GroupSuggestions( - bloc: _bloc, - suggestions: state.widgets, - fields: state.fields, - group: state.group, - ); - } else { - suggestionsContainer = suggestionsAreaBuilder( - constrains, - offset, - state.widgets, - state.fields, - state.group, - ); - } + final suggestionsContainer = suggestionsAreaBuilder == null + ? GroupSuggestions( + bloc: _bloc, + suggestions: state.widgets, + fields: state.fields, + group: state.group, + ) + : suggestionsAreaBuilder( + constrains, + offset, + state.widgets, + state.fields, + state.group, + ); + + _overlayEntry?.remove(); + _overlayEntry = null; - _overlayEntry = buildOverlayContainer( + final overlayEntry = buildOverlayContainer( child: suggestionsContainer, constrains: constrains, offset: offset, ); + _overlayEntry = overlayEntry; final overlay = Overlay.of(context); - overlay?.insert(_overlayEntry!); + overlay?.insert(overlayEntry); } }); } @@ -288,19 +281,20 @@ class _GroupsFieldState extends State { } _textEditingController.clear(); - _overlayEntry?.remove(); - _overlayEntry = null; SchedulerBinding.instance.addPostFrameCallback( (timeStamp) { + _overlayEntry?.remove(); + _overlayEntry = null; + final lastChildElement = - _lastFieldKey.currentContext!.findRenderObject() as RenderBox; + _lastFieldKey.currentContext?.findRenderObject() as RenderBox; final parentElement = - _fieldsKey.currentContext!.findRenderObject() as RenderBox; + _fieldsKey.currentContext?.findRenderObject() as RenderBox; - final parentLayoutElement = _fieldsLayoutKey.currentContext! - .findRenderObject() as RenderBox; + final parentLayoutElement = _fieldsLayoutKey.currentContext + ?.findRenderObject() as RenderBox; final event = GroupFieldsRendered( lastChildElement: lastChildElement, @@ -344,7 +338,48 @@ class _GroupsFieldState extends State { return Stack( alignment: Alignment.centerLeft, children: [ - buildTextField(constraints), + StreamBuilder( + stream: _bloc.stateStream + .where((event) => event is CursorPositionUpdate), + builder: (context, snapshot) { + final state = snapshot.data; + + final textFieldBuilder = widget.textFieldBuilder; + + if (state is CursorPositionUpdate) { + final controller = _textEditingController; + final cursorPosition = state.cursorPosition; + final fieldsSize = state.fieldsSize; + final lastFieldSize = state.lastFieldSize; + + return Container( + key: _textFieldKey, + child: RawKeyboardListener( + focusNode: _focusNode, + onKey: textFieldOnKeyPressed, + child: textFieldBuilder == null + ? DefaultTextField( + controller: controller, + cursorPosition: cursorPosition, + lastFieldSize: lastFieldSize, + onSubmitted: widget.onSubmitted, + textFieldFocusNode: _textFieldFocusNode, + ) + : textFieldBuilder( + context, + constraints, + controller, + cursorPosition, + fieldsSize, + lastFieldSize, + _textFieldFocusNode, + ), + ), + ); + } + return Container(); + }, + ), SizedBox( key: _fieldsLayoutKey, width: constraints.maxWidth - widget.textFieldWidth, @@ -372,16 +407,18 @@ class _GroupsFieldState extends State { ); } + /// When the widget is rendered, get the last child element, the parent element, + /// and the parent layout element, and then send a SizeChanged event to the bloc Future onSizeChanged() async { SchedulerBinding.instance.addPostFrameCallback((timeStamp) { final lastChildElement = _lastFieldKey.currentContext?.findRenderObject() as RenderBox?; final parentElement = - _fieldsKey.currentContext!.findRenderObject() as RenderBox; + _fieldsKey.currentContext?.findRenderObject() as RenderBox; final parentLayoutElement = - _fieldsLayoutKey.currentContext!.findRenderObject() as RenderBox; + _fieldsLayoutKey.currentContext?.findRenderObject() as RenderBox; final event = SizeChanged( lastChildElement: lastChildElement, @@ -393,107 +430,31 @@ class _GroupsFieldState extends State { }); } - Widget buildTextField(BoxConstraints constraints) { - return StreamBuilder( - stream: _bloc.stateStream.where((event) => event is CursorPositionUpdate), - builder: (context, snapshot) { - final state = snapshot.data; - - if (state is CursorPositionUpdate) { - final controller = _textEditingController; - final cursorPosition = state.cursorPosition; - final fieldsSize = state.fieldsSize; - final lastFieldSize = state.lastFieldSize; - - return Container( - key: _textFieldKey, - child: RawKeyboardListener( - focusNode: _focusNode, - onKey: (event) { - if (event is RawKeyUpEvent && - event.logicalKey == widget.keyForTriggerRemoveField) { - _isRemovedFieldKeyPressed = true; - final currentText = _textEditingController.text; - if (currentText.isEmpty) { - if (_overlayEntry != null) { - _overlayEntry!.remove(); - _overlayEntry = null; - } - - textFieldOnChangeHandler( - context: context, - fieldText: _textEditingController.text, - isRemoved: _isRemovedFieldKeyPressed, - ); - } - } else { - _isRemovedFieldKeyPressed = false; - } - }, - child: widget.textFieldBuilder == null - ? textFieldBuilder( - context: context, - constrains: constraints, - controller: controller, - cursorPosition: cursorPosition, - fieldsSize: fieldsSize, - lastFieldSize: lastFieldSize, - textFieldFocusNode: _textFieldFocusNode, - ) - : widget.textFieldBuilder!( - context, - constraints, - controller, - cursorPosition, - fieldsSize, - lastFieldSize, - _textFieldFocusNode, - ), - ), - ); - } - return Container(); - }, - ); - } - - /// textFieldBuilder - build twice. - /// First build with default cursor position - /// offset and second with other offset. - Widget textFieldBuilder({ - required BuildContext context, - required BoxConstraints constrains, - required TextEditingController controller, - required Offset cursorPosition, - required Size fieldsSize, - required Size lastFieldSize, - required FocusNode textFieldFocusNode, - }) { - return TextField( - focusNode: textFieldFocusNode, - onSubmitted: (_) => - widget.onSubmitted == null ? null : widget.onSubmitted!(), - controller: controller, - decoration: InputDecoration( - contentPadding: EdgeInsets.only( - top: cursorPosition.dy, - left: cursorPosition.dx, - bottom: cursorPosition.dy == 0 ? 0 : lastFieldSize.height / 2, - ), - ), - ); - } + /// If the user presses the key that is set to trigger the removal of the text + /// field, and the text field is empty, then remove the text field + /// + /// Args: + /// event (RawKeyEvent): The event that was triggered. + void textFieldOnKeyPressed(RawKeyEvent event) { + final text = _textEditingController.text; + + if (event is RawKeyUpEvent && + event.logicalKey == widget.keyForTriggerRemoveField) { + if (text.isEmpty) { + final event = TextFieldChanged( + textFieldValue: text, + isRemovedFieldKeyPressed: true, + ); - void textFieldOnChangeHandler({ - required BuildContext context, - required String fieldText, - required bool isRemoved, - }) { - final event = TextFieldChanged( - textFieldValue: fieldText, - isRemovedFieldKeyPressed: isRemoved, - ); + _bloc.eventController.add(event); + } + } else { + final event = TextFieldChanged( + textFieldValue: text, + isRemovedFieldKeyPressed: false, + ); - _bloc.eventController.add(event); + _bloc.eventController.add(event); + } } } diff --git a/lib/ui/default_text_field.dart b/lib/ui/default_text_field.dart index fadd706..125d2c7 100644 --- a/lib/ui/default_text_field.dart +++ b/lib/ui/default_text_field.dart @@ -1,25 +1,30 @@ import 'package:flutter/material.dart'; -class DefaultTextField extends StatefulWidget { - const DefaultTextField({super.key}); +/// It's a TextField that has a custom cursor position and a custom bottom padding +class DefaultTextField extends StatelessWidget { + final Size lastFieldSize; + final Offset cursorPosition; - @override - State createState() => _DefaultTextFieldState(); -} + final Function? onSubmitted; + final FocusNode? textFieldFocusNode; + final TextEditingController? controller; -class _DefaultTextFieldState extends State { - @override - void initState() { - // TODO: implement initState - super.initState(); - } + const DefaultTextField({ + super.key, + required this.lastFieldSize, + required this.cursorPosition, + this.onSubmitted, + this.textFieldFocusNode, + this.controller, + }); @override Widget build(BuildContext context) { + final callback = onSubmitted; + return TextField( focusNode: textFieldFocusNode, - onSubmitted: (_) => - widget.onSubmitted == null ? null : widget.onSubmitted!(), + onSubmitted: (_) => callback == null ? null : callback(), controller: controller, decoration: InputDecoration( contentPadding: EdgeInsets.only(