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

elang發表於2019-03-04

通過挑戰 UI 製作來學習 Flutter

介紹

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

靈感

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

Flutter 從無到有構建所有 UI 元素(稱為 Widget)。沒有去封裝原生檢視,沒有使用基於 web 的 UI 元素。如同遊戲框架在遊戲中構建遊戲世界的方式(角色、敵人、宮殿…)那樣,Flutter 基於 Skia 圖形渲染引擎來繪製自己的 UI。這樣做真的很有意義,因為你可以完全控制你在螢幕上繪製的東西。這是否讓你在腦海中想到點什麼?對我來說,這聽起來似乎是在告訴我我可以更加容易地進行 UI 定製開發了。我嘗試挑戰一些 UI 效果實現來證明這一點。

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

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

怎麼做

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

關鍵是我不想僅僅為文字內容來做這個效果。這種效果在現代的移動應用中作為載入動畫是非常流行的。

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

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

現在是時候參考一下 Flutter 文件和示例程式碼去了解如何實現這種效果了。

經過研究我發現一個名為 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章