Flutter实现一个较好看的计算器

效果图

实现原理

第三方库

总体设计是基于375*812的,为统一尺寸,所以这里采用了flutter_screenutil库。同时为解决浮点数精度问题,使用了decimal
具体引入如下:

dependencies:
    ...
  flutter_screenutil: 0.5.3
  decimal: 0.3.5

总体布局

布局方式可以使用多种方式,如ListView,Stack等。本项目采用的是Stack+Positioned(动画显示部分用到),底部Positionedc数字及操作符号部分采用Column+Row。


布局伪代码

Stack(
    chindren:<Widget>[
        Positioned(),
        Positioned(),
        Positioned(
            left:0,
            right:0,
            bottom:0,
            child:Container(
                child:Colum(
                    Row(),
                    Row(),
                    Row(),
                    Row(
                        children:<Widget>[
                            buildNumberItem("00"),
                            buildNumberItem("0"),
                            buildNumberItem("."),
                            buildEqualItem()
                        ]
                    )
                )
            )
        )
    ]
);

CalculatorItem操作符组件

数字及操作符号的布局类似,我们统一建一个CalculatorItem组件

class CalculatorItem extends StatefulWidget {
  final Color activeColor;
  final Color color;
  final Widget child;
  final GestureTapCallback onTap;
  final double width;

  const CalculatorItem({Key key, this.activeColor, this.color, this.child, this.onTap, this.width}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _CalculatorItemState(activeColor, color, child, onTap, width);
  }
}

class _CalculatorItemState extends State<CalculatorItem> {
  bool active = false;
  final Color activeColor;
  final Color color;
  final Widget child;
  final GestureTapCallback onTap;
  final double width;

  _CalculatorItemState(this.activeColor, this.color, this.child, this.onTap, this.width);

  void _active(bool flag) {
    setState(() {
      active = flag;
    });
  }

  @override
  Widget build(BuildContext context) {
    double dp8 = WidgetUtil.getWidth(8);
    double dp24 = WidgetUtil.getWidth(24);
    double dp60 = WidgetUtil.getWidth(58);
    return GestureDetector(
        onTapDown: (arg) => _active(true),
        onTapUp: (arg) => _active(false),
        onTapCancel: () => _active(false),
        onTap: onTap,
        child: Container(
            margin: EdgeInsets.fromLTRB(dp8, dp24, dp8, 0),
            width: width ?? dp60,
            height: dp60,
            decoration:
                BoxDecoration(borderRadius: BorderRadius.circular(dp60 / 2), color: active ? activeColor : color),
            child: child));
  }
}

实现buildNumberItem创建数字及操作符组件

Widget buildNumberItem(String text) {
    return CalculatorItem(
      activeColor: Color(0xffD3D3D3),
      color: Color(0xFF1D3247),
      onTap: () => addText(text),
      child: Center(
        child: Text(
          text,
          style: TextStyle(
              fontSize: WidgetUtil.getFontSize(34),
              color: Colors.white,
              fontFamily: "din_medium"),
        ),
      ),
    );
  }

表达式布局及计算结果布局

我们通过改变Positioned的top值来实现计算结果的动画展示效果,当点击"="号,计算结果移至表达式,有一个字体向上平移放大效果。
首先创建表达式布局和计算结果布局,放在Stack的前两个子view中

Stack(
        children: <Widget>[
          Positioned(
            top: top1,
            right: 5,
            child: Container(
                height: 50,
                child: Center(
                  child: Text(
                    text1,
                    style: TextStyle(
                        fontSize: font1,
                        color: const Color(0xff333333),
                        fontFamily: "din_medium"),
                  ),
                )),
          ),
          Positioned(
              top: top2,
              right: 5,
              child: Offstage(
                offstage: hideFont2,
                child: Container(
                    height: 50,
                    child: Center(
                      child: Text(
                        text2,
                        style: TextStyle(
                            fontSize: font2,
                            color: const Color(0xffCCCCCC),
                            fontFamily: "din_medium"),
                      ),
                    )),
              )),
          Positioned()
        ]
)     

然后使用AnimationController实现动画效果,通过调用controller.forward()开始动画,Positioned(计算结果)通过top属性上移(移至表达式布局所在位置),当动画完成时,重置计算结果布局和表达式布局的位置,并且Offstage控制隐藏结果布局,完成动画效果。


  String text1 = "";
  String text2 = "";

  AnimationController controller;
  Animation<double> animation;
  double top1 = 100;
  double top2 = 150;
  double font1 = 35;
  double font2 = 20;
  bool hideFont2 = false;

  @override
  void initState() {
    super.initState();

    controller = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 300));
    CurvedAnimation easy =
        CurvedAnimation(parent: controller, curve: Curves.easeIn);
    animation = Tween(begin: 0.0, end: 1.0).animate(easy)
      ..addListener(() {
        double v = animation.value * 50;
        double level = text2.length > 15 ? fontLevel2 : fontLevel1;
        double f = animation.value * (level - fontLevel2);
        top1 = 100 - v;
        top2 = 150 - v;
        font2 = fontLevel2 + f;
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          top1 = 100;
          top2 = 150;
          hideFont2 = true;
          text1 = text2;
          calculator.reset(text2);
          text2 = "";
          setState(() {});
          controller.reset();
        } else if (status == AnimationStatus.forward) {
          setState(() {
            hideFont2 = false;
          });
        }
      });
  }
  void calculateAndUpdate() {
    if (text2.isEmpty) return;
    controller.forward();
  }
  ...
}

计算逻辑封装Calculator

我们用一个数组来存放计算表达式,如2×3-4÷5在数组里表现为["2","×","3","-","4","÷","5"],通过addText来接收进来的操作符如1,2,3,+,-,×,÷等,特殊的操作如清空,和删除等操作用C,<代替,于是在addText就有了这些逻辑。

class CalcuCalculatorlator {
  //计算器表达式项
  var expressionItems = [];

  String getCurrent() {
    var len = expressionItems.length;
    if (len == 0)
      return "";
    else
      return expressionItems[len - 1];
  }

  void addText(String text) {
    if (expressionItems.isEmpty && isOpt(text)) return;
    var current = getCurrent();
    if (current.isEmpty && !("C" == text || "<" == text || "00" == text)) {
      expressionItems.add(text);
      return;
    }
    switch (text) {
      case "1":
      case "2":
      case "3":
      case "4":
      case "5":
      case "6":
      case "7":
      case "8":
      case "9":
      case "0":
        if (isNumber(current)) {//如果当前是数字,替换当前数字
          current += text;
          replaceLast(current);
        } else {
          expressionItems.add(text);
        }
        break;
      case ".":
        if (isInteger(current)) {//如果是整数,添加小数点
          current = "$current.";
          replaceLast(current);
        }
        break;
      case "00":
        if (isNumber(current)) {
          current += "00";
          replaceLast(current);
        }
        break;
      case "+":
      case "-":
      case "×":
      case "÷":
        if (isNumber(current)) {//如果最后的操作符为数字,则新增操作符,否则替换操作符
          expressionItems.add(text);
        } else {
          replaceLast(text);
        }
        break;
      case "C":
        expressionItems.clear();//清空操作符
        break;
      case "<":
        deleteItem();           //退格操作
        break;
    }
  }
  ...
}

接下来就是进行四则运算了,我们通过两个栈实现。

String calculate() {
    DStack<Decimal> numbers = DStack(30);
    DStack<String> opts = DStack(30);
    int i = 0;
    if (expressionItems.isEmpty) return "";
    if (expressionItems.length == 1) return expressionItems[0];
    var end = expressionItems.length;
    if (isCurrentOpt()) end -= 1;
    while (i < end || !opts.isEmpty) {
      String str;
      if (i < end) str = expressionItems[i];
      if (str != null && isNumber(str)) {
        numbers.push(Decimal.parse(str));
        i++;
      } else if (str != null &&
          (opts.isEmpty || level(str) > level(opts.top))) {
        opts.push(str);
        i++;
      } else {
        try {
          Decimal right = numbers.pop();
          Decimal left = numbers.pop();
          String opt = opts.top;
          if ("+" == opt) {
            numbers.push(left + right);
          } else if ("-" == opt) {
            numbers.push(left - right);
          } else if ("×" == opt) {
            numbers.push(left * right);
          } else if ("÷" == opt) {
            numbers.push(left / right);
          }
          opts.pop();
        } catch (e) {
          print(e);
        }
      }
    }
    Decimal v = numbers.pop();
    var result = "$v";
    if (result.length > 15)//超出15位,使用指数表示
      return v.toStringAsExponential(5).replaceAll("+", "");
    return result;
  }

  int level(String str) {
    if ("×÷".contains(str)) {
      return 2;
    } else if ("+-".contains(str)) {
      return 1;
    } else {
      return 0;
    }
  }

项目地址

https://github.com/iamyours/flutter_demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357