背景
最近在开发 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, 本文版权属于再惠研发团队,欢迎转载,转载请保留出处。