整个项目的 UI 分为两大部分,一部分是顶部显示数字和计算结果,另一部分是底部的输入按钮。
calculator.dart 整体布局
整体布局使用 Column,在不同分辨率的手机上,规定底部固定大小,剩余空间都由顶部组件填充,所以顶部组件使用 Expanded 扩充。
import 'package:flutter/material.dart';
import 'calculator_keyboard.dart';
class CalCulatorDemo extends StatefulWidget {
@override
_CalCulatorDemoState createState() => _CalCulatorDemoState();
}
class _CalCulatorDemoState extends State<CalCulatorDemo> {
String _text = '';
_onValueChange(String value) {
print('_onValueChange => $value');
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.black,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
width: double.infinity,
child: Column(
children: [
Expanded(
child: Container(
color: Colors.green,
alignment: Alignment.bottomRight,
padding: EdgeInsets.only(right: 10),
child: Text(
'$_text',
maxLines: 1,
style: TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.w700),
),
),
),
SizedBox(height: 20),
//底部的输入按钮组件
CalculatorKeyboard(
onValueChange: _onValueChange,
),
SizedBox(height: 80),
],
),
),
);
}
}
calculator_item.dart 按钮封装
CalculatorKeyboard 是底部的输入按钮组件,也是此项目的重点,除了 0 这个按钮外,其余都是圆形按钮,不同之处是 高亮颜色(按住时颜色)、背景颜色、按钮文本、文本颜色不同,因此需要封装。
import 'package:flutter/material.dart';
typedef CalculatorValueChanged<T> = Function(T value);
class CalculatorItem extends StatelessWidget {
CalculatorItem({
key,
this.text,
this.textColor = Colors.white,
this.color,
this.highlightColor,
this.width,
this.onValueChange,
}) : super(key: key);
final String text; //文字
final Color textColor; //前景色
final Color color; //背景色
final Color highlightColor; //高亮背景色
final double width; //宽度
final CalculatorValueChanged<String> onValueChange; //点击按钮的回调,参数是当前按钮的文本
@override
Widget build(BuildContext context) {
return Ink(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(200),
),
child: InkWell(
borderRadius: BorderRadius.circular(200),
radius: 200,
highlightColor: highlightColor ?? color,
onTap: () {
onValueChange('$text');
},
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),
),
),
),
);
}
}
calculator_keyboard.dart 输入按钮布局
输入按钮的布局使用 Wrap 布局组件,如果没有 0 这个组件也可以使用 GridView组件。
import 'package:flutter/material.dart';
import 'calculator_item.dart';
class CalculatorKeyboard extends StatelessWidget {
CalculatorKeyboard({key, this.onValueChange}) : super(key: key);
final CalculatorValueChanged<String> onValueChange;
static const colorNum = Color(0xFF363636);
static const colorTopSymbol = Color(0xFFA5A5A5);
static const colorRightSymbol = Color(0xFFE89E28);
static const colorRightHighlight = Color(0xFFE89E28);
static const colorRightSymbolHighlight = Color(0xFFEDC68F);
final List<Map> _keyboardList = [
{'text': 'AC', 'color': colorTopSymbol, 'highlightColor': colorRightHighlight, 'textColor': Colors.black},
{'text': '+/-', 'color': colorTopSymbol, 'highlightColor': colorRightHighlight, 'textColor': Colors.black},
{'text': '%', 'color': colorTopSymbol, 'highlightColor': colorRightHighlight, 'textColor': Colors.black},
{'text': '÷', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
{'text': '7', 'color': colorNum},
{'text': '8', 'color': colorNum},
{'text': '9', 'color': colorNum},
{'text': 'x', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
{'text': '4', 'color': colorNum},
{'text': '5', 'color': colorNum},
{'text': '6', 'color': colorNum},
{'text': '-', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
{'text': '1', 'color': colorNum},
{'text': '2', 'color': colorNum},
{'text': '3', 'color': colorNum},
{'text': '+', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
// 0 这个按钮的宽度是两个按钮的宽度 + 两个按钮的间隙
{'text': '0', 'color': colorNum, 'width': 170.0},
{'text': '.', 'color': colorNum},
{'text': '=', 'color': colorRightSymbol, 'highlightColor': colorRightSymbolHighlight},
];
@override
Widget build(BuildContext context) {
return Wrap(
runSpacing: 18, //垂直间距
spacing: 30, //水平间距
children: _keyboardList.map((item) {
return CalculatorItem(
text: item['text'],
textColor: item['textColor'],
color: item['color'],
highlightColor: item['highlightColor'],
width: item['width'],
onValueChange: onValueChange,
);
}).toList(),
);
}
}
计算
这里有4个变量:
- _text:显示当前输入的数字和计算结果。
- _beforeText:用于保存被加数,比如输入 5+1,保存 5 ,用于后面的计算。
- _isResult:表示当前值是否为计算的结果,true:新输入数字直接显示,false:新输入数字和当前字符串相加,比如当前显示 5,如果是计算的结果,点击 1 时,直接显示1,否则显示 51。
- _operateText:保存加减乘除。
import 'package:flutter/material.dart';
import 'calculator_keyboard.dart';
class CalCulatorDemo extends StatefulWidget {
@override
_CalCulatorDemoState createState() => _CalCulatorDemoState();
}
class _CalCulatorDemoState extends State<CalCulatorDemo> {
String _text = '0'; //显示当前输入的数字和计算结果。
String _beforeText = '0'; //用于保存被加数,比如输入 5+1,保存 5 ,用于后面的计算。
bool _isResult = false; //表示当前值是否为计算的结果,true:新输入数字直接显示,false:新输入数字和当前字符串相加,
// 比如当前显示 5,如果是计算的结果,点击 1 时,直接显示1,否则显示 51。
String _operateText = ''; //保存加减乘除。
double _valueToDouble(String value) {
if (_text.startsWith('-')) {
String s = value.substring(1);
return double.parse(s) * -1;
} else {
return double.parse(value);
}
}
_onValueChange(String value) {
print('_onValueChange => $value');
setState(() {
switch (value) {
// 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 = _valueToDouble(_text);
_isResult = true;
_text = '${d / 100.0}';
break;
// +、-、x、÷ 按钮,保存当前 操作符号:
case '+':
case '-':
case 'x':
case '÷':
_isResult = false;
_operateText = value;
break;
// 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 = _valueToDouble(_beforeText);
double d1 = _valueToDouble(_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;
default:
}
});
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.black,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20),
width: double.infinity,
child: Column(
children: [
Expanded(
child: Container(
color: Colors.green,
alignment: Alignment.bottomRight,
padding: EdgeInsets.only(right: 10),
child: Text(
'$_text',
maxLines: 1,
style: TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.w700),
),
),
),
SizedBox(height: 20),
CalculatorKeyboard(
onValueChange: _onValueChange,
),
SizedBox(height: 80),
],
),
),
);
}
}
- 不足之一:计算结果逻辑,上面计算结果的逻辑是不完美的,当增加一个操作符(比如 取余),计算逻辑复杂度将会以指数级方式增加,那为什么还要用此方式?最重要的原因是计算结果逻辑不是此项目的重点,作为一个Flutter的入门项目重点是熟悉组件的使用,计算器的计算逻辑有一个比较著名的方式:后缀表达式的计算过程,然而此方式偏向于算法,对初学者非常不友好,因此,我采用了一种不完美但适合初学者的逻辑。
- 不足之二:此App没有考虑横屏的情况,为什么?因为横屏很可能导致整体布局发生变化,横屏时按钮是变大还是拉伸,或者拉伸间隙?不同的方式使用的布局会发生变化,因此,目前只考虑了竖屏的布局,实际项目中要考虑横屏情况吗?其实这是一个用户体验的问题,首先问问自己,为什么要横屏?横屏可以显著的提升用户体验吗?如果不能,为什么要花费大力气适配横屏呢?