2
votes

I want to force user to enters only one dot and 3 decimal points.

I found code below:

class NumberRemoveExtraDotFormatter extends TextInputFormatter {
  NumberRemoveExtraDotFormatter({this.decimalRange = 3}) : assert(decimalRange == null || decimalRange > 0);

  final int decimalRange;

  @override
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    var nValue = newValue.text;
    var nSelection = newValue.selection;

    Pattern p = RegExp(r'(\d+\.?)|(\.?\d+)|(\.?)');
    nValue = p.allMatches(nValue).map<String>((Match match) => match.group(0)).join();

    if (nValue.startsWith('.')) {
      nValue = '0.';
    } else if (nValue.contains('.')) {
      if (nValue.substring(nValue.indexOf('.') + 1).length > decimalRange) {
        nValue = oldValue.text;
      } else {
        if (nValue.split('.').length > 2) {
          var split = nValue.split('.');
          nValue = split[0] + '.' + split[1];
        }
      }
    }

    nSelection = newValue.selection.copyWith(
      baseOffset: math.min(nValue.length, nValue.length + 1),
      extentOffset: math.min(nValue.length, nValue.length + 1),
    );

    return TextEditingValue(text: Utils.addCommad(nValue), selection: nSelection, composing: TextRange.empty);
  }
}

but the problem is when user enters more than 3 decimal points and then want to remove, it doesn't. because numbers save in textformfield and they to remove until they reach to 3 decimal points and also when typing from middle of input cursor jump to end.

Also I want to shift number out from right if user enter more than 3 decimal points.

How can I achieve this?

3

3 Answers

1
votes

If you just want to force user to enters only one dot and 3 decimal points, this could work.

FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,3}'))

According to your comment:

  1. How to add thousands separator?
  2. Shifting number out not work. I want to shift number out if user start to typing in decimal part when decimal point reached at maximum. e.g. current value is 0.333 and user set cursor at second 3 (0.3|33) and type 2. then value must be 0.323.

We can use intl NumberFormat to format the number.

This is my code, I did not have a thorough and detailed test. If you find any bugs, please point them out.

UPDATE

when enter long number whit 0 maximumFractionDigits, wrong number will added. => this is not depends on maximumFractionDigits. it's happening always.

I think there has some unexpected behavior in the NumberFormat, and I changed it to custom method and it support negative number now.

class NumberInputFormatter extends TextInputFormatter {
  final int maximumFractionDigits;

  NumberInputFormatter({
    this.maximumFractionDigits = 3,
  }) : assert(maximumFractionDigits != null && maximumFractionDigits >= 0);

  @override
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    var newText = newValue.text;
    var selectionOffset = newValue.selection.extent.offset;
    bool isNegative = false;
    if (newText.startsWith('-')) {
      newText = newText.substring(1);
      isNegative = true;
    }
    if (newText.isEmpty) {
      return newValue;
    }
    if (newText.indexOf('.') != newText.lastIndexOf('.')) {
      // inputted more than one dot.
      return oldValue;
    }
    if (newText.startsWith('.') && maximumFractionDigits > 0) {
      newText = '0$newText';
      selectionOffset += 1;
    }
    while (newText.length > 1 && !newText.startsWith('0.') && newText.startsWith('0')) {
      newText = newText.substring(1);
      selectionOffset -= 1;
    }
    if (_decimalDigitsOf(newText) > maximumFractionDigits) {
      // delete the extra digits.
      newText = newText.substring(0, newText.indexOf('.') + 1 + maximumFractionDigits);
    }
    if (newValue.text.length == oldValue.text.length - 1 &&
        oldValue.text.substring(newValue.selection.extentOffset, newValue.selection.extentOffset + 1) == ',') {
      // in this case, user deleted the thousands separator, we should delete the digit number before the cursor.
      newText = newText.replaceRange(newValue.selection.extentOffset - 1, newValue.selection.extentOffset, '');
      selectionOffset -= 1;
    }
    if (newText.endsWith('.')) {
      // in order to calculate the selection offset correctly, we delete the last decimal point first.
      newText = newText.replaceRange(newText.length - 1, newText.length, '');
    }
    int lengthBeforeFormat = newText.length;
    newText = _removeComma(newText);
    if (double.tryParse(newText) == null) {
      // invalid decimal number
      return oldValue;
    }
    newText = _addComma(newText);
    selectionOffset += newText.length - lengthBeforeFormat; // thousands separator newly added
    if (maximumFractionDigits > 0 && newValue.text.endsWith('.')) {
      // decimal point is at the last digit, we need to append it back.
      newText = '$newText.';
    }
    if (isNegative) {
      newText = '-$newText';
    }
    return TextEditingValue(
      text: newText,
      selection: TextSelection.collapsed(offset: min(selectionOffset, newText.length)),
    );
  }

  static int _decimalDigitsOf(String text) {
    var index = text?.indexOf('.') ?? -1;
    return index == -1 ? 0 : text.length - index - 1;
  }

  static String _addComma(String text) {
    StringBuffer sb = StringBuffer();
    var pointIndex = text.indexOf('.');
    String integerPart;
    String decimalPart;
    if (pointIndex >= 0) {
      integerPart = text.substring(0, pointIndex);
      decimalPart = text.substring(pointIndex);
    } else {
      integerPart = text;
      decimalPart = '';
    }
    List<String> parts = [];
    while (integerPart.length > 3) {
      parts.add(integerPart.substring(integerPart.length - 3));
      integerPart = integerPart.substring(0, integerPart.length - 3);
    }
    parts.add(integerPart);
    sb.writeAll(parts.reversed, ',');
    sb.write(decimalPart);
    return sb.toString();
  }

  static String _removeComma(String text) {
    return text.replaceAll(',', '');
  }
}
0
votes

Try using this:

FilteringTextInputFormatter(RegExp(r'(^[0-9]*(?:\.[0-9]{0,3})?$)'), allow: true),

Basically the regex will try to match 0 or more occurences of digits followed by optional decimal followed by upto 3 digits after decimal. You can modify it to use negative value also ^(?:\-)?[0-9]*(?:\.[0-9]{0,3})?$.

0
votes

enter image description here

full code is here, (update you can also change for data by cursor)

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

class DecimalChecker extends TextInputFormatter {
  DecimalChecker({this.decimalRange = 3})
      : assert(decimalRange == null || decimalRange > 0);

  final int decimalRange;

  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    String valueTxt = newValue.text;
    TextSelection valueSet = newValue.selection;
    var newlength = newValue.text.length;
    var oldlength = oldValue.text.length;
    if (oldlength < newlength) {
      Pattern p = RegExp(r'(\d+\.?)|(\.?\d+)|(\.?)');
      valueTxt = p
          .allMatches(valueTxt)
          .map<String>((Match match) => match.group(0))
          .join();
      print("------>");
      if (valueTxt.startsWith('.')) {
        valueTxt = '0.';
      } else if (valueTxt.contains('.')) {
        if (valueTxt.substring(valueTxt.indexOf('.') + 1).length >
            decimalRange) {
          valueTxt = oldValue.text;
        } else {
          if (valueTxt.split('.').length > 2) {
            List<String> split = valueTxt.split('.');
            valueTxt = split[0] + '.' + split[1];
          }
        }
      }

      valueSet = newValue.selection.copyWith(
        baseOffset: math.min(valueTxt.length, valueTxt.length + 1),
        extentOffset: math.min(valueTxt.length, valueTxt.length + 1),
      );

      return TextEditingValue(
          text: valueTxt, selection: valueSet, composing: TextRange.empty);
    } else {
      return TextEditingValue(
          text: valueTxt, selection: valueSet, composing: TextRange.empty);
    }
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'My Decimal Check App'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController numberController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: EdgeInsets.fromLTRB(10.0, 0.0, 10.0, 0.0),
              child: TextField(
                controller: numberController,
                keyboardType: TextInputType.numberWithOptions(decimal: true),
                inputFormatters: [DecimalChecker()],
                decoration: InputDecoration(
                  hintText: "Please enter Number",
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}