背景

最近在开发 Flutter 项目过程中遇到了一个很有意思的 bug,如果页面在 InkWell 动画期间弹出一个 Dialog,那么 InkWell 的动画效果不会消失,如下图右上角所示。以此为契机对 InkWell 的源码进行了探索和浅析

概述

InkWellFlutter 提供的一个用于实现 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 显示构成

显示效果由 childhighlight 背景动画和 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 colorgetset 方法,说明动画相关的接口定义还在上级接口,即 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(),接下来了解下 InkSplashInkHighlight 的具体实现

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);
}
复制代码

InkSplashInkHighlight

@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 源码有这么一句注释,其实它是非常关键的信息,通过跟踪 InkFeaturepaintFeature() 方法的调用方可以发现结果指向 _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 回调方法中直接或间接调用 InkFeaturedispose()
  • State 生命周期 deactivate() 方法 (应用返回后台或者页面跳转会调用,弹出 Dialog 不会调用) 中直接或间接调用 InkFeaturedispose()

总结

  • InkWell 在响应 GestureDetectoronTapDown() 回调时创建了 InkSplashInkHighlight (均是 InkFeature 的子类,各自实现了 paintFeature())
  • InkSplashInkHighlight 创建时将自己添加到 _RenderInkFeaturesInkFeature 队列中
  • InkWellMaterial 祖先在 build() 的时候会调用 _RenderInkFeaturespaint()
  • _RenderInkFeaturespaint() 会遍历 InkFeature 队列并调用 InkFeaturepaintFeature() 绘制动画效果
  • GestureDetector 回调方法或 State 生命周期 deactivate() 方法直接或间接调用 InkFeaturedispose()
  • InkFeaturedispose() 将自己从 _RenderInkFeaturesInkFeature 队列中移除,动画效果结束

@123lxw123, 本文版权属于再惠研发团队,欢迎转载,转载请保留出处。

Flutter InkWell 动画浅析相关推荐

  1. Flutter Hero动画让你的APP页面切换充满动效 不一样的体验 不一样的细节处理

    优美的应用体验 来自于细节的处理,更源自于码农的自我要求与努力,当然也需要码农年轻灵活的思维. 本文章实现的Demo效果,如下图所示: 1 首先是页面的主体 在这里使用的是Scaffold脚手架来构建 ...

  2. flutter 透明度动画_Flutter中的动画填充+不透明度动画✨

    flutter 透明度动画 Flutter SDK provides us with many widgets which help us in animating elements on scree ...

  3. Flutter InkWell Ink组件

    文章目录 Flutter InkWell & Ink组件 Flutter InkWell & Ink组件 InkWell组件可以在用户点击是出现水波纹效果. Ink组件可以将水波纹效果 ...

  4. Flutter 平移动画 — 4种实现方式

    系列文章 Flutter 旋转动画 - RotationTransition Flutter 平移动画 - 4种实现方式 Flutter 淡入淡出与逐渐出现动画 Flutter 尺寸缩放.形状.颜色. ...

  5. Flutter 自定义动画 — 数字递增动画和文字逐行逐字出现或消失动画

    系列文章 Flutter 旋转动画 - RotationTransition Flutter 平移动画 - 4种实现方式 Flutter 淡入淡出与逐渐出现动画 Flutter 尺寸缩放.形状.颜色. ...

  6. 神奇的 Flutter 文字动画-animated_text_kit

    神奇的Flutter 文字动画,文本动画. https://pub.flutter-io.cn/packages/animated_text_kit ... 可以实现在FLutter状态下的文字的打字 ...

  7. flutter圆形动画菜单,Flow流式布局动画圆形菜单

    题记 -- 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天. 重要消息 flutter中网络请求dio使用分析 视频教程在这里 Flutter 从入门实践到开发一个APP之UI基础 ...

  8. Flutter实现动画卡片式Tab导航 | 掘金技术征文

    前言 本人接触Flutter不到一个月,深深感受到了这个平台的威力,于是不断学习,Flutter官方Example中的flutter_gallery,很好的展示了Flutter各个widget的功能 ...

  9. Flutter抖动动画、颤抖动画、Flutter文字抖动效果

    题记 -- 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天. github? 你可能需要 百度同步 CSDN 网易云课堂教程 掘金 知乎 Flutter系列文章 头条同步 1 添加 ...

最新文章

  1. SiteMesh:一个优于Apache Tiles的Web页面布局、装饰框架
  2. 是c语言自带的数据类型吗_计协带你了解C语言程序
  3. 图的两种存储形式(邻接矩阵、邻接表)
  4. CheckBox控件
  5. sublime text3设置空格和tab键
  6. 可定制的PHP缩略图生成程式(需要GD库支持)
  7. LINUX C# 加载本地库的范例代码
  8. 前后端交互模式大总结 艾提拉 总结 attilax总结 目录 1. 通过ajax ajax就是js的网络api 完全解耦合 推荐 1 1.1. Query Ajax 操作函数 1 1.2. 服务
  9. MD文件阅读工具及配置
  10. java反射面试_总结Java反射面试题(附答案)
  11. macOS Big Sur初体验之自带五笔输入法质变
  12. pycharm格式化的html_pycharm格式化代码 常用快捷键
  13. 中兴配置dhcp服务器,中兴F623路由器如何投入使用dhcp服务器
  14. 宽度优先算法求解八数码问题
  15. 第一章 冯诺伊曼结构
  16. OIer常见问题与错误总结
  17. 传统会计和计算机会计的职能,论会计信息化对传统财务会计职能的影响
  18. 今天的学生要做汤饭吗
  19. 什么是敏捷开发?敏捷开发流程的8个步骤
  20. [CVPR2021-oral]Learning to Aggregate and Personalize 3D Face from In-the-Wild Photo Collection

热门文章

  1. php开发Hive Web查询
  2. Linux下Kafka单机安装配置
  3. Linux Watchdog Test Program
  4. 微软:推开窗户,我看到了云
  5. [转载] 民兵葛二蛋——第21集
  6. 怎么根据输入的n来输入n组数组_【题解一维数组】1106:年龄与疾病
  7. python函数详解_Python函数详解(转)
  8. 如何修改WP文章字体格式、字号大小、字体颜色
  9. Win10 Redstone再添新技能:深度集成App-V应用虚拟化
  10. 协议 - 收藏集 - 掘金