zoukankan      html  css  js  c++  java
  • 【Flutter 实战】简约而不简单的计算器

    老孟导读:这是 【Flutter 实战】组件系列文章的最后一篇,其他组件地址:http://laomengit.com/guide/widgets/Text.html,接下来将会讲解动画系列,关注老孟,精彩不断。

    先看一下效果:

    大家学习UI编程语言时喜欢用哪个 App 当作第一个练手的项目呢?,我喜欢使用 计算器 ,可能是习惯了吧,学习 Android 和 React Native 都用此 App 当作练手的项目。

    下面我会一步一步的教大家如何实现此项目。

    整个项目的 UI 分为两大部分,一部分是顶部显示数字和计算结果,另一部分是底部的输入按钮。

    所以整体布局使用 Column,在不同分辨率的手机上,规定底部固定大小,剩余空间都由顶部组件填充,所以顶部组件使用 Expanded 扩充,代码如下:

    Container(
      padding: EdgeInsets.symmetric(horizontal: 18),
      child: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              alignment: Alignment.bottomRight,
              padding: EdgeInsets.only(right: 10),
              child: Text(
                '$_text',
                maxLines: 1,
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 48,
                    fontWeight: FontWeight.w400),
              ),
            ),
          ),
          SizedBox(
            height: 20,
          ),
          _CalculatorKeyboard(
            onValueChange: _onValueChange,
          ),
          SizedBox(
            height: 80,
          )
        ],
      ),
    )
    

    SizedBox 组件用于两个组件之间的间隔。

    _CalculatorKeyboard 是底部的输入按钮组件,也是此项目的重点,除了 0 这个按钮外,其余都是圆形按钮,不同之处是 高亮颜色(按住时颜色)、背景颜色、按钮文本、文本颜色不同,因此先实现一个按钮组件,代码如下:

    Ink(
      decoration: BoxDecoration(
          color: Color(0xFF363636),
          borderRadius: BorderRadius.all(Radius.circular(200))),
      child: InkWell(
        borderRadius: BorderRadius.all(Radius.circular(200)),
        highlightColor: Color(0xFF363636),
        child: Container(
           70,
          height: 70,
          alignment: Alignment.center,
          child: Text(
            '1',
            style: TextStyle(color: Colors.white, fontSize: 24),
          ),
        ),
      ),
    )
    

    0 这个按钮的宽度是两个按钮的宽度 + 两个按钮的间隙,所以 0 按钮代码如下:

    Ink(
      decoration: BoxDecoration(
          color: Color(0xFF363636),
          borderRadius: BorderRadius.all(Radius.circular(200))),
      child: InkWell(
        borderRadius: BorderRadius.all(Radius.circular(200)),
        highlightColor: Color(0xFF363636),
        child: Container(
           158,
          height: 70,
          alignment: Alignment.center,
          child: Text(
            '0',
            style: TextStyle(color: Colors.white, fontSize: 24),
          ),
        ),
      ),
    )
    

    将按钮组件进行封装,其中高亮颜色(按住时颜色)、背景颜色、按钮文本、文本颜色属性作为参数,封装如下:

    class _CalculatorItem extends StatelessWidget {
      final String text;
      final Color textColor;
      final Color color;
      final Color highlightColor;
      final double width;
      final ValueChanged<String> onValueChange;
    
      _CalculatorItem(
          {this.text,
          this.textColor,
          this.color,
          this.highlightColor,
          this.width,
          this.onValueChange});
    
      @override
      Widget build(BuildContext context) {
        return Ink(
          decoration: BoxDecoration(
              color: color, borderRadius: BorderRadius.all(Radius.circular(200))),
          child: InkWell(
            onTap: () {
              onValueChange('$text');
            },
            borderRadius: BorderRadius.all(Radius.circular(200)),
            highlightColor: highlightColor ?? color,
            child: Container(
               width ?? 70,
              height: 70,
              padding: EdgeInsets.only(left: width == null ? 0 : 25),
              alignment: width == null ? Alignment.center : Alignment.centerLeft,
              child: Text(
                '$text',
                style: TextStyle(color: textColor ?? Colors.white, fontSize: 24),
              ),
            ),
          ),
        );
      }
    }
    

    输入按钮

    输入按钮的布局使用 Wrap 布局组件,如果没有 0 这个组件也可以使用 GridView组件,按钮的数据:

    final List<Map> _keyboardList = [
      {
        'text': 'AC',
        'textColor': Colors.black,
        'color': Color(0xFFA5A5A5),
        'highlightColor': Color(0xFFD8D8D8)
      },
      {
        'text': '+/-',
        'textColor': Colors.black,
        'color': Color(0xFFA5A5A5),
        'highlightColor': Color(0xFFD8D8D8)
      },
      {
        'text': '%',
        'textColor': Colors.black,
        'color': Color(0xFFA5A5A5),
        'highlightColor': Color(0xFFD8D8D8)
      },
      {
        'text': '÷',
        'color': Color(0xFFE89E28),
        'highlightColor': Color(0xFFEDC68F)
      },
      {'text': '7', 'color': Color(0xFF363636)},
      {'text': '8', 'color': Color(0xFF363636)},
      {'text': '9', 'color': Color(0xFF363636)},
      {
        'text': 'x',
        'color': Color(0xFFE89E28),
        'highlightColor': Color(0xFFEDC68F)
      },
      {'text': '4', 'color': Color(0xFF363636)},
      {'text': '5', 'color': Color(0xFF363636)},
      {'text': '6', 'color': Color(0xFF363636)},
      {
        'text': '-',
        'color': Color(0xFFE89E28),
        'highlightColor': Color(0xFFEDC68F)
      },
      {'text': '1', 'color': Color(0xFF363636)},
      {'text': '2', 'color': Color(0xFF363636)},
      {'text': '3', 'color': Color(0xFF363636)},
      {
        'text': '+',
        'color': Color(0xFFE89E28),
        'highlightColor': Color(0xFFEDC68F)
      },
      {'text': '0', 'color': Color(0xFF363636), 'width': 158.0},
      {'text': '.', 'color': Color(0xFF363636)},
      {
        'text': '=',
        'color': Color(0xFFE89E28),
        'highlightColor': Color(0xFFEDC68F)
      },
    ];
    

    整个输入按钮组件:

    class _CalculatorKeyboard extends StatelessWidget {
      final ValueChanged<String> onValueChange;
    
      const _CalculatorKeyboard({Key key, this.onValueChange}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Wrap(
          runSpacing: 18,
          spacing: 18,
          children: List.generate(_keyboardList.length, (index) {
            return _CalculatorItem(
              text: _keyboardList[index]['text'],
              textColor: _keyboardList[index]['textColor'],
              color: _keyboardList[index]['color'],
              highlightColor: _keyboardList[index]['highlightColor'],
               _keyboardList[index]['width'],
              onValueChange: onValueChange,
            );
          }),
        );
      }
    }
    

    onValueChange 是点击按钮的回调,参数是当前按钮的文本,用于计算,下面说下计算逻辑:

    这里有4个变量:

    • _text:显示当前输入的数字和计算结果。
    • _beforeText:用于保存被加数,比如输入 5+1,保存 5 ,用于后面的计算。
    • _isResult:表示当前值是否为计算的结果,true:新输入数字直接显示,false:新输入数字和当前字符串相加,比如当前显示 5,如果是计算的结果,点击 1 时,直接显示1,否则显示 51。
    • _operateText:保存加减乘除。

    AC 按钮表示清空当前输入,显示 0,同时初始化其他变量:

    case 'AC':
      _text = '0';
      _beforeText = '0';
      _isResult = false;
      break;
    

    +/- 按钮表示对当前数字取反,比如 5->-5:

    case '+/-':
      if (_text.startsWith('-')) {
        _text = _text.substring(1);
      } else {
        _text = '-$_text';
      }
      break;
    

    % 按钮表示当前数除以100:

    case '%':
      double d = _value2Double(_text);
      _isResult = true;
      _text = '${d / 100.0}';
      break;
    

    +、-、x、÷ 按钮,保存当前 操作符号:

    case '+':
    case '-':
    case 'x':
    case '÷':
      _isResult = false;
      _operateText = value;
    

    0-9 和 . 按钮根据是否是计算结果和是否有操作符号进行显示:

    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
    case '.':
      if (_isResult) {
        _text = value;
      }
      if (_operateText.isNotEmpty && _beforeText.isEmpty) {
        _beforeText = _text;
        _text = '';
      }
      _text += value;
      if (_text.startsWith('0')) {
        _text = _text.substring(1);
      }
      break;
    

    = 按钮计算结果:

    case '=':
      double d = _value2Double(_beforeText);
      double d1 = _value2Double(_text);
      switch (_operateText) {
        case '+':
          _text = '${d + d1}';
          break;
        case '-':
          _text = '${d - d1}';
          break;
        case 'x':
          _text = '${d * d1}';
          break;
        case '÷':
          _text = '${d / d1}';
          break;
      }
      _beforeText = '';
      _isResult = true;
      _operateText = '';
      break;
    
    
    double _value2Double(String value) {
        if (_text.startsWith('-')) {
          String s = value.substring(1);
          return double.parse(s) * -1;
        } else {
          return double.parse(value);
        }
      }
    

    回过头来,发现代码仅仅只有250多行,当然App也是有不足的地方:

    1. 不足之一:计算结果逻辑,上面计算结果的逻辑是不完美的,当增加一个操作符(比如 取余),计算逻辑复杂度将会以指数级方式增加,那为什么还要用此方式?最重要的原因是计算结果逻辑不是此项目的重点,作为一个Flutter的入门项目重点是熟悉组件的使用,计算器的计算逻辑有一个比较著名的方式:后缀表达式的计算过程,然而此方式偏向于算法,对初学者非常不友好,因此,我采用了一种不完美但适合初学者的逻辑。
    2. 不足之二:此App没有考虑横屏的情况,为什么?因为横屏很可能导致整体布局发生变化,横屏时按钮是变大还是拉伸,或者拉伸间隙?不同的方式使用的布局会发生变化,因此,目前只考虑了竖屏的布局,实际项目中要考虑横屏情况吗?其实这是一个用户体验的问题,首先问问自己,为什么要横屏?横屏可以显著的提升用户体验吗?如果不能,为什么要花费大力气适配横屏呢?

    交流

    老孟Flutter博客地址(330个控件用法):http://laomengit.com

    欢迎加入Flutter交流群(微信:laomengit)、关注公众号【老孟Flutter】:

  • 相关阅读:
    BugReport-仿微信app
    成长、责任、和公司的关系
    团队如何做决定
    课堂练习
    课堂练习
    NABCD model
    课堂练习
    日程管理测试用例
    测试
    Bug报告
  • 原文地址:https://www.cnblogs.com/mengqd/p/13217179.html
Copyright © 2011-2022 走看看