| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| |
| /// Flutter code sample for [TextFieldTapRegion]. |
| |
| void main() => runApp(const TapRegionApp()); |
| |
| class TapRegionApp extends StatelessWidget { |
| const TapRegionApp({super.key}); |
| |
| @override |
| Widget build(BuildContext context) { |
| return MaterialApp( |
| home: Scaffold( |
| appBar: AppBar(title: const Text('TextFieldTapRegion Example')), |
| body: const TextFieldTapRegionExample(), |
| ), |
| ); |
| } |
| } |
| |
| class TextFieldTapRegionExample extends StatefulWidget { |
| const TextFieldTapRegionExample({super.key}); |
| |
| @override |
| State<TextFieldTapRegionExample> createState() => _TextFieldTapRegionExampleState(); |
| } |
| |
| class _TextFieldTapRegionExampleState extends State<TextFieldTapRegionExample> { |
| int value = 0; |
| |
| @override |
| Widget build(BuildContext context) { |
| return ListView( |
| children: <Widget>[ |
| Center( |
| child: Padding( |
| padding: const EdgeInsets.all(20.0), |
| child: SizedBox( |
| width: 150, |
| height: 80, |
| child: IntegerSpinnerField( |
| value: value, |
| autofocus: true, |
| onChanged: (int newValue) { |
| if (value == newValue) { |
| // Avoid unnecessary redraws. |
| return; |
| } |
| setState(() { |
| // Update the value and redraw. |
| value = newValue; |
| }); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ], |
| ); |
| } |
| } |
| |
| /// An integer example of the generic [SpinnerField] that validates input and |
| /// increments by a delta. |
| class IntegerSpinnerField extends StatelessWidget { |
| const IntegerSpinnerField({ |
| super.key, |
| required this.value, |
| this.autofocus = false, |
| this.delta = 1, |
| this.onChanged, |
| }); |
| |
| final int value; |
| final bool autofocus; |
| final int delta; |
| final ValueChanged<int>? onChanged; |
| |
| @override |
| Widget build(BuildContext context) { |
| return SpinnerField<int>( |
| value: value, |
| onChanged: onChanged, |
| autofocus: autofocus, |
| fromString: (String stringValue) => int.tryParse(stringValue) ?? value, |
| increment: (int i) => i + delta, |
| decrement: (int i) => i - delta, |
| // Add a text formatter that only allows integer values and a leading |
| // minus sign. |
| inputFormatters: <TextInputFormatter>[ |
| TextInputFormatter.withFunction( |
| (TextEditingValue oldValue, TextEditingValue newValue) { |
| String newString; |
| if (newValue.text.startsWith('-')) { |
| newString = '-${newValue.text.replaceAll(RegExp(r'\D'), '')}'; |
| } else { |
| newString = newValue.text.replaceAll(RegExp(r'\D'), ''); |
| } |
| return newValue.copyWith( |
| text: newString, |
| selection: newValue.selection.copyWith( |
| baseOffset: newValue.selection.baseOffset.clamp(0, newString.length), |
| extentOffset: newValue.selection.extentOffset.clamp(0, newString.length), |
| ), |
| ); |
| }, |
| ) |
| ], |
| ); |
| } |
| } |
| |
| /// A generic "spinner" field example which adds extra buttons next to a |
| /// [TextField] to increment and decrement the value. |
| /// |
| /// This widget uses [TextFieldTapRegion] to indicate that tapping on the |
| /// spinner buttons should not cause the text field to lose focus. |
| class SpinnerField<T> extends StatefulWidget { |
| SpinnerField({ |
| super.key, |
| required this.value, |
| required this.fromString, |
| this.autofocus = false, |
| String Function(T value)? asString, |
| this.increment, |
| this.decrement, |
| this.onChanged, |
| this.inputFormatters = const <TextInputFormatter>[], |
| }) : asString = asString ?? ((T value) => value.toString()); |
| |
| final T value; |
| final T Function(T value)? increment; |
| final T Function(T value)? decrement; |
| final String Function(T value) asString; |
| final T Function(String value) fromString; |
| final ValueChanged<T>? onChanged; |
| final List<TextInputFormatter> inputFormatters; |
| final bool autofocus; |
| |
| @override |
| State<SpinnerField<T>> createState() => _SpinnerFieldState<T>(); |
| } |
| |
| class _SpinnerFieldState<T> extends State<SpinnerField<T>> { |
| TextEditingController controller = TextEditingController(); |
| |
| @override |
| void initState() { |
| super.initState(); |
| _updateText(widget.asString(widget.value)); |
| } |
| |
| @override |
| void dispose() { |
| controller.dispose(); |
| super.dispose(); |
| } |
| |
| @override |
| void didUpdateWidget(covariant SpinnerField<T> oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (oldWidget.asString != widget.asString || oldWidget.value != widget.value) { |
| final String newText = widget.asString(widget.value); |
| _updateText(newText); |
| } |
| } |
| |
| void _updateText(String text, {bool collapsed = true}) { |
| if (text != controller.text) { |
| controller.value = TextEditingValue( |
| text: text, |
| selection: collapsed |
| ? TextSelection.collapsed(offset: text.length) |
| : TextSelection(baseOffset: 0, extentOffset: text.length), |
| ); |
| } |
| } |
| |
| void _spin(T Function(T value)? spinFunction) { |
| if (spinFunction == null) { |
| return; |
| } |
| final T newValue = spinFunction(widget.value); |
| widget.onChanged?.call(newValue); |
| _updateText(widget.asString(newValue), collapsed: false); |
| } |
| |
| void _increment() { |
| _spin(widget.increment); |
| } |
| |
| void _decrement() { |
| _spin(widget.decrement); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return CallbackShortcuts( |
| bindings: <ShortcutActivator, VoidCallback>{ |
| const SingleActivator(LogicalKeyboardKey.arrowUp): _increment, |
| const SingleActivator(LogicalKeyboardKey.arrowDown): _decrement, |
| }, |
| child: Row( |
| children: <Widget>[ |
| Expanded( |
| child: TextField( |
| autofocus: widget.autofocus, |
| inputFormatters: widget.inputFormatters, |
| decoration: const InputDecoration( |
| border: OutlineInputBorder(), |
| ), |
| onChanged: (String value) => widget.onChanged?.call(widget.fromString(value)), |
| controller: controller, |
| textAlign: TextAlign.center, |
| ), |
| ), |
| const SizedBox(width: 12), |
| // Without this TextFieldTapRegion, tapping on the buttons below would |
| // increment the value, but it would cause the text field to be |
| // unfocused, since tapping outside of a text field should unfocus it |
| // on non-mobile platforms. |
| TextFieldTapRegion( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| Expanded( |
| child: OutlinedButton( |
| onPressed: _increment, |
| child: const Icon(Icons.add), |
| ), |
| ), |
| Expanded( |
| child: OutlinedButton( |
| onPressed: _decrement, |
| child: const Icon(Icons.remove), |
| ), |
| ), |
| ], |
| ), |
| ) |
| ], |
| ), |
| ); |
| } |
| } |