1. 程式人生 > 實用技巧 >【Flutter 實戰】簡約而不簡單的計算器

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