利用Flutter制作一個摸魚桌面版App

Win10商店上架瞭一款名為《摸魚》的App,在下載打開之後,這個App會讓你的電腦進入一個假更新的畫面,讓別人以為你的電腦正在升級,這時候你就可以休息一下,優雅地喝一杯咖啡。 

頓時這個念頭劃過瞭我的腦海:好東西,但是我用的是 MacBook,不能用這個應用。但是貌似我可以自己寫一個?

準備工作

年輕最需要的就是行動力,想到就幹,盡管我此刻正在理順 DevFest 的講稿,但絲毫不妨礙我用 10 分鐘寫一個 App。於是我打出瞭一套組合拳:

flutter config --enable-macos-desktop
flutter create --platforms=macos touch_fish_on_macos

一個支持 macOS 的 Flutter 項目就創建好瞭。(此時大約過去瞭 1 分鐘)

開始敲代碼

找到資源

我們首先需要一張高清無碼的 圖片,這裡你可以在網上進行搜尋,有一點需要註意的是,使用 LOGO 要註意使用場景帶來的版權問題。找到圖片後,丟到 assets/apple-logo.png,並在 pubspec.yaml 中加上資源引用:

flutter:
  use-material-design: true
+ assets:
+   - assets/apple-logo.png

思考佈局

我們來觀察一下 macOS 的啟動畫面,有幾個要點:

LOGO 在屏幕中間,固定大小約為 100dp;

LOGO 與進度條間隔約 100 dp;

進度條高度約 5dp,寬度約 200dp,圓角幾乎完全覆蓋高度,值部分為白色,背景部分為填充色+淺灰色邊框。

(別問我為什麼這些東西能觀察出來,問就是天天教 UI 改 UI。)

確認瞭大概的佈局模式,接下來我們開始搭佈局。(此時大約過去瞭 2 分鐘)

實現佈局

首先將 LOGO 居中、著色、設定寬度為 100,上下間隔 100:

return Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      const Spacer(),
      Padding(
        padding: const EdgeInsets.symmetric(vertical: 100),
        child: Image.asset(
          'assets/apple-logo.png',
          color: CupertinoColors.white, // 使用 Cupertino 系列的白色著色
          width: 100,
        ),
      ),
      const Spacer(),
    ],
  ),
);

然後在下方放一個相對靠上的進度條:

return Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      const Spacer(),
      Padding(
        padding: const EdgeInsets.symmetric(vertical: 100),
        child: Image.asset(
          'assets/apple-logo.png',
          color: CupertinoColors.white, // 使用 Cupertino 系列的白色
          width: 100,
        ),
      ),
      Expanded(
        child: Container(
          width: 200,
          alignment: Alignment.topCenter, // 相對靠上中部對齊
          child: DecoratedBox(
            border: Border.all(color: CupertinoColors.systemGrey), // 設置邊框
            borderRadius: BorderRadius.circular(10), // 這裡的值比高大就行
          ),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(10), // 需要進行圓角裁剪
            child: LinearProgressIndicator(
              value: 0.3, // 當前的進度值
              backgroundColor: CupertinoColors.lightBackgroundGray.withOpacity(.3),
              color: CupertinoColors.white,
              minHeight: 5, // 設置進度條的高度
            ),
          ),
        ),
      ),
    ],
  ),
);

到這裡你可以直接 run,一個靜態的界面已經做好瞭。(此時大約過去瞭 4 分鐘)

打開 App,你已經可以放在一旁掛機瞭,老板走到你的身邊,可能會跟你閑聊更新的內容。但是,更新界面不會動,能稱之為更新界面? 當老板一而再再而三地從你身邊經過,發現還是這個進度的時候,也許就已經把你的工資劃掉瞭,或者第二天你因為進辦公室在椅子上坐下而被辭退。

那麼下一步我們就要思考如何讓它動起來。

思考動畫

來看看啟動動畫大概是怎麼樣的:

開始是沒有進度條的;

進度條會逐級移動、速度不一定相等。

基於以上兩個條件,我設計瞭一種動畫處理方式:

  • 構造分段的時長 (Duration),可以自由組合由多個時長;
  • 動畫通過時長的數量決定每個時長最終的進度;
  • 每段時長控制起始值到結束值的間隔。

隻有三個條件,簡單到起飛,開動!(此時大約過去瞭 5 分鐘)

實現動畫

開局一個 AnimationController:

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  /// 巧用 late 初始化,節省代碼量
  late final AnimationController _controller = AnimationController(vsync: this);
 
  /// 啟動後等待的時長
  Duration get _waitingDuration => const Duration(seconds: 5);
 
  /// 分段的動畫時長
  List<Duration> get _periodDurations {
    return <Duration>[
      const Duration(seconds: 5),
      const Duration(seconds: 10),
      const Duration(seconds: 4),
    ];
  }
 
  /// 當前進行到哪一個分段
  final ValueNotifier<int> _currentPeriod = ValueNotifier<int>(1);

接下來實現動畫方法,采用瞭遞歸調用的方式,減少調用鏈的控制:

@override
void initState() {
  super.initState();
  // 等待對應秒數後,開始進度條動畫
  Future.delayed(_waitingDuration).then((_) => _callAnimation());
}
 
Future<void> _callAnimation() async {
  // 取當前分段
  final Duration _currentDuration = _periodDurations[currentPeriod];
  // 準備下一分段
  currentPeriod++;
  // 如果到瞭最後一個分段,取空
  final Duration? _nextDuration = currentPeriod < _periodDurations.length ? _periodDurations.last : null;
  // 計算當前分段動畫的結束值
  final double target = currentPeriod / _periodDurations.length;
  // 執行動畫
  await _controller.animateTo(target, duration: _currentDuration);
  // 如果下一分段為空,即執行到瞭最後一個分段,重設當前分段,動畫結束
  if (_nextDuration == null) {
    currentPeriod = 0;
    return;
  }
  // 否則調用下一分段的動畫
  await _callAnimation();
}

以上短短幾行代碼,就完美的實現瞭進度條的動畫操作。(此時大約過去瞭 8 分鐘)

最後一步,將動畫、分段二者與進度條綁定,在沒進入分段前不展示進度條,在動畫開始後展示對應的進度:

ValueListenableBuilder<int>(
  valueListenable: _currentPeriod,
  builder: (_, int period, __) {
    // 分段為0時,不展示
    if (period == 0) {
      return const SizedBox.shrink();
    }
    return DecoratedBox(
      decoration: BoxDecoration(
        border: Border.all(color: CupertinoColors.systemGrey),
        borderRadius: BorderRadius.circular(10),
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(10),
        child: AnimatedBuilder( // 使用 AnimatedBuilder,在動畫進行時觸發更新
          animation: _controller,
          builder: (_, __) => LinearProgressIndicator(
            value: _controller.value, // 將 controller 的值綁定給進度
            backgroundColor: CupertinoColors.lightBackgroundGray.withOpacity(.3),
            color: CupertinoColors.white,
            minHeight: 5,
          ),
        ),
      ),
    );
  },
)

大功告成,總共用時 10 分鐘,讓我們跑起來看看效果。(下圖 22.1 M)

打包發佈

發佈正式版的 macOS 應用較為復雜,但我們可以打包給自己使用,隻需要一行命令即可:flutter build macos。

成功後,產物將會輸出在 build/macos/Build/Products/Release/touch_fish_on_macos.app,雙擊即可使用

結語

可能大多數人都沒有想到,編寫一個 Flutter 應用,跑在 macOS 上,能有這麼簡單。當然,看似短暫的 10 分鐘並沒有包括安裝環境、搜索素材、提交到 git 的時間,但在這個時間范圍內,完成相關的事情也是綽綽有餘。

到此這篇關於利用Flutter制作一個摸魚桌面版App的文章就介紹到這瞭,更多相關Flutter摸魚App內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: