【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】:

热门文章

暂无图片
编程学习 ·

7 模块与包

7 模块与包 Python中的模块和包一个py文件就是一个模块,py文件的文件名就是模块名。在 Python 里为了对模块分类管理,就需要划分不同的文件夹。多个有联系的模块可以将其放到同一个文件夹下,为了称呼方便,一般把 Python 里的一个代码文件夹称为一个包。7.1 导入模块导入模块…
暂无图片
编程学习 ·

【Linux基础编程】echo命令

01.文章目录 文章目录01.文章目录02.命令概述03.命令格式04.常用选项05.参考示例5.1 输出字符串5.2 输出变量PATH5.3 转义特殊字符5.4 重定向到文件中5.5 输出命令结果5.6 输出换行符5.7 输出退格符5.8 输出字符串不换行5.9 支持通配符5.10 指定输出颜色5.11 设置背景色5.12 文…
暂无图片
编程学习 ·

Java的ConcurrentHashMap 底层了解

最近有人问Java8 中ConcurrentHashMap 底层实现,这里简单列下。大家都知道 Java8 对 HashMap 、ConcurrentHashMap 进行了改进,前者非线程安全,后者线程安全。HashMap在Java 7 中,采用哈希表结构在Java 8 中,采用哈希表 + 红黑树ConcurrentHashMap在Java 7 中,采用分段的…
暂无图片
编程学习 ·

Layui 扩展字体图标

layui 目前(2020-06-28)提供了168个图标,但是很多时候这些图标中没有自己想要的,今天在项目中想找一个二维码的图标,但是在layui提供的图标中并没有,此时我们可以扩展图标(阿里巴巴矢量图标库 www.iconfont.cn)layui提供的图标也是取材于此文章目录1. 进入阿里巴巴矢量…
暂无图片
编程学习 ·

yolov5训练测试

书接上回,下面测试一下yolov5的训练。 参考文章目录官方教程1.数据集下载2.启动tensorboard3.训练4.结果4.1 打印信息4.2 测试训练的权重4.2 Apex 官方教程 官方tutorial(打不开的话,把整个仓库(迟早要下)下下来然后自己打开这个文件) 从这个位置开始读(前面工作在另一篇…
暂无图片
编程学习 ·

对简单文本的下载

import requests ser=requests.get() #一般为md格式 novel=ser.text k=open(要保存文件的位置和文件名,a+) #例:F:/desktop file/文件名.txt k.write(novel) k.close()什么是md文件, md全称markdown,markdown是一种标记语言。
暂无图片
编程学习 ·

四.面向对象

解释说明姓名 职位 动作张三 程序员 打卡,开会李四 前台 打卡,开会王五 财务 打卡,开会用表格表示一组数据,表结构理解为类,每一行数据对应一个对象; 姓名、职位相当于类中的属性; 动作早会相当于类中的方法; 面向过程:执行者思维,对于简单问题,比如开车步骤 按照12…
暂无图片
编程学习 ·

使用john软件进行账户弱口令检测实验

使用john软件进行账户弱口令检测实验 前言 在生产环境中,服务器账号的密码能够不被黑客入侵破解是尤为重要的,关系着业务正常运行的安全,所以在创建完账户的密码后,我们需要进行弱口令的检测,排查出是否有容易被破解的密码存在。 本次实验使用的破解密码软件是john-1.8.0版…
暂无图片
编程学习 ·

【项目总结】第三方OA对接、项目从零入手

前言近半年在平台项目的接触过程中,发现底层源码的能力很重要,有助于帮助我们理解项目代码,整理思路。代码思维能力也很重要。 项目经历 一、开发内容 第三方对接 华为云WeLink对接 2019-12 ~ 2020-04 企业微信小程序对接 2020-03 ~ 2020-05 好视通视频会议对接 2020-0…
暂无图片
编程学习 ·

centos7 64位使用心得

一、安装宝塔面板 1、安装宝塔面板,安装方法去官网查询 2、修改默认路径,面板设置 - 默认建站目录和默认备份目录修改为: /home /home/backup二、安装云锁 1、安装最新版、提示Install Complete(安装完成),下面是安装运行代码。 wget https://download.yunsuo.com.cn/v3/…
暂无图片
编程学习 ·

进程保活

进程保活一.为什么需要进程保活二.进程优先级前台进程(Foreground process)可见进程(Visible process)服务进程(Service process)后台进程(Background process)空进程(Empty process)三.保活方式1. 利用 Notification 提升权限2. 利用系统Service机制拉活3. 添加Manifest文件属…
暂无图片
编程学习 ·

sso单点登录

无状态登录 微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份带来的好处是什么呢?客户端请求不依赖服务端…
暂无图片
编程学习 ·

腾讯疑似回应被骗“辣椒酱不香了”!百度趁机诉苦!

导读:腾讯和老干妈怼怂,百度受伤,我们来看看到底怎么回事? 这一事件从腾讯告老干妈开始,理由是拖欠千万元广告费,故申请冻结老干妈1624万财产,结果老干妈直接发布声明:从未与腾讯公司进行过任何商业合作。 正当我们吃瓜群众等待下文的时候,贵阳警方发了一则通告:3人伪…
暂无图片
编程学习 ·

opencv画3d骨架图

效果: import matplotlib.pyplot as plt import numpy as np# h36m骨架连接顺序,每个骨架三个维度,分别为:起始关节,终止关节,左右关节标识(1 left 0 right),用来区别颜色 human36m_connectivity_dict = [[0, 1, 0], [1, 2, 0], [2, 6, 0], [5, 4, 1], [4, 3, 1], [3, 6…
暂无图片
编程学习 ·

OpenCV读取中文路径图像

引言 这几天做点小东西,涉及到OpenCV读取中文图像的问题如果直接读取中文路径的图像,往往返回[]import cv2cv_im = cv2.imread(‘老干妈.jpg’)缘起偶然发现opencv 读取图像,解决imread不能读取中文路径的问题文章,代码简单有效,im = cv2.imdecode(np.fromfile(im_name,dt…
暂无图片
编程学习 ·

Python科学计算系列12—积分变换

1.拉普拉斯变换及逆变换拉普拉斯变换公式拉普拉斯逆变换公式例子:代码如下:from sympy import * from sympy.integrals import laplace_transformt, s, a = symbols(t s a) # 拉普拉斯变换 F1 = laplace_transform(sin(a * t), t, s) F2 = laplace_transform(exp(a * t), t, …
暂无图片
编程学习 ·

深入javascript计划六:深入浅出异步

什么是进程?进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。通俗来讲就是:一个进程就是一个程序的运行实例(详细解释就是,启动一个程序的时候,操作…
暂无图片
编程学习 ·

吴说区块链:吉比特创始人「疑遭警方调查」暂时失联

据吴说区块链消息,吉比特创始人雷太国遭到举报暂时失联,疑似遭到警方调查。雷太国旗下主要有三大业务,分别是云算力销售平台吉比特、发币LTG(辣条哥,也是雷太国名字的三个首字母)、交易所CHANGE COIN(币兑)。吉比特自称,矿场分布在四川有4个,新疆有3个,内蒙古有3个,…
暂无图片
编程学习 ·

HDFS中将普通用户加入到supergroup组来访问HDFS

本机是linux系统,使用远程的hadoop。程序直接访问hdfs://node1:8020 会有权限问题。比较简单的解决粗暴方式是把用户加入到supergroup组。Hadoop本身的用户和组的关系,是同步Linux系统中的用户权限,但是HDFS和Linux的超级用户组又有一点差别,HDFS中的超级用户组是supergrou…