【Flutter 實戰】簡約而不簡單的計算器

老孟Flutter發表於2020-06-30

老孟導讀:這是 【Flutter 實戰】元件系列文章的最後一篇,其他元件地址:laomengit.com/guide/widge…,接下來將會講解動畫系列,關注老孟,精彩不斷。

先看一下效果:

大家學習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個控制元件用法):laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

相關文章