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

Question

Asked by Andrey S on November 23, 2021 (source).

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!

Answer

Question answered by Andrey S (source).

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;
  }
}
FLUTTER REGEX TEXTFIELD TEXTFORMFIELD TIMER
SHARE: