[譯] 在 Flutter 中實現微光閃爍效果

經過挑戰 UI 製做來學習 Flutter前端

介紹

我是一個狂熱的移動開發者,Android 平臺和 iOS 平臺開發都有涉及。過去我不相信任何跨平臺的開發框架(Ionic,Xamarin,ReactNative),但如今我要講一下我遇到跨平臺開發框架 Flutter 以後的故事。android

靈感

做爲一名原生應用開發人員,我深深感到 UI 定製開發是多麼痛苦,即便是使用跨平臺開發框架去進行開發,這種痛苦也不能獲得緩解,有時甚至會更糟糕。但 Flutter 的出現讓我看到改善這種痛苦的但願。ios

Flutter 從無到有構建全部 UI 元素(稱爲 Widget)。沒有去封裝原生視圖,沒有使用基於 web 的 UI 元素。如同遊戲框架在遊戲中構建遊戲世界的方式(角色、敵人、宮殿…)那樣,Flutter 基於 Skia 圖形渲染引擎來繪製本身的 UI。這樣作真的頗有意義,由於你能夠徹底控制你在屏幕上繪製的東西。這是否讓你在腦海中想到點什麼?對我來講,這聽起來彷佛是在告訴我我能夠更加容易地進行 UI 定製開發了。我嘗試挑戰一些 UI 效果實現來證實這一點。git

我想到的一個挑戰是微光閃爍效果。這是一個很是常見的效果,若是你不熟悉這個名字,那麼想一下你喚醒手機時所顯示的「滑動解鎖」動畫。github

怎麼作

基本思路很簡單。動畫效果由從左到右移動的漸變所組成。web

關鍵是我不想僅僅爲文本內容來作這個效果。這種效果在現代的移動應用中做爲加載動畫是很是流行的。canvas

第一個初始想法是在內容佈局的頂部繪製一個不透明的漸變區域。雖然這能夠實現,但不是一個好方法。咱們不但願動畫效果弄髒咱們的整個白色背景。效果須要僅適用在給定的內容佈局上。後端

如今是時候參考一下 Flutter 文檔和示例代碼去了解如何實現這種效果了。bash

通過研究我發現一個名爲 SingleChildRenderObjectWidget 的基類,該基類露出一個 Canvas 對象。Canvas 是一個對象,它負責在屏幕上繪製內容,它有一個有趣的方法稱爲 saveLayer,它用來「在保存堆棧上保存當前變換和片斷的副本,而後建立一個新的組,用於保存後續調用」(摘自官方文檔)。這正是我須要的特性,它讓我能夠在特定內容佈局上實現微光閃爍效果。框架

實現

在 Flutter 中,有一個很不錯的小練習能夠參考。一個 widget 一般包含一個名爲 childchildren 的參數,它能夠幫助咱們將變換應用到後代 widget。咱們的 Shimmer widget 也有一個 child,它可讓咱們建立任何咱們想要的佈局,而後將它做爲 Shimmerchild 進行傳遞,Shimmer widget 反過來只會對那個 child 起做用。

import 'package:flutter/material.dart';

class Shimmer extends StatefulWidget {
  final Widget child;
  final Duration period;
  final Gradient gradient;
  
  Shimmer({Key key, this.child, this.period, this.gradient}): super(key: key);
  
  @override
  _ShimmerState createState() => _ShimmerState();
}

class _ShimmerState extends State<Shimmer> {
  @override
  Widget build(BuildContext context) {
    return _Shimmer();
  }
}
複製代碼

_Shimmer 是負責效果繪畫的內部類。它從 SingleChildRenderObjectWidget 擴展而來並重寫了 paint 方法來執行繪製任務。咱們使用 Canvas 對象的 saveLayerpaintChild 方法來捕捉咱們的 child 做爲一個圖層並在上面繪製漸變效果(帶上一點 BlendMode 的魔法)。

import 'package:flutter/rendering.dart';

class _Shimmer extends SingleChildRenderObjectWidget {
  final Gradient gradient;

  _Shimmer({Widget child, this.gradient})
      : super(child: child);

  @override
  _ShimmerFilter createRenderObject(BuildContext context) {
    return _ShimmerFilter(gradient);
  }
}

class _ShimmerFilter extends RenderProxyBox {
  final _clearPaint = Paint();
  final Paint _gradientPaint;
  final Gradient _gradient;

  _ShimmerFilter(this._gradient)
      : _gradientPaint = Paint()..blendMode = BlendMode.srcIn;

  @override
  bool get alwaysNeedsCompositing => child != null;
  
  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
      assert(needsCompositing);

      final rect = offset & child.size;
      _gradientPaint.shader = _gradient.createShader(rect);

      context.canvas.saveLayer(rect, _clearPaint);
      context.paintChild(child, offset);
      context.canvas.drawRect(rect, _gradientPaint);
      context.canvas.restore();
    }
  }
}
複製代碼

剩下的就是添加一個動效,讓咱們的效果動起來。這裏沒什麼特別的,咱們將建立一個動效來在繪製漸變以前從左到右移動 Canvas,這樣就能產生漸變移動的效果。

咱們在 _ShimmerState 中爲動效建立一個新的 AnimationController。咱們的 _Shimmer 類和 _ShimmerFilter 類還須要一個新變量(稱之爲 percent)來存儲該動畫執行的進度結果,並在每次 AnimationController 發出新值時調用 markNeedsPaint(這會讓 widget 從新繪製)。Canvas 的移動位移量能夠根據 percent 的值計算出來。

class _ShimmerState extends State<Shimmer> with TickerProviderStateMixin {
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(vsync: this, duration: widget.period)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.repeat();
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return _Shimmer(
      child: widget.child,
      gradient: widget.gradient,
      percent: controller.value,
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

}
複製代碼

flutter_shimmer3_1.dart

class _Shimmer extends SingleChildRenderObjectWidget {
  ...
  final double percent;

  _Shimmer({Widget child, this.gradient, this.percent})
      : super(child: child);

  @override
  _ShimmerFilter createRenderObject(BuildContext context) {
    return _ShimmerFilter(percent, gradient);
  }

  @override
  void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) {
    shimmer.percent = percent;
  }
}

class _ShimmerFilter extends RenderProxyBox {
  ...
  double _percent;

  _ShimmerFilter(this._percent, this._gradient)
      : _gradientPaint = Paint()..blendMode = BlendMode.srcIn;

  ...

  set percent(double newValue) {
    if (newValue != _percent) {
      _percent = newValue;
      markNeedsPaint();
    }
  }


  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
      assert(needsCompositing);

      final width = child.size.width;
      final height = child.size.height;
      Rect rect;
      double dx, dy;

      dx = _offset(-width, width, _percent);
      dy = 0.0;
      rect = Rect.fromLTWH(offset.dx - width, offset.dy, 3 * width, height);

      _gradientPaint.shader = _gradient.createShader(rect);

      context.canvas.saveLayer(offset & child.size, _clearPaint);
      context.paintChild(child, offset);
      context.canvas.translate(dx, dy);
      context.canvas.drawRect(rect, _gradientPaint);
      context.canvas.restore();
    }
  }

  double _offset(double start, double end, double percent) {
    return start + (end - start) * percent;
  }
}
複製代碼

flutter_shimmer3_2.dart

還不錯。咱們剛剛只用了大約 100 行代碼就實現了微光閃爍效果。這就是 Flutter 的美妙之處。

這只是個開始。接下來我會使用 Flutter 來挑戰更多更復雜的 UI 效果。我將在下一篇文章中分享個人成果。感謝你的閱讀!

備註:我已經將個人代碼發佈爲一個名爲 shimmer 的包.

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索