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 1 : ScrollController get controller { 26 2 : order.clear(); 27 2 : values.clear(); 28 1 : return _controller; 29 : } 30 : 31 2 : int key(dynamic item) => item?.hashCode ?? 0; 32 : 33 1 : FocusNode last(dynamic item) { 34 2 : idButton = key(item); 35 4 : scope[idButton] ??= FocusNode(); 36 3 : 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 3 : bool _isControllerActive() => _controller.hasClients; 53 : 54 1 : double _getPosition(BuildContext context) { 55 1 : RenderObject? obj = context.findRenderObject(); 56 3 : return obj is RenderBox ? obj.localToGlobal(Offset.zero).dy : 0; 57 : } 58 : 59 3 : double _getMinPosition() => order.values 60 5 : .map((id) => scope[id]?.context) 61 2 : .where((element) => element != null) 62 3 : .map((e) => _getPosition(e!)) 63 1 : .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 1 : void scrollToFocusedElement(dynamic item, [int? idx]) { 70 0 : idx ??= key(item!); 71 3 : if (!_isControllerActive() || scope.isEmpty) { 72 : return; 73 : } 74 7 : if (order.containsValue(idx) && scope[idx] != null && scope[idx]!.context != null) { 75 7 : double position = _getPosition(scope[idx]!.context!) - _getMinPosition() - ThemeHelper.barHeight; 76 1 : if (position < 0) { 77 : position = 0; 78 : } 79 2 : _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 3 : values[idButton] = null; 98 2 : 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 5 : void blur() => scope.forEach((_, value) => value.unfocus()); 114 : 115 1 : void onFocus(dynamic item, [int? idx, bool isForced = true]) { 116 1 : idx ??= key(item); 117 1 : focus = idx; 118 4 : if ((order.containsValue(idx) || idx == idButton) && 119 2 : scope[idx] != null && 120 3 : scope[idx]!.context != null && 121 4 : scope[idx]!.context!.mounted) { 122 1 : blur(); 123 3 : scope[idx]?.requestFocus(); 124 : if (item != null) { 125 4 : _scrollTo.run(() => scrollToFocusedElement(item, idx)); 126 : } 127 : } 128 : force = isForced; 129 : } 130 : 131 1 : void dispose() { 132 4 : scope.forEach((_, node) => node.dispose()); 133 2 : scope.clear(); 134 2 : values.clear(); 135 2 : order.clear(); 136 2 : _controller.dispose(); 137 1 : focus = null; 138 : } 139 : }