先看一下效果:
大家學習UI編程語言時喜歡用哪個 App 當作第一個練手的項目呢?,我喜歡使用 計算器 ,可能是習慣了吧,學習 Android 和 React Native 都用此 App 當作練手的項目。
下面我會一步一步的教大家如何實現此項目。
整個項目的 UI 分為兩大部分,一部分是頂部顯示數字和計算結果,另一部分是底部的輸入按鈕。
所以整體佈局使用 Column,在不同分辨率的手機上,規定底部固定大小,剩餘空間都由頂部組件填充,所以頂部組件使用 Expanded 擴充,代碼如下:
<code>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,
)
],
),
)/<code>
<code>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),
),
),
),
)/<code>
<code>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),
),
),
),
)/<code>
<code>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),
),
),
),
);
}
}/<code>
<code>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)
},
];/<code>
<code>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,
);
}),
);
}
}/<code>
<code>case 'AC':
_text = '0';
_beforeText = '0';
_isResult = false;
break;/<code>
<code>case '+/-':
if (_text.startsWith('-')) {
_text = _text.substring(1);
} else {
_text = '-$_text';
}
break;/<code>
- 不足之一:計算結果邏輯,上面計算結果的邏輯是不完美的,當增加一個操作符(比如 取餘),計算邏輯複雜度將會以指數級方式增加,那為什麼還要用此方式?最重要的原因是計算結果邏輯不是此項目的重點,作為一個Flutter的入門項目重點是熟悉組件的使用,計算器的計算邏輯有一個比較著名的方式:後綴表達式的計算過程,然而此方式偏向於算法,對初學者非常不友好,因此,我採用了一種不完美但適合初學者的邏輯。
- 不足之二:此App沒有考慮橫屏的情況,為什麼?因為橫屏很可能導致整體佈局發生變化,橫屏時按鈕是變大還是拉伸,或者拉伸間隙?不同的方式使用的佈局會發生變化,因此,目前只考慮了豎屏的佈局,實際項目中要考慮橫屏情況嗎?其實這是一個用戶體驗的問題,首先問問自己,為什麼要橫屏?橫屏可以顯著的提升用戶體驗嗎?如果不能,為什麼要花費大力氣適配橫屏呢?
回過頭來,發現代碼僅僅只有250多行,當然App也是有不足的地方:
<code>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);
}
}/<code>
= 按鈕計算結果:
<code>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;/<code>
0-9 和 . 按鈕根據是否是計算結果和是否有操作符號進行顯示:
<code>case '+':
case '-':
case 'x':
case '÷':
_isResult = false;
_operateText = value;/<code>
+、-、x、÷ 按鈕,保存當前 操作符號:
<code>case '%':
double d = _value2Double(_text);
_isResult = true;
_text = '${d / 100.0}';
break;/<code>
% 按鈕表示當前數除以100:
+/- 按鈕表示對當前數字取反,比如 5->-5:
AC 按鈕表示清空當前輸入,顯示 0,同時初始化其他變量:
- _text:顯示當前輸入的數字和計算結果。
- _beforeText:用於保存被加數,比如輸入 5+1,保存 5 ,用於後面的計算。
- _isResult:表示當前值是否為計算的結果,true:新輸入數字直接顯示,false:新輸入數字和當前字符串相加,比如當前顯示 5,如果是計算的結果,點擊 1 時,直接顯示1,否則顯示 51。
- _operateText:保存加減乘除。
這裡有4個變量:
onValueChange 是點擊按鈕的回調,參數是當前按鈕的文本,用於計算,下面說下計算邏輯:
整個輸入按鈕組件:
輸入按鈕的佈局使用 Wrap 佈局組件,如果沒有 0 這個組件也可以使用 GridView組件,按鈕的數據:
輸入按鈕
將按鈕組件進行封裝,其中高亮顏色(按住時顏色)、背景顏色、按鈕文本、文本顏色屬性作為參數,封裝如下:
而 0 這個按鈕的寬度是兩個按鈕的寬度 + 兩個按鈕的間隙,所以 0 按鈕代碼如下:
_CalculatorKeyboard 是底部的輸入按鈕組件,也是此項目的重點,除了 0 這個按鈕外,其餘都是圓形按鈕,不同之處是 高亮顏色(按住時顏色)、背景顏色、按鈕文本、文本顏色不同,因此先實現一個按鈕組件,代碼如下:
SizedBox 組件用於兩個組件之間的間隔。
## 交流
老孟Flutter博客地址(330個控件用法):http://laomengit.com
歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】
閱讀更多 老孟程序員 的文章