计算器例子 - 图1

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

calculator.dart 整体布局

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

  1. import 'package:flutter/material.dart';
  2. import 'calculator_keyboard.dart';
  3. class CalCulatorDemo extends StatefulWidget {
  4. @override
  5. _CalCulatorDemoState createState() => _CalCulatorDemoState();
  6. }
  7. class _CalCulatorDemoState extends State<CalCulatorDemo> {
  8. String _text = '';
  9. _onValueChange(String value) {
  10. print('_onValueChange => $value');
  11. }
  12. @override
  13. Widget build(BuildContext context) {
  14. return Material(
  15. color: Colors.black,
  16. child: Container(
  17. padding: EdgeInsets.symmetric(horizontal: 20),
  18. width: double.infinity,
  19. child: Column(
  20. children: [
  21. Expanded(
  22. child: Container(
  23. color: Colors.green,
  24. alignment: Alignment.bottomRight,
  25. padding: EdgeInsets.only(right: 10),
  26. child: Text(
  27. '$_text',
  28. maxLines: 1,
  29. style: TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.w700),
  30. ),
  31. ),
  32. ),
  33. SizedBox(height: 20),
  34. //底部的输入按钮组件
  35. CalculatorKeyboard(
  36. onValueChange: _onValueChange,
  37. ),
  38. SizedBox(height: 80),
  39. ],
  40. ),
  41. ),
  42. );
  43. }
  44. }

calculator_item.dart 按钮封装

CalculatorKeyboard 是底部的输入按钮组件,也是此项目的重点,除了 0 这个按钮外,其余都是圆形按钮,不同之处是 高亮颜色(按住时颜色)、背景颜色、按钮文本、文本颜色不同,因此需要封装。

  1. import 'package:flutter/material.dart';
  2. typedef CalculatorValueChanged<T> = Function(T value);
  3. class CalculatorItem extends StatelessWidget {
  4. CalculatorItem({
  5. key,
  6. this.text,
  7. this.textColor = Colors.white,
  8. this.color,
  9. this.highlightColor,
  10. this.width,
  11. this.onValueChange,
  12. }) : super(key: key);
  13. final String text; //文字
  14. final Color textColor; //前景色
  15. final Color color; //背景色
  16. final Color highlightColor; //高亮背景色
  17. final double width; //宽度
  18. final CalculatorValueChanged<String> onValueChange; //点击按钮的回调,参数是当前按钮的文本
  19. @override
  20. Widget build(BuildContext context) {
  21. return Ink(
  22. decoration: BoxDecoration(
  23. color: color,
  24. borderRadius: BorderRadius.circular(200),
  25. ),
  26. child: InkWell(
  27. borderRadius: BorderRadius.circular(200),
  28. radius: 200,
  29. highlightColor: highlightColor ?? color,
  30. onTap: () {
  31. onValueChange('$text');
  32. },
  33. child: Container(
  34. width: width ?? 70,
  35. height: 70,
  36. padding: EdgeInsets.only(left: width == null ? 0 : 25),
  37. alignment: width == null ? Alignment.center : Alignment.centerLeft,
  38. child: Text(
  39. '$text',
  40. style: TextStyle(color: textColor ?? Colors.white, fontSize: 24),
  41. ),
  42. ),
  43. ),
  44. );
  45. }
  46. }

calculator_keyboard.dart 输入按钮布局

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

  1. import 'package:flutter/material.dart';
  2. import 'calculator_item.dart';
  3. class CalculatorKeyboard extends StatelessWidget {
  4. CalculatorKeyboard({key, this.onValueChange}) : super(key: key);
  5. final CalculatorValueChanged<String> onValueChange;
  6. static const colorNum = Color(0xFF363636);
  7. static const colorTopSymbol = Color(0xFFA5A5A5);
  8. static const colorRightSymbol = Color(0xFFE89E28);
  9. static const colorRightHighlight = Color(0xFFE89E28);
  10. static const colorRightSymbolHighlight = Color(0xFFEDC68F);
  11. final List<Map> _keyboardList = [
  12. {'text': 'AC', 'color': colorTopSymbol, 'highlightColor': colorRightHighlight, 'textColor': Colors.black},
  13. {'text': '+/-', 'color': colorTopSymbol, 'highlightColor': colorRightHighlight, 'textColor': Colors.black},
  14. {'text': '%', 'color': colorTopSymbol, 'highlightColor': colorRightHighlight, 'textColor': Colors.black},
  15. {'text': '÷', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
  16. {'text': '7', 'color': colorNum},
  17. {'text': '8', 'color': colorNum},
  18. {'text': '9', 'color': colorNum},
  19. {'text': 'x', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
  20. {'text': '4', 'color': colorNum},
  21. {'text': '5', 'color': colorNum},
  22. {'text': '6', 'color': colorNum},
  23. {'text': '-', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
  24. {'text': '1', 'color': colorNum},
  25. {'text': '2', 'color': colorNum},
  26. {'text': '3', 'color': colorNum},
  27. {'text': '+', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
  28. // 0 这个按钮的宽度是两个按钮的宽度 + 两个按钮的间隙
  29. {'text': '0', 'color': colorNum, 'width': 170.0},
  30. {'text': '.', 'color': colorNum},
  31. {'text': '=', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
  32. ];
  33. @override
  34. Widget build(BuildContext context) {
  35. return Wrap(
  36. runSpacing: 18, //垂直间距
  37. spacing: 30, //水平间距
  38. children: _keyboardList.map((item) {
  39. return CalculatorItem(
  40. text: item['text'],
  41. textColor: item['textColor'],
  42. color: item['color'],
  43. highlightColor: item['highlightColor'],
  44. width: item['width'],
  45. onValueChange: onValueChange,
  46. );
  47. }).toList(),
  48. );
  49. }
  50. }

计算

这里有4个变量:

  • _text:显示当前输入的数字和计算结果。
  • _beforeText:用于保存被加数,比如输入 5+1,保存 5 ,用于后面的计算。
  • _isResult:表示当前值是否为计算的结果,true:新输入数字直接显示,false:新输入数字和当前字符串相加,比如当前显示 5,如果是计算的结果,点击 1 时,直接显示1,否则显示 51。
  • _operateText:保存加减乘除。
  1. import 'package:flutter/material.dart';
  2. import 'calculator_keyboard.dart';
  3. class CalCulatorDemo extends StatefulWidget {
  4. @override
  5. _CalCulatorDemoState createState() => _CalCulatorDemoState();
  6. }
  7. class _CalCulatorDemoState extends State<CalCulatorDemo> {
  8. String _text = '0'; //显示当前输入的数字和计算结果。
  9. String _beforeText = '0'; //用于保存被加数,比如输入 5+1,保存 5 ,用于后面的计算。
  10. bool _isResult = false; //表示当前值是否为计算的结果,true:新输入数字直接显示,false:新输入数字和当前字符串相加,
  11. // 比如当前显示 5,如果是计算的结果,点击 1 时,直接显示1,否则显示 51。
  12. String _operateText = ''; //保存加减乘除。
  13. double _valueToDouble(String value) {
  14. if (_text.startsWith('-')) {
  15. String s = value.substring(1);
  16. return double.parse(s) * -1;
  17. } else {
  18. return double.parse(value);
  19. }
  20. }
  21. _onValueChange(String value) {
  22. print('_onValueChange => $value');
  23. setState(() {
  24. switch (value) {
  25. // AC 按钮表示清空当前输入,显示 0,同时初始化其他变量:
  26. case 'AC':
  27. _text = '0';
  28. _beforeText = '0';
  29. _isResult = false;
  30. break;
  31. // +/- 按钮表示对当前数字取反,比如 5->-5:
  32. case '+/-':
  33. if (_text.startsWith('-')) {
  34. _text = _text.substring(1);
  35. } else {
  36. _text = '-$_text';
  37. }
  38. break;
  39. // % 按钮表示当前数除以100:
  40. case '%':
  41. double d = _valueToDouble(_text);
  42. _isResult = true;
  43. _text = '${d / 100.0}';
  44. break;
  45. // +、-、x、÷ 按钮,保存当前 操作符号:
  46. case '+':
  47. case '-':
  48. case 'x':
  49. case '÷':
  50. _isResult = false;
  51. _operateText = value;
  52. break;
  53. // 0-9 和 . 按钮根据是否是计算结果和是否有操作符号进行显示:
  54. case '0':
  55. case '1':
  56. case '2':
  57. case '3':
  58. case '4':
  59. case '5':
  60. case '6':
  61. case '7':
  62. case '8':
  63. case '9':
  64. case '.':
  65. // 重新开始计算
  66. if (_isResult) {
  67. _text = value;
  68. }
  69. //
  70. if (_operateText.isNotEmpty && _beforeText.isEmpty) {
  71. _beforeText = _text;
  72. _text = '';
  73. }
  74. _text += value;
  75. if (_text.startsWith('0')) {
  76. _text = _text.substring(1);
  77. }
  78. break;
  79. // = 按钮计算结果:
  80. case '=':
  81. double d = _valueToDouble(_beforeText);
  82. double d1 = _valueToDouble(_text);
  83. switch (_operateText) {
  84. case '+':
  85. _text = '${d + d1}';
  86. break;
  87. case '-':
  88. _text = '${d - d1}';
  89. break;
  90. case 'x':
  91. _text = '${d * d1}';
  92. break;
  93. case '÷':
  94. _text = '${d / d1}';
  95. break;
  96. }
  97. _beforeText = '';
  98. _isResult = true;
  99. _operateText = '';
  100. break;
  101. default:
  102. }
  103. });
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. return Material(
  108. color: Colors.black,
  109. child: Container(
  110. padding: EdgeInsets.symmetric(horizontal: 20),
  111. width: double.infinity,
  112. child: Column(
  113. children: [
  114. Expanded(
  115. child: Container(
  116. color: Colors.green,
  117. alignment: Alignment.bottomRight,
  118. padding: EdgeInsets.only(right: 10),
  119. child: Text(
  120. '$_text',
  121. maxLines: 1,
  122. style: TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.w700),
  123. ),
  124. ),
  125. ),
  126. SizedBox(height: 20),
  127. CalculatorKeyboard(
  128. onValueChange: _onValueChange,
  129. ),
  130. SizedBox(height: 80),
  131. ],
  132. ),
  133. ),
  134. );
  135. }
  136. }
  1. 不足之一:计算结果逻辑,上面计算结果的逻辑是不完美的,当增加一个操作符(比如 取余),计算逻辑复杂度将会以指数级方式增加,那为什么还要用此方式?最重要的原因是计算结果逻辑不是此项目的重点,作为一个Flutter的入门项目重点是熟悉组件的使用,计算器的计算逻辑有一个比较著名的方式:后缀表达式的计算过程,然而此方式偏向于算法,对初学者非常不友好,因此,我采用了一种不完美但适合初学者的逻辑。
  2. 不足之二:此App没有考虑横屏的情况,为什么?因为横屏很可能导致整体布局发生变化,横屏时按钮是变大还是拉伸,或者拉伸间隙?不同的方式使用的布局会发生变化,因此,目前只考虑了竖屏的布局,实际项目中要考虑横屏情况吗?其实这是一个用户体验的问题,首先问问自己,为什么要横屏?横屏可以显著的提升用户体验吗?如果不能,为什么要花费大力气适配横屏呢?