Skip to content

Commit b792f46

Browse files
committed
WIP: controller
1 parent f49cb36 commit b792f46

File tree

7 files changed

+237
-151
lines changed

7 files changed

+237
-151
lines changed

lib/src/base_spin_box.dart

Lines changed: 63 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import 'package:flutter/services.dart';
2424
import 'package:flutter/widgets.dart';
2525

26+
import 'spin_controller.dart';
2627
import 'spin_formatter.dart';
2728

2829
// ignore_for_file: public_member_api_docs
@@ -43,98 +44,86 @@ abstract class BaseSpinBox extends StatefulWidget {
4344
VoidCallback? get afterChange;
4445
bool get readOnly;
4546
FocusNode? get focusNode;
47+
SpinController? get controller;
4648
}
4749

4850
mixin SpinBoxMixin<T extends BaseSpinBox> on State<T> {
49-
late double _value;
50-
late double _cachedValue;
51+
late final SpinController _controller;
52+
late final TextEditingController _editor;
5153
late final FocusNode _focusNode;
52-
late final TextEditingController _controller;
5354

54-
double get value => _value;
55-
bool get hasFocus => _focusNode.hasFocus;
55+
SpinController get controller => _controller;
56+
TextEditingController get editor => _editor;
5657
FocusNode get focusNode => _focusNode;
57-
TextEditingController get controller => _controller;
58-
SpinFormatter get formatter => SpinFormatter(
59-
min: widget.min, max: widget.max, decimals: widget.decimals);
60-
61-
static double _parseValue(String text) => double.tryParse(text) ?? 0;
62-
String _formatText(double value) {
63-
return value.toStringAsFixed(widget.decimals).padLeft(widget.digits, '0');
64-
}
58+
TextInputFormatter get formatter => SpinFormatter(_controller);
6559

6660
Map<ShortcutActivator, VoidCallback> get bindings {
6761
return {
6862
// ### TODO: use SingleActivator fixed in Flutter 2.10+
6963
// https://github.com/flutter/flutter/issues/92717
70-
LogicalKeySet(LogicalKeyboardKey.arrowUp): _stepUp,
71-
LogicalKeySet(LogicalKeyboardKey.arrowDown): _stepDown,
72-
LogicalKeySet(LogicalKeyboardKey.pageUp): _pageStepUp,
73-
LogicalKeySet(LogicalKeyboardKey.pageDown): _pageStepDown,
64+
LogicalKeySet(LogicalKeyboardKey.arrowUp): _controller.stepUp,
65+
LogicalKeySet(LogicalKeyboardKey.arrowDown): _controller.stepDown,
66+
LogicalKeySet(LogicalKeyboardKey.pageUp): _controller.pageStepUp,
67+
LogicalKeySet(LogicalKeyboardKey.pageDown): _controller.pageStepDown,
7468
};
7569
}
7670

7771
@override
7872
void initState() {
7973
super.initState();
80-
_value = widget.value;
81-
_cachedValue = widget.value;
82-
_controller = TextEditingController(text: _formatText(_value));
83-
_controller.addListener(_updateValue);
74+
_controller = widget.controller ??
75+
SpinController(
76+
min: widget.min,
77+
max: widget.max,
78+
step: widget.step,
79+
pageStep: widget.pageStep,
80+
value: widget.value,
81+
decimals: widget.decimals,
82+
digits: widget.digits,
83+
canChange: widget.canChange,
84+
beforeChange: widget.beforeChange,
85+
afterChange: widget.afterChange,
86+
);
87+
_controller.addListener(_handleValueChange);
88+
_editor = TextEditingController(text: _controller.format(widget.value));
89+
_editor.addListener(_handleTextChange);
8490
_focusNode = widget.focusNode ?? FocusNode();
85-
_focusNode.addListener(_handleFocusChanged);
91+
_focusNode.addListener(_handleFocusChange);
8692
}
8793

8894
@override
8995
void dispose() {
90-
_focusNode.removeListener(_handleFocusChanged);
96+
_focusNode.removeListener(_handleFocusChange);
9197
if (widget.focusNode == null) {
9298
_focusNode.dispose();
9399
}
94-
_controller.dispose();
100+
_controller.removeListener(_handleValueChange);
101+
if (widget.controller == null) {
102+
_controller.dispose();
103+
}
104+
_editor.dispose();
95105
super.dispose();
96106
}
97107

98-
void _stepUp() => setValue(value + widget.step);
99-
void _stepDown() => setValue(value - widget.step);
100-
101-
void _pageStepUp() => setValue(value + widget.pageStep);
102-
void _pageStepDown() => setValue(value - widget.pageStep);
103-
104-
void _updateValue() {
105-
final v = _parseValue(_controller.text);
106-
if (v == _value) return;
107-
108-
if (widget.canChange?.call(v) == false) {
109-
controller.text = _formatText(_cachedValue);
110-
setState(() {
111-
_value = _cachedValue;
112-
});
113-
return;
114-
}
115-
116-
setState(() => _value = v);
117-
widget.onChanged?.call(v);
108+
void _handleValueChange() {
109+
widget.onChanged?.call(_controller.value);
110+
setState(() => _updateText(_controller.value));
118111
}
119112

120-
void setValue(double v) {
121-
final newValue = v.clamp(widget.min, widget.max);
122-
if (newValue == value) return;
123-
124-
if (widget.canChange?.call(newValue) == false) return;
125-
126-
widget.beforeChange?.call();
127-
setState(() => _updateController(value, newValue));
128-
widget.afterChange?.call();
113+
void _handleTextChange() {
114+
final value = _controller.parse(_editor.text);
115+
if (value != null && value >= controller.min && value <= controller.max) {
116+
_controller.value = value;
117+
}
129118
}
130119

131-
void _updateController(double oldValue, double newValue) {
132-
final text = _formatText(newValue);
133-
final selection = _controller.selection;
134-
final oldOffset = value.isNegative ? 1 : 0;
135-
final newOffset = _parseValue(text).isNegative ? 1 : 0;
120+
void _updateText(double newValue) {
121+
final text = _controller.format(newValue);
122+
final selection = _editor.selection;
123+
final oldOffset = _controller.value.isNegative ? 1 : 0;
124+
final newOffset = _controller.parse(text)?.isNegative == true ? 1 : 0;
136125

137-
_controller.value = _controller.value.copyWith(
126+
_editor.value = _editor.value.copyWith(
138127
text: text,
139128
selection: selection.copyWith(
140129
baseOffset: selection.baseOffset - oldOffset + newOffset,
@@ -144,37 +133,36 @@ mixin SpinBoxMixin<T extends BaseSpinBox> on State<T> {
144133
}
145134

146135
@protected
147-
void fixupValue(String value) {
148-
final v = _parseValue(value);
149-
if (value.isEmpty || (v < widget.min || v > widget.max)) {
150-
// will trigger notify to _updateValue()
151-
_controller.text = _formatText(_cachedValue);
152-
} else {
153-
_cachedValue = _value;
136+
void fixupValue(String text) {
137+
final v = _controller.parse(text);
138+
if (v == null) {
139+
_editor.text = _controller.format(_controller.value);
140+
} else if (v < _controller.min || v > _controller.max) {
141+
_controller.value = v.clamp(_controller.min, _controller.max);
154142
}
155143
}
156144

157-
void _handleFocusChanged() {
158-
if (hasFocus) {
145+
void _handleFocusChange() {
146+
if (focusNode.hasFocus) {
159147
setState(_selectAll);
160148
} else {
161-
fixupValue(_controller.text);
149+
fixupValue(_editor.text);
162150
}
163151
}
164152

165153
void _selectAll() {
166-
_controller.selection = _controller.selection
167-
.copyWith(baseOffset: 0, extentOffset: _controller.text.length);
154+
_editor.selection = _editor.selection
155+
.copyWith(baseOffset: 0, extentOffset: _editor.text.length);
168156
}
169157

170158
@override
171159
void didUpdateWidget(T oldWidget) {
172160
super.didUpdateWidget(oldWidget);
173161
if (oldWidget.value != widget.value) {
174-
_controller.removeListener(_updateValue);
175-
_value = _cachedValue = widget.value;
176-
_updateController(oldWidget.value, widget.value);
177-
_controller.addListener(_updateValue);
162+
_editor.removeListener(_handleTextChange);
163+
_controller.value = widget.value;
164+
_updateText(widget.value);
165+
_editor.addListener(_handleTextChange);
178166
}
179167
}
180168
}

lib/src/cupertino/spin_box.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import 'package:flutter/cupertino.dart';
2424

2525
import '../base_spin_box.dart';
26+
import '../spin_controller.dart';
2627
import 'spin_button.dart';
2728

2829
part 'third_party/default_rounded_border.dart';
@@ -84,6 +85,7 @@ class CupertinoSpinBox extends BaseSpinBox {
8485
this.beforeChange,
8586
this.afterChange,
8687
this.focusNode,
88+
this.controller,
8789
}) : assert(min <= max),
8890
keyboardType = keyboardType ??
8991
TextInputType.numberWithOptions(
@@ -199,6 +201,10 @@ class CupertinoSpinBox extends BaseSpinBox {
199201
@override
200202
final FocusNode? focusNode;
201203

204+
/// Controls the spinbox.
205+
@override
206+
final SpinController? controller;
207+
202208
/// Called when the user has changed the value.
203209
@override
204210
final ValueChanged<double>? onChanged;
@@ -267,7 +273,7 @@ class _CupertinoSpinBoxState extends State<CupertinoSpinBox> with SpinBoxMixin {
267273
final textField = CallbackShortcuts(
268274
bindings: bindings,
269275
child: CupertinoTextField(
270-
controller: controller,
276+
controller: editor,
271277
style: widget.textStyle,
272278
textAlign: widget.textAlign,
273279
keyboardType: widget.keyboardType,
@@ -315,19 +321,19 @@ class _CupertinoSpinBoxState extends State<CupertinoSpinBox> with SpinBoxMixin {
315321
final incrementButton = CupertinoSpinButton(
316322
step: widget.step,
317323
icon: widget.incrementIcon,
318-
enabled: widget.enabled && value < widget.max,
324+
enabled: widget.enabled && controller.value < controller.max,
319325
interval: widget.interval,
320326
acceleration: widget.acceleration,
321-
onStep: (step) => setValue(value + step),
327+
onStep: (step) => controller.value += step,
322328
);
323329

324330
final decrementButton = CupertinoSpinButton(
325331
step: widget.step,
326332
icon: widget.decrementIcon,
327-
enabled: widget.enabled && value > widget.min,
333+
enabled: widget.enabled && controller.value > controller.min,
328334
interval: widget.interval,
329335
acceleration: widget.acceleration,
330-
onStep: (step) => setValue(value - step),
336+
onStep: (step) => controller.value -= step,
331337
);
332338

333339
if (isHorizontal) {

lib/src/material/spin_box.dart

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import 'dart:math';
2525
import 'package:flutter/material.dart';
2626

2727
import '../base_spin_box.dart';
28+
import '../spin_controller.dart';
2829
import 'spin_box_theme.dart';
2930
import 'spin_button.dart';
3031

@@ -85,6 +86,7 @@ class SpinBox extends BaseSpinBox {
8586
this.beforeChange,
8687
this.afterChange,
8788
this.focusNode,
89+
this.controller,
8890
}) : assert(min <= max),
8991
keyboardType = keyboardType ??
9092
TextInputType.numberWithOptions(
@@ -197,6 +199,10 @@ class SpinBox extends BaseSpinBox {
197199
@override
198200
final FocusNode? focusNode;
199201

202+
/// Controls the spinbox.
203+
@override
204+
final SpinController? controller;
205+
200206
/// Called when the user has changed the value.
201207
@override
202208
final ValueChanged<double>? onChanged;
@@ -262,7 +268,7 @@ class SpinBox extends BaseSpinBox {
262268

263269
class _SpinBoxState extends State<SpinBox> with SpinBoxMixin {
264270
Color _activeColor(ThemeData theme) {
265-
if (hasFocus) {
271+
if (focusNode.hasFocus) {
266272
switch (theme.brightness) {
267273
case Brightness.dark:
268274
return theme.colorScheme.secondary;
@@ -275,7 +281,7 @@ class _SpinBoxState extends State<SpinBox> with SpinBoxMixin {
275281

276282
Color? _iconColor(ThemeData theme, String? errorText) {
277283
if (!widget.enabled) return theme.disabledColor;
278-
if (hasFocus && errorText == null) return _activeColor(theme);
284+
if (focusNode.hasFocus && errorText == null) return _activeColor(theme);
279285

280286
switch (theme.brightness) {
281287
case Brightness.dark:
@@ -308,23 +314,27 @@ class _SpinBoxState extends State<SpinBox> with SpinBoxMixin {
308314
.applyDefaults(theme.inputDecorationTheme);
309315

310316
final errorText =
311-
decoration.errorText ?? widget.validator?.call(controller.text);
317+
decoration.errorText ?? widget.validator?.call(editor.text);
312318

313319
final iconColor = widget.iconColor ??
314320
spinBoxTheme?.iconColor ??
315321
MaterialStateProperty.all(_iconColor(theme, errorText));
316322

317323
final states = <MaterialState>{
318324
if (!widget.enabled) MaterialState.disabled,
319-
if (hasFocus) MaterialState.focused,
325+
if (focusNode.hasFocus) MaterialState.focused,
320326
if (errorText != null) MaterialState.error,
321327
};
322328

323329
final decrementStates = Set<MaterialState>.of(states);
324-
if (value <= widget.min) decrementStates.add(MaterialState.disabled);
330+
if (controller.value <= controller.min) {
331+
decrementStates.add(MaterialState.disabled);
332+
}
325333

326334
final incrementStates = Set<MaterialState>.of(states);
327-
if (value >= widget.max) incrementStates.add(MaterialState.disabled);
335+
if (controller.value >= controller.max) {
336+
incrementStates.add(MaterialState.disabled);
337+
}
328338

329339
var bottom = 0.0;
330340
final isHorizontal = widget.direction == Axis.horizontal;
@@ -385,7 +395,7 @@ class _SpinBoxState extends State<SpinBox> with SpinBoxMixin {
385395
final textField = CallbackShortcuts(
386396
bindings: bindings,
387397
child: TextField(
388-
controller: controller,
398+
controller: editor,
389399
style: widget.textStyle,
390400
textAlign: widget.textAlign,
391401
textDirection: widget.textDirection,
@@ -410,10 +420,10 @@ class _SpinBoxState extends State<SpinBox> with SpinBoxMixin {
410420
step: widget.step,
411421
color: iconColor.resolve(incrementStates),
412422
icon: widget.incrementIcon,
413-
enabled: widget.enabled && value < widget.max,
423+
enabled: widget.enabled && controller.value < controller.max,
414424
interval: widget.interval,
415425
acceleration: widget.acceleration,
416-
onStep: (step) => setValue(value + step),
426+
onStep: (step) => controller.value += step,
417427
);
418428

419429
if (!widget.showButtons) return textField;
@@ -422,10 +432,10 @@ class _SpinBoxState extends State<SpinBox> with SpinBoxMixin {
422432
step: widget.step,
423433
color: iconColor.resolve(decrementStates),
424434
icon: widget.decrementIcon,
425-
enabled: widget.enabled && value > widget.min,
435+
enabled: widget.enabled && controller.value > controller.min,
426436
interval: widget.interval,
427437
acceleration: widget.acceleration,
428-
onStep: (step) => setValue(value - step),
438+
onStep: (step) => controller.value -= step,
429439
);
430440

431441
if (isHorizontal) {

0 commit comments

Comments
 (0)