背景
最近在開發 Flutter
專案過程中遇到了一個很有意思的 bug,如果頁面在 InkWell
動畫期間彈出一個 Dialog
,那麼 InkWell
的動畫效果不會消失,如下圖右上角所示。以此為契機對 InkWell
的原始碼進行了探索和淺析
概述
InkWell
是 Flutter
提供的一個用於實現 Material
觸控水波效果的 Widget
,相當於 Android
裡的 Ripple
InkWell
繼承關係
InkWell
原始碼
class InkWell extends InkResponse {
/// Creates an ink well.
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// The [enableFeedback] and [excludeFromSemantics] arguments must not be
/// null.
const InkWell({
Key key,
Widget child,
...省略
bool enableFeedback = true,
bool excludeFromSemantics = false,
}) : super(
key: key,
child: child,
...省略
containedInkWell: true,
highlightShape: BoxShape.rectangle,
...省略
enableFeedback: enableFeedback,
excludeFromSemantics: excludeFromSemantics,
);
}
複製程式碼
原始碼非常簡單,其實就是具有特定屬性值的 InkResponse
,即 InkResponse
的特例
InkWell
顯示構成
顯示效果由 child
、highlight
背景動畫和 splash
水波紋動畫構成
動畫分析基於 InkResponse
分析思路
從顯示效果來看,觸控 InkWell
之後動畫就啟動了,所以從 GestureDetector
入手
@override
Widget build(BuildContext context) {
...省略
return GestureDetector(
onTapDown: enabled ? _handleTapDown : null,
onTap: enabled ? () => _handleTap(context) : null,
onTapCancel: enabled ? _handleTapCancel : null,
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
behavior: HitTestBehavior.opaque,
child: widget.child,
excludeFromSemantics: widget.excludeFromSemantics,
);
}
複製程式碼
接著看 onTapDown
回撥函式,_createInkFeature(details)
和 updateHighlight(true)
分別啟動了 splash
水波紋動畫和 highlight
背景動畫
void _handleTapDown(TapDownDetails details) {
final InteractiveInkFeature splash = _createInkFeature(details);
_splashes ??= HashSet<InteractiveInkFeature>();
_splashes.add(splash);
_currentSplash = splash;
if (widget.onTapDown != null) {
widget.onTapDown(details);
}
updateKeepAlive();
updateHighlight(true);
}
複製程式碼
接著看 _createInkFeature(details)
,水波紋動畫是以觸控點為中心向周邊擴散的,_handleTapDown(TapDownDetails details)
的引數 TapDownDetails
提供了 pointer position
;
這裡用 Android Studio
看原始碼有個坑,點內部的 create
方法會直接進入 InteractiveInkFeature
原始碼,實際上它是個父類,動畫實現是個空方法,真正實現 splash
水波紋動畫的是它的子類 InkSplash
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
...省略
splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
referenceBox: referenceBox,
position: position,
...省略
);
return splash;
}
複製程式碼
接著看 updateHighlight(true)
,實現 highlight
背景動畫的是 InkHighlight
void updateHighlight(bool value) {
...省略
if (_lastHighlight == null) {
final RenderBox referenceBox = context.findRenderObject();
_lastHighlight = InkHighlight(
controller: Material.of(context),
referenceBox: referenceBox,
...省略
updateKeepAlive();
} else {
_lastHighlight.activate();
}
... 省略
}
複製程式碼
動畫繪製
繼承關係
可以看出這倆其實是兄弟,他們有共同的祖先
接著看 InteractiveInkFeature
,它定義了兩個空方法和實現了一個 ink color
的 get
、set
方法,說明動畫相關的介面定義還在上級介面,即 InkFeature
abstract class InteractiveInkFeature extends InkFeature {
... 省略
void confirm() {
}
void cancel() {
}
/// The ink's color.
Color get color => _color;
Color _color;
set color(Color value) {
if (value == _color)
return;
_color = value;
controller.markNeedsPaint();
}
}
複製程式碼
最終定位到關鍵介面方法就是 paintFeature()
,接下來了解下 InkSplash
、InkHighlight
的具體實現
abstract class InkFeature {
...省略
///
/// The transform argument gives the coordinate conversion from the coordinate
/// system of the canvas to the coordinate system of the [referenceBox].
@protected
void paintFeature(Canvas canvas, Matrix4 transform);
}
複製程式碼
InkSplash
、InkHighlight
@override
void paintFeature(Canvas canvas, Matrix4 transform) {
// 獲取背景色,_alpha 型別是 Animation<int>,splash 顏色由淺到深就是它控制的
final Paint paint = Paint()..color = color.withAlpha(_alpha.value);
// 水波紋效果中心點,由此向外擴散
Offset center = _position;
if (_repositionToReferenceBox)
center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value);
// 矩陣變換
final Offset originOffset = MatrixUtils.getAsTranslation(transform);
canvas.save();
if (originOffset == null) {
canvas.transform(transform.storage);
} else {
canvas.translate(originOffset.dx, originOffset.dy);
}
// 定義水波紋邊界
if (_clipCallback != null) {
final Rect rect = _clipCallback();
if (_customBorder != null) {
canvas.clipPath(_customBorder.getOuterPath(rect, textDirection: _textDirection));
} else if (_borderRadius != BorderRadius.zero) {
canvas.clipRRect(RRect.fromRectAndCorners(
rect,
topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight,
bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight,
));
} else {
canvas.clipRect(rect);
}
}
// 獲取水波紋半徑大小,_radius 型別是 Animation<double>,水波紋擴散效果就是它的值由小到大變化造成的
canvas.drawCircle(center, _radius.value, paint);
canvas.restore();
}
複製程式碼
InkHighlight
相對比較簡單,實現原理和 InkSplash
是一樣的,只不過動畫只改變了顏色透明度,就不具體分析了
動畫開啟
文章開頭 InkWell
原始碼有這麼一句註釋,其實它是非常關鍵的資訊,通過跟蹤 InkFeature
的 paintFeature()
方法的呼叫方可以發現結果指向 _MaterialState
/// Must have an ancestor [Material] widget in which to cause ink reactions.
複製程式碼
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
...省略
List<InkFeature> _inkFeatures;
// InkSplash、InkHighlight 建構函式末尾都呼叫 addInkFeature()
@override
void addInkFeature(InkFeature feature) {
assert(!feature._debugDisposed);
assert(feature._controller == this);
_inkFeatures ??= <InkFeature>[];
assert(!_inkFeatures.contains(feature));
_inkFeatures.add(feature);
markNeedsPaint();
}
// InkFeature dispose() 函式末尾呼叫 _removeFeature()
void _removeFeature(InkFeature feature) {
assert(_inkFeatures != null);
_inkFeatures.remove(feature);
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Offset.zero & size);
// 迴圈遍歷所有的 InkFeature 並呼叫它們的 _paint() 繪製顯示效果
for (InkFeature inkFeature in _inkFeatures)
inkFeature._paint(canvas);
canvas.restore();
}
super.paint(context, offset);
}
}
複製程式碼
class _MaterialState extends State<Material> with TickerProviderStateMixin {
final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer');
...省略
@override
Widget build(BuildContext context) {
...省略
onNotification: (LayoutChangedNotification notification) {
// _MaterialState build 的時候繪製了 splash 水波紋動畫和 highlight 背景動畫,這也就印證了註釋裡要求 InkWell 在繪製樹中必須有個 Material 祖先
final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
renderer._didChangeLayout();
return true;
},
child: _InkFeatures(
key: _inkFeatureRenderer,
color: backgroundColor,
child: contents,
vsync: this,
)
);
... 省略
}
}
複製程式碼
動畫結束
動畫結束主要有兩種時機,回到 InkResponse
來看一段原始碼
class InkResponse extends StatefulWidget {
... 省略
void _handleTap(BuildContext context) {
_currentSplash?.confirm();
_currentSplash = null;
updateHighlight(false);
if (widget.onTap != null) {
if (widget.enableFeedback)
Feedback.forTap(context);
widget.onTap();
}
}
void _handleTapCancel() {
_currentSplash?.cancel();
_currentSplash = null;
if (widget.onTapCancel != null) {
widget.onTapCancel();
}
updateHighlight(false);
}
void _handleDoubleTap() {
_currentSplash?.confirm();
_currentSplash = null;
if (widget.onDoubleTap != null)
widget.onDoubleTap();
}
void _handleLongPress(BuildContext context) {
_currentSplash?.confirm();
_currentSplash = null;
if (widget.onLongPress != null) {
if (widget.enableFeedback)
Feedback.forLongPress(context);
widget.onLongPress();
}
}
@override
void deactivate() {
if (_splashes != null) {
final Set<InteractiveInkFeature> splashes = _splashes;
_splashes = null;
for (InteractiveInkFeature splash in splashes)
splash.dispose();
_currentSplash = null;
}
assert(_currentSplash == null);
_lastHighlight?.dispose();
_lastHighlight = null;
super.deactivate();
}
@override
Widget build(BuildContext context) {
...省略
return GestureDetector(
onTapDown: enabled ? _handleTapDown : null,
onTap: enabled ? () => _handleTap(context) : null,
onTapCancel: enabled ? _handleTapCancel : null,
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : null,
behavior: HitTestBehavior.opaque,
child: widget.child,
excludeFromSemantics: widget.excludeFromSemantics,
);
}
}
複製程式碼
GestureDetector
回撥方法中直接或間接呼叫InkFeature
的dispose()
State
生命週期deactivate()
方法 (應用返回後臺或者頁面跳轉會呼叫,彈出Dialog
不會呼叫) 中直接或間接呼叫InkFeature
的dispose()
總結
InkWell
在響應GestureDetector
的onTapDown()
回撥時建立了InkSplash
、InkHighlight
(均是InkFeature
的子類,各自實現了paintFeature()
)InkSplash
、InkHighlight
建立時將自己新增到_RenderInkFeatures
的InkFeature
佇列中InkWell
的Material
祖先在build()
的時候會呼叫_RenderInkFeatures
的paint()
_RenderInkFeatures
的paint()
會遍歷InkFeature
佇列並呼叫InkFeature
的paintFeature()
繪製動畫效果GestureDetector
回撥方法或State
生命週期deactivate()
方法直接或間接呼叫InkFeature
的dispose()
InkFeature
的dispose()
將自己從_RenderInkFeatures
的InkFeature
佇列中移除,動畫效果結束
@123lxw123, 本文版權屬於再惠研發團隊,歡迎轉載,轉載請保留出處。