Flutter InkWell 动画浅析
背景
最近在开发 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].@protectedvoid 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()@overridevoid 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();}@overridevoid 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');...省略@overrideWidget 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();}}@overridevoid 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();}@overrideWidget 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, 本文版权属于再惠研发团队,欢迎转载,转载请保留出处。
Flutter InkWell 动画浅析相关推荐
- Flutter Hero动画让你的APP页面切换充满动效 不一样的体验 不一样的细节处理
优美的应用体验 来自于细节的处理,更源自于码农的自我要求与努力,当然也需要码农年轻灵活的思维. 本文章实现的Demo效果,如下图所示: 1 首先是页面的主体 在这里使用的是Scaffold脚手架来构建 ...
- flutter 透明度动画_Flutter中的动画填充+不透明度动画✨
flutter 透明度动画 Flutter SDK provides us with many widgets which help us in animating elements on scree ...
- Flutter InkWell Ink组件
文章目录 Flutter InkWell & Ink组件 Flutter InkWell & Ink组件 InkWell组件可以在用户点击是出现水波纹效果. Ink组件可以将水波纹效果 ...
- Flutter 平移动画 — 4种实现方式
系列文章 Flutter 旋转动画 - RotationTransition Flutter 平移动画 - 4种实现方式 Flutter 淡入淡出与逐渐出现动画 Flutter 尺寸缩放.形状.颜色. ...
- Flutter 自定义动画 — 数字递增动画和文字逐行逐字出现或消失动画
系列文章 Flutter 旋转动画 - RotationTransition Flutter 平移动画 - 4种实现方式 Flutter 淡入淡出与逐渐出现动画 Flutter 尺寸缩放.形状.颜色. ...
- 神奇的 Flutter 文字动画-animated_text_kit
神奇的Flutter 文字动画,文本动画. https://pub.flutter-io.cn/packages/animated_text_kit ... 可以实现在FLutter状态下的文字的打字 ...
- flutter圆形动画菜单,Flow流式布局动画圆形菜单
题记 -- 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天. 重要消息 flutter中网络请求dio使用分析 视频教程在这里 Flutter 从入门实践到开发一个APP之UI基础 ...
- Flutter实现动画卡片式Tab导航 | 掘金技术征文
前言 本人接触Flutter不到一个月,深深感受到了这个平台的威力,于是不断学习,Flutter官方Example中的flutter_gallery,很好的展示了Flutter各个widget的功能 ...
- Flutter抖动动画、颤抖动画、Flutter文字抖动效果
题记 -- 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天. github? 你可能需要 百度同步 CSDN 网易云课堂教程 掘金 知乎 Flutter系列文章 头条同步 1 添加 ...
最新文章
- SiteMesh:一个优于Apache Tiles的Web页面布局、装饰框架
- 是c语言自带的数据类型吗_计协带你了解C语言程序
- 图的两种存储形式(邻接矩阵、邻接表)
- CheckBox控件
- sublime text3设置空格和tab键
- 可定制的PHP缩略图生成程式(需要GD库支持)
- LINUX C# 加载本地库的范例代码
- 前后端交互模式大总结 艾提拉 总结 attilax总结 目录 1. 通过ajax ajax就是js的网络api 完全解耦合 推荐	1 1.1. Query Ajax 操作函数	1 1.2. 服务
- MD文件阅读工具及配置
- java反射面试_总结Java反射面试题(附答案)
- macOS Big Sur初体验之自带五笔输入法质变
- pycharm格式化的html_pycharm格式化代码 常用快捷键
- 中兴配置dhcp服务器,中兴F623路由器如何投入使用dhcp服务器
- 宽度优先算法求解八数码问题
- 第一章 冯诺伊曼结构
- OIer常见问题与错误总结
- 传统会计和计算机会计的职能,论会计信息化对传统财务会计职能的影响
- 今天的学生要做汤饭吗
- 什么是敏捷开发?敏捷开发流程的8个步骤
- [CVPR2021-oral]Learning to Aggregate and Personalize 3D Face from In-the-Wild Photo Collection