Flutter 購物車計數(shù)器 CounterStep

Flutter 可能會用到的計數(shù)器葡秒,支持[最小值, 最大值, 初始值]
全部刪除后賦最小值&選中賦值。

image.png

import 'dart:math';

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

class CounterStep extends StatefulWidget {
  const CounterStep({
    required this.min,
    required this.max,
    this.initial,
    this.step = 1,
    required this.valueChanged,
    Key? key,
  })  : assert(min < max),
        assert(initial == null || (initial >= min && initial <= max)),
        assert(step > 0),
        super(key: key);

  /// 最小值
  final int min;

  /// 最大值
  final int max;

  /// 初始值,如果初始值為null或無效奢赂,則初始值為[min]最小值
  final int? initial;

  /// 步進排霉,每次+/- Value變化數(shù)值,必須是正數(shù)
  final int step;

  /// Value值改變回調(diào)
  final ValueChanged<int> valueChanged;

  @override
  State<CounterStep> createState() => _CounterStepState();
}

class _CounterStepState extends State<CounterStep> {
 
 late final controller = TextEditingController(text: widget.initial ?? widget.min);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 30,
      decoration: ShapeDecoration(
        shape: RoundedRectangleBorder(
          side: BorderSide(
              color: Theme.of(context).colorScheme.outline, width: 0.5),
          borderRadius: BorderRadius.circular(4.0),
        ),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          buildReduceButton(),
          VerticalDivider(
              width: 1, color: Theme.of(context).colorScheme.outline),
          SizedBox(
            width: 100,
            child: buildTextFieldInput(),
          ),
          VerticalDivider(
              width: 1, color: Theme.of(context).colorScheme.outline),
          buildIncreaseButton(),
        ],
      ),
    );
  }

  Widget buildTextFieldInput() {
    return TextField(
      decoration: const InputDecoration(
        isDense: true,
        contentPadding: EdgeInsets.zero,
        border: InputBorder.none,
      ),
      controller: controller,
      keyboardType: TextInputType.number,
      textAlign: TextAlign.center,
      maxLines: 1,
      inputFormatters: [
        CounterTextInputFormatter(min: widget.min, max: widget.max),
      ],
      onChanged: (value) {
        widget.valueChanged(int.parse(value));
      },
    );
  }

  Widget buildReduceButton() {
    return InkWell(
      child: Icon(Icons.remove, color: Theme.of(context).colorScheme.outline),
      onTap: () {
        count = max(count - widget.step, widget.min);
        String text = count.toString();
        controller.value = TextEditingValue(text: text, selection: TextSelection.collapsed(offset: text.length));
      },
    );
  }

  Widget buildIncreaseButton() {
    return InkWell(
      child: Icon(Icons.add, color: Theme.of(context).colorScheme.outline),
      onTap: () {
        count = min(count + widget.step, widget.max);
        String text = count.toString();
        controller.value = TextEditingValue(text: text, selection: TextSelection.collapsed(offset: text.length));
      },
    );
  }
}

class CounterTextInputFormatter extends TextInputFormatter {
  final int min;
  final int max;

  CounterTextInputFormatter({required this.min, required this.max});

  int get maxLength => '$max'.length;
  late final regExp = RegExp("^\\d{0,$maxLength}?\$");

  @override
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue ) {
    String oldText = oldValue.text;
    String newText = newValue.text;

    if (newText.isEmpty) {
      String text =  '$min';
      return TextEditingValue(
        text: text,
        selection: TextSelection(baseOffset: 0, extentOffset: text.length),
      );
    }
    // 判定 新輸入值符合輸入預(yù)期
    bool isValid = (oldText.length > newText.length) ||
        regExp.hasMatch(newText);
    
    if (isValid) {
      // 如果以0開頭煮落、轉(zhuǎn)換為有效數(shù)字
      if (newText.startsWith('0')) {
        String text =  int.parse(newText).toString();
        return TextEditingValue(
          text: text,
          selection: TextSelection.collapsed(offset: text.length),
        );
      }
      return newValue;
    }

    return oldValue;
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末敞峭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蝉仇,更是在濱河造成了極大的恐慌旋讹,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轿衔,死亡現(xiàn)場離奇詭異沉迹,居然都是意外死亡,警方通過查閱死者的電腦和手機害驹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門鞭呕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宛官,你說我怎么就攤上這事琅拌。” “怎么了摘刑?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵进宝,是天一觀的道長。 經(jīng)常有香客問我枷恕,道長党晋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任徐块,我火速辦了婚禮未玻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胡控。我一直安慰自己扳剿,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布昼激。 她就那樣靜靜地躺著庇绽,像睡著了一般锡搜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瞧掺,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天耕餐,我揣著相機與錄音,去河邊找鬼辟狈。 笑死肠缔,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哼转。 我是一名探鬼主播明未,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼壹蔓!你這毒婦竟也來了趟妥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤庶溶,失蹤者是張志新(化名)和其女友劉穎煮纵,沒想到半個月后懂鸵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偏螺,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年匆光,在試婚紗的時候發(fā)現(xiàn)自己被綠了套像。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡终息,死狀恐怖夺巩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情周崭,我是刑警寧澤柳譬,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站续镇,受9級特大地震影響美澳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摸航,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一制跟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酱虎,春花似錦雨膨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撒妈。三九已至,卻和暖如春甥雕,著一層夾襖步出監(jiān)牢的瞬間踩身,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工社露, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挟阻,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓峭弟,卻偏偏與公主長得像附鸽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瞒瘸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容