- 原文地址:Make shimmer effect in Flutter: Learn Flutter from UI challenges
- 原文作者:Hung HD
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:geniusq1981
通過挑戰 UI 製作來學習 Flutter
介紹
我是一個狂熱的移動開發者,Android 平臺和 iOS 平臺開發都有涉及。過去我不相信任何跨平臺的開發框架(Ionic,Xamarin,ReactNative),但現在我要講一下我遇到跨平臺開發框架 Flutter 之後的故事。
靈感
作為一名原生應用開發人員,我深深感到 UI 定製開發是多麼痛苦,即使是使用跨平臺開發框架去進行開發,這種痛苦也不能得到緩解,有時甚至會更糟糕。但 Flutter 的出現讓我看到改善這種痛苦的希望。
Flutter 從無到有構建所有 UI 元素(稱為 Widget)。沒有去封裝原生檢視,沒有使用基於 web 的 UI 元素。如同遊戲框架在遊戲中構建遊戲世界的方式(角色、敵人、宮殿…)那樣,Flutter 基於 Skia 圖形渲染引擎來繪製自己的 UI。這樣做真的很有意義,因為你可以完全控制你在螢幕上繪製的東西。這是否讓你在腦海中想到點什麼?對我來說,這聽起來似乎是在告訴我我可以更加容易地進行 UI 定製開發了。我嘗試挑戰一些 UI 效果實現來證明這一點。
我想到的一個挑戰是微光閃爍效果。這是一個非常常見的效果,如果你不熟悉這個名字,那麼想一下你喚醒手機時所顯示的“滑動解鎖”動畫。
怎麼做
基本思路很簡單。動畫效果由從左到右移動的漸變所組成。
關鍵是我不想僅僅為文字內容來做這個效果。這種效果在現代的移動應用中作為載入動畫是非常流行的。
第一個初始想法是在內容佈局的頂部繪製一個不透明的漸變區域。雖然這可以實現,但不是一個好方法。我們不希望動畫效果弄髒我們的整個白色背景。效果需要僅適用在給定的內容佈局上。
現在是時候參考一下 Flutter 文件和示例程式碼去了解如何實現這種效果了。
經過研究我發現一個名為 SingleChildRenderObjectWidget 的基類,該基類露出一個 Canvas 物件。Canvas 是一個物件,它負責在螢幕上繪製內容,它有一個有趣的方法稱為 saveLayer,它用來“在儲存堆疊上儲存當前變換和片段的副本,然後建立一個新的組,用於儲存後續呼叫”(摘自官方文件)。這正是我需要的特性,它讓我可以在特定內容佈局上實現微光閃爍效果。
實現
在 Flutter 中,有一個很不錯的小練習可以參考。一個 widget 通常包含一個名為 child 或 children 的引數,它可以幫助我們將變換應用到後代 widget。我們的 Shimmer widget 也有一個 child,它可以讓我們建立任何我們想要的佈局,然後將它作為 Shimmer 的 child 進行傳遞,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 物件的 saveLayer 和 paintChild 方法來捕捉我們的 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();
}
}
複製程式碼
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;
}
}
複製程式碼
還不錯。我們剛剛只用了大約 100 行程式碼就實現了微光閃爍效果。這就是 Flutter 的美妙之處。
這只是個開始。接下來我會使用 Flutter 來挑戰更多更復雜的 UI 效果。我將在下一篇文章中分享我的成果。感謝你的閱讀!
備註:我已經將我的程式碼釋出為一個名為 shimmer 的包.
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。