Line data Source code
1 : // Copyright 2023 The terCAD team. All rights reserved. 2 : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file. 3 : 4 : import 'dart:collection'; 5 : import 'dart:math'; 6 : 7 : import 'package:app_finance/_classes/controller/delayed_call.dart'; 8 : import 'package:app_finance/_configs/theme_helper.dart'; 9 : import 'package:flutter/material.dart'; 10 : 11 : class FocusController { 12 : final DelayedCall _scrollTo = DelayedCall(300); 13 : final DelayedCall _focus = DelayedCall(300); 14 : final _controller = ScrollController(); 15 : 16 : final scope = <int?, FocusNode>{}; 17 : final values = <int?, dynamic>{}; 18 : final order = SplayTreeMap<int, int?>(); 19 : int? focus; 20 : int? idButton; 21 : static bool force = false; 22 : 23 1 : FocusController(); 24 : 25 0 : ScrollController get controller { 26 0 : order.clear(); 27 0 : values.clear(); 28 0 : return _controller; 29 : } 30 : 31 2 : int key(dynamic item) => item?.hashCode ?? 0; 32 : 33 0 : FocusNode last(dynamic item) { 34 0 : idButton = key(item); 35 0 : scope[idButton] ??= FocusNode(); 36 0 : return scope[idButton]!; 37 : } 38 : 39 1 : FocusNode bind(dynamic item, {required BuildContext context, dynamic value}) { 40 1 : final id = key(item); 41 2 : if (!order.containsValue(id)) { 42 4 : order[order.length] = id; 43 : } 44 3 : scope[id] ??= FocusNode(); 45 2 : values[id] = value; 46 : if (!force) { 47 3 : _focus.run(onEditingComplete); 48 : } 49 2 : return scope[id]!; 50 : } 51 : 52 0 : bool _isControllerActive() => _controller.hasClients; 53 : 54 0 : double _getPosition(BuildContext context) { 55 0 : RenderObject? obj = context.findRenderObject(); 56 0 : return obj is RenderBox ? obj.localToGlobal(Offset.zero).dy : 0; 57 : } 58 : 59 0 : double _getMinPosition() => order.values 60 0 : .map((id) => scope[id]?.context) 61 0 : .where((element) => element != null) 62 0 : .map((e) => _getPosition(e!)) 63 0 : .reduce(min); 64 : 65 6 : bool _isLast(int? idx) => idx == order[order.lastKey()]; 66 : 67 3 : TextInputAction getAction(dynamic item) => _isLast(key(item)) ? TextInputAction.done : TextInputAction.next; 68 : 69 0 : void scrollToFocusedElement(dynamic item, [int? idx]) { 70 0 : idx ??= key(item!); 71 0 : if (!_isControllerActive() || scope.isEmpty) { 72 : return; 73 : } 74 0 : if (order.containsValue(idx) && scope[idx] != null && scope[idx]!.context != null) { 75 0 : double position = _getPosition(scope[idx]!.context!) - _getMinPosition() - ThemeHelper.barHeight; 76 0 : if (position < 0) { 77 : position = 0; 78 : } 79 0 : _controller.animateTo( 80 : position, 81 : duration: const Duration(milliseconds: 600), 82 : curve: Curves.easeInOut, 83 : ); 84 : } 85 : } 86 : 87 1 : void onEditingComplete([dynamic item]) { 88 4 : int? targetKey = item == null ? focus ?? order.values.firstOrNull : null; 89 : int? idx; 90 : force = false; 91 : if (item != null) { 92 1 : idx = key(item); 93 2 : values[idx] = true; 94 : } 95 3 : List<int?> check = order.values.toList(); 96 1 : if (idButton != null) { 97 0 : values[idButton] = null; 98 0 : check.add(idButton); 99 : } 100 2 : for (int? key in check) { 101 6 : if (targetKey != null && key != targetKey && (values[key] == '' || values[key] == null)) { 102 1 : onFocus(null, key, false); 103 : targetKey = null; 104 : } 105 1 : if (key == idx) { 106 : targetKey = key; 107 : } 108 : } 109 : } 110 : 111 4 : bool isFocused(dynamic item) => key(item) == focus; 112 : 113 0 : void blur() => scope.forEach((_, value) => value.unfocus()); 114 : 115 1 : void onFocus(dynamic item, [int? idx, bool isForced = true]) { 116 0 : idx ??= key(item); 117 1 : focus = idx; 118 2 : if ((order.containsValue(idx) || idx == idButton) && 119 2 : scope[idx] != null && 120 3 : scope[idx]!.context != null && 121 0 : scope[idx]!.context!.mounted) { 122 0 : blur(); 123 0 : scope[idx]?.requestFocus(); 124 : if (item != null) { 125 0 : _scrollTo.run(() => scrollToFocusedElement(item, idx)); 126 : } 127 : } 128 : force = isForced; 129 : } 130 : 131 0 : void dispose() { 132 0 : scope.forEach((_, node) => node.dispose()); 133 0 : scope.clear(); 134 0 : values.clear(); 135 0 : order.clear(); 136 0 : _controller.dispose(); 137 0 : focus = null; 138 : } 139 : }