Flutter input time in hh:mm:ss format in TextFormField or TextField without using pickers

3,079

Solution 1

I used TextInputFormatter in the text field instead of onChanged. This allows to access old value and format the new input depending on if the new character was added or deleted. It also allows to use RegExp to accept only numbers. This would be the solution:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:math' as math;

class TimeInputField extends StatefulWidget {
  TimeInputField({Key key}) : super(key: key);

  @override
  _TimeInputFieldState createState() => _TimeInputFieldState();
}

class _TimeInputFieldState extends State<TimeInputField> {
  TextEditingController _txtTimeController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: _txtTimeController,
      keyboardType: TextInputType.numberWithOptions(decimal: false),
      decoration: InputDecoration(
        hintText: '00:00:00',
      ),
      inputFormatters: <TextInputFormatter>[
        TimeTextInputFormatter() // This input formatter will do the job        
      ],
    );
  }
}

class TimeTextInputFormatter extends TextInputFormatter {
  RegExp _exp;
  TimeTextInputFormatter() {
    _exp = RegExp(r'^[0-9:]+$');
  }

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    if (_exp.hasMatch(newValue.text)) {
      TextSelection newSelection = newValue.selection;

      String value = newValue.text;
      String newText;

      String leftChunk = '';
      String rightChunk = '';

      if (value.length >= 8) {
        if (value.substring(0, 7) == '00:00:0') {
          leftChunk = '00:00:';
          rightChunk = value.substring(leftChunk.length + 1, value.length);
        } else if (value.substring(0, 6) == '00:00:') {
          leftChunk = '00:0';
          rightChunk = value.substring(6, 7) + ":" + value.substring(7);
        } else if (value.substring(0, 4) == '00:0') {
          leftChunk = '00:';
          rightChunk = value.substring(4, 5) +
              value.substring(6, 7) +
              ":" +
              value.substring(7);
        } else if (value.substring(0, 3) == '00:') {
          leftChunk = '0';
          rightChunk = value.substring(3, 4) +
              ":" +
              value.substring(4, 5) +
              value.substring(6, 7) +
              ":" +
              value.substring(7, 8) +
              value.substring(8);
        } else {
          leftChunk = '';
          rightChunk = value.substring(1, 2) +
              value.substring(3, 4) +
              ":" +
              value.substring(4, 5) +
              value.substring(6, 7) +
              ":" +
              value.substring(7);
        }
      } else if (value.length == 7) {
        if (value.substring(0, 7) == '00:00:0') {
          leftChunk = '';
          rightChunk = '';
        } else if (value.substring(0, 6) == '00:00:') {
          leftChunk = '00:00:0';
          rightChunk = value.substring(6, 7);
        } else if (value.substring(0, 1) == '0') {
          leftChunk = '00:';
          rightChunk = value.substring(1, 2) +
              value.substring(3, 4) +
              ":" +
              value.substring(4, 5) +
              value.substring(6, 7);
        } else {
          leftChunk = '';
          rightChunk = value.substring(1, 2) +
              value.substring(3, 4) +
              ":" +
              value.substring(4, 5) +
              value.substring(6, 7) +
              ":" +
              value.substring(7);
        }
      } else {
        leftChunk = '00:00:0';
        rightChunk = value;
      }

      if (oldValue.text.isNotEmpty && oldValue.text.substring(0, 1) != '0') {
        if (value.length > 7) {
          return oldValue;
        } else {
          leftChunk = '0';
          rightChunk = value.substring(0, 1) +
              ":" +
              value.substring(1, 2) +
              value.substring(3, 4) +
              ":" +
              value.substring(4, 5) +
              value.substring(6, 7);
        }
      }

      newText = leftChunk + rightChunk;

      newSelection = newValue.selection.copyWith(
        baseOffset: math.min(newText.length, newText.length),
        extentOffset: math.min(newText.length, newText.length),
      );

      return TextEditingValue(
        text: newText,
        selection: newSelection,
        composing: TextRange.empty,
      );
    }
    return oldValue;
  }
}

Solution 2

Flutter time input formatter with hh:mm format in TextFormField or TextField without using pickers, with max hours and max minutes :

import 'dart:math' as math;

import 'package:flutter/services.dart';

class TimeTextInputFormatter extends TextInputFormatter {
  TimeTextInputFormatter(
      {required this.hourMaxValue, required this.minuteMaxValue}) {
    _exp = RegExp(r'^$|[0-9:]+$');
  }
  late RegExp _exp;

  final int hourMaxValue;
  final int minuteMaxValue;
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    if (_exp.hasMatch(newValue.text)) {
    TextSelection newSelection = newValue.selection;

    final String value = newValue.text;
    String newText;

    String leftChunk = '';
    String rightChunk = '';

    if (value.length > 1 &&
        (int.tryParse(value.substring(0, 2)) ?? 0) == hourMaxValue)
    //this logic is to restrict value more than max hour
    {
      if (oldValue.text.contains(':')) {
        leftChunk = value.substring(0, 1);
      } else {
        leftChunk = '${value.substring(0, 2)}:';
        rightChunk = '00';
      }
    } else if (value.length > 5) {
      //this logic is to not allow more value
      leftChunk = oldValue.text;
    } else if (value.length == 5) {
      if ((int.tryParse(value.substring(3)) ?? 0) > minuteMaxValue) {
        //this logic is to restrict value more than max minute
        leftChunk = oldValue.text;
      } else {
        leftChunk = value;
      }
    } else if (value.length == 2) {
      if (oldValue.text.contains(':')) {
        //this logic is to delete : & value before : ,when backspacing
        leftChunk = value.substring(0, 1);
      } else {
        if ((int.tryParse(value) ?? 0) > hourMaxValue) {
          //this logic is to restrict value more than max hour
          leftChunk = oldValue.text;
        } else {
          //this logic is to add : with second letter
          leftChunk = '${value.substring(0, 2)}:';
          rightChunk = value.substring(2);
        }
      }
    } else {
      leftChunk = value;
    }
    newText = leftChunk + rightChunk;

    newSelection = newValue.selection.copyWith(
      baseOffset: math.min(newText.length, newText.length),
      extentOffset: math.min(newText.length, newText.length),
    );

    return TextEditingValue(
      text: newText,
      selection: newSelection,
    ); }
    return oldValue;
  }
}
Share:
3,079
Andrey Sorokin
Author by

Andrey Sorokin

Updated on January 02, 2023

Comments

  • Andrey Sorokin
    Andrey Sorokin over 1 year

    Can anyone help to figure out how to set up a TextField or TextFormField widget for time input in hh:mm:ss format?

    The formatting should be displayed in the field live, so the leading zeroes will be replaced while the user is typing.

    For example, if we need to enter 1 hour 59 minutes and 27 seconds, it would need to work as follows:

    • 00:00:00 - text hint before typing
    • 00:00:01 - starts typing and typed 1
    • 00:00:15 - typed 5
    • 00:01:59 - typed 9
    • 00:15:92 - typed 2 (92 seconds is acceptable here, it can be converted after the input is complete)
    • 01:59:27 - typed 7

    It is working in the similar manner on the timer in the Android's built in Clock app.

    I tried to use mask_text_input_formatter package, but it does not work as I need it to. Also, I do not wish to use time pickers.

    import 'package:flutter/services.dart';
    import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
    
    class TimeInputField extends StatefulWidget {
      TimeInputField({Key key}) : super(key: key);
    
      @override
      _TimeInputFieldState createState() => _TimeInputFieldState();
    }
    
    class _TimeInputFieldState extends State<TimeInputField> {
      TextEditingController _txtTimeController = TextEditingController();
    
      final MaskTextInputFormatter timeMaskFormatter =
          MaskTextInputFormatter(mask: '##:##:##', filter: {"#": RegExp(r'[0-9]')});
    
      @override
      Widget build(BuildContext context) {
        return TextFormField(
          controller: _txtTimeController,
          keyboardType: TextInputType.numberWithOptions(decimal: false),
          decoration: InputDecoration(
            hintText: '00:00:00',
          ),
          inputFormatters: <TextInputFormatter>[
            timeMaskFormatter
            // Not sure if it can be done with RegExp or a custom class here instead
          ],
        );
      }
    }
    

    Any help is greatly appreciated!

  • Andrey Sorokin
    Andrey Sorokin over 2 years
    Thank you for your help. It would not work for me though. I will not have a label for my field. Because the controller text is updated after the 6th digit case in the provided code, it would only update the text editing value after typing 6 digits. I need it updated after each digit. Updating the controller text value after each case in the switch would not let me type anything after the first digit.
  • dartKnightRises
    dartKnightRises over 2 years
    Try the latest one.
  • Andrey Sorokin
    Andrey Sorokin over 2 years
    That one will not work. If using this code, the controller value length will always be either 1 or 6 (when not empty). I will post my own solution. Thanks for your input again.
  • Jeremy Caney
    Jeremy Caney over 2 years
    Welcome to Stack Overflow, and thank you for returning to your question to share your solution with the community. Would you kindly edit your answer to include an explanation of your code? That will help future readers better understand what is going on, and especially those members of the community who are new to the language and struggling to understand the concepts.