【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(
      width: 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(
      width: 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: 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'],
          width: _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】:

热门文章

暂无图片
编程学习 ·

学习Java第十六天

Java多态(下) 接口 接口定义了某一批类所要遵守的规范,接口不关心这些类的内部数据,页不关心这些类里方法的实现细节,它只规定这些类里必须提供某些方法 语法: [修饰符] interface 接口名 [ extends 父接口1,父接口2...] {零到多个常量定义...零到多个抽象方法的定义...零…
暂无图片
编程学习 ·

Fincy究竟如何成为东南亚值得信赖的首选支付平台?

“去中心化”的区块链网络提供了低成本的信用解决机制,基础规则在被写入区块链协议后,可以一直保障链上合约按规则运行,而不需要特定的中心化机构维护。区块链技术带来的“共同账簿”有潜力实现全球市场的跨场所实时结算,大幅提高交易后处理环节的运行效率,为全球支付的拓…
暂无图片
编程学习 ·

Spring-@Order注解

一、@Order 注解@Order的作用是定义Spring容器加载Bean的顺序 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order {/*** 默认最低优先级*/int value() default Ordered.LOWEST_PR…
暂无图片
编程学习 ·

[46]api接口文档的生成和与项目文档进行集成

导出api文档 登录Eolinker 平台,生成api文档因为我们不是付费用户,所以无法导出markdown文档,所以导出了html文档 集成到docsify 因为docsify默认时处理markdown的,所以如果我们放上一个html文件就会出现解析错误,所以我们需要对该路径启用不处理window.$docsify = {name:…
暂无图片
编程学习 ·

火狐浏览器网页不显示文字内容

网页的内容是个表格。表头只显示了 一部分内容。问题页面如下:但是在谷歌浏览器里显示正常。然后发现火狐里面多个标签他只显示一行,后面的都消失了。改好后效果如下:
暂无图片
编程学习 ·

如何理解numpy中的c_函数

其实,c_并不是一个函数,它只是CClass类的一个实例,而CClass是定义了切片方法__getitem__的类,所以c_就可以使用切片,仿佛就像一个函数一样。源代码是这样的#头部声明c_变量__all__ = [ravel_multi_index, unravel_index, mgrid, ogrid, r_, c_,s_, index_exp, ix_, ndenum…
暂无图片
编程学习 ·

Hadoop(二)——HDFS的 I/O 流操作

API操作的HDFS系统都是框架封装好的,可以采用 I/O 流的方式实现数据的上传和下载。 HDFS文件上传 1、需求:将本地D盘上的honglou.txt文件上传到HDFS根目录 2、代码块@Test public void putFileToHDFS() throws IOException,InterruptedException,URISyntaxException{//1、获取…
暂无图片
编程学习 ·

css border-radius的用法及自适应的椭圆

我们知道border-radius允许您为元素添加圆角边框! 而border-radius 属性是一个简写属性,用于设置四个 border-*-radius 属性。 如果省略 bottom-left,则与 top-right 相同。如果省略 bottom-right,则与 top-left 相同。如果省略 top-right,则与 top-left 相同。 先看个例子…
暂无图片
编程学习 ·

设计模式一——创建型模式(笔记)

简要描述 这些设计模式提供了一种方式:在创建对象的时候隐藏创建逻辑。(不是使用new运算符直接实例化对象) 带来的效果:使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。 包括:工厂模式,抽象工厂模式,单例模式,建造者模式,原型模式。 设计模式的六大原则:…
暂无图片
编程学习 ·

LCOF16 快速幂

链接 lcof16 快速幂 描述 实现函数double Power(double base, int exponent),求base的exponent次方。 分析十进制正整数n,二进制表示“bm…b3b2b1” 二进制转十进制,n = 1b1 + 2b2 + 4b3 + … + 2(m-1)bm 所以计算每一个二进制位的幂(x1,x2 ,x4,…),将所有位的幂相乘 利…
暂无图片
编程学习 ·

01:渗透测试及Kali Linux介绍

文章目录kali linux 渗透测试介绍安全问题的根源安全目标渗透测试渗透测试标准渗透测试项目渗透测试误区KALIKALI LINUX 策略 kali linux 渗透测试 介绍 安全问题的根源分层思想的优劣OSI采用了分层的结构化技术,共分七层,物理层、数据链路层、网络层、传输层、会话层、表示层…
暂无图片
编程学习 ·

近三位数增长,苏宁银行金融科技之花结出普惠金融之果

文|曾响铃来源|科技向令说(xiangling0815)美联储无限QE,2020年中国不设GDP目标,2万亿直达基层扶危纾困……国内疫情已经基本控制,经济基本面迎来全面复苏阶段,作为市场中最活跃的存在之一,小微企业在复苏过程中,面临的融资难等问题也被热议。在中国有一群喊着帮助小微企…
暂无图片
编程学习 ·

LeetCode——remove n-th node from end of list

题目描述: 给定一个链表,删除链表的倒数第n个节点并返回链表的头指针 例如, 给出的链表为:1->2->3->4->5, n= 2.↵↵ 删除了链表的倒数第n个节点之后,链表变为1->2->3->5. 备注: 题目保证n一定是合法的 请尝试只用一步操作完成该功能 解题思路: 删除…
暂无图片
编程学习 ·

Java语言基础(二)变量和数据类型

Java语言基础(二)二、变量和数据类型2.1 变量的基本概念2.2 变量的声明和使用2.3 变量使用是注意事项2.4 标识符的命名规则(笔试)关键字2.5 变量输入输出的案例实现2.6 变量输入输出案例的优化和手册介绍2.7 数据类型的分类2.8 常用的进制2.9 十进制与二进制之间的转换(1)正…
暂无图片
编程学习 ·

Postman调用 .net 的webservice

1、使用post方式调用,url以 asmx 止。2、设置header,content-type text/xml;charset=utf-8。3、body里选择 raw,参数模板如下:<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSch…
暂无图片
编程学习 ·

Golang语言基础教程:函数的返回值

函数的返回值1.1 什么是函数的返回值一个函数被调用后,返回给调用处的执行结果,叫做函数的返回值。调用处需要使用变量接收该结果1.2 一个函数可以返回多个值一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。package main ​ import "fmt" ​ fu…
暂无图片
编程学习 ·

网络安全技术及应用第3版 主编贾铁军等——教材习题 期末重点 复习题 知识提炼(第8章 防火墙应用技术)

参考教材:网络安全技术及应用 第3版 主编贾铁军等 第8章 防火墙应用技术填空题论述题1)==防火墙的 分类 及 主要技术 有哪些?==2)正确配置防火墙以后,是否能够必然保证网络安全?如果不是,试介绍防火墙的缺点。3) 防火墙阻止SYN Flood攻击,可使用SYN网关防护方式。说明S…
暂无图片
编程学习 ·

OpenCV学习笔记(一):opencv安装,边缘提取

OpenCV学习笔记(C++,win10+OpenCV4.1.2+VS2017) 记录了本人在图像处理相关学习过程中对opencv的使用心得,主要是供自己复习,但如果碰巧为你解决了问题,那就更好了。 由于本博客写作目的是用于复习,故顺序依照本人学习过程来进行编写。 如有错误,欢迎指正。 一、OpenCV的…
暂无图片
编程学习 ·

c++ windows console 快速编辑模式 关闭

问题: win10下编写Windows下控制台程序时发现程序经常被阻塞. 原因: windows Powershell 分快速编辑模式和标准模式. 当处于快速编辑模式时, 鼠标点击控制台会导致,改程序被阻塞. 且系统默认情况下,是快速编辑模式 解决: 通过代码重置控制台的属性.HANDLE hStdin = GetStdHand…