阅读这篇文章前,你需要对Flutter有一定的了解,包括生命周期、高斯模糊、动画、MediaQuery
等相关知识,当然,所有内容都可以通过搜索找到~文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
效果图:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
交互过程主要分为以下三步:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
- 点击加号,从加号位置以圆形扩散高斯模糊效果;
- 操作项依次出现,并附带一定的动画效果;
- 点击"X"或空白处或系统返回键,背景以圆形收缩至加号位置。
完整demo及组件已上传至项目,走过路过留个star~文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
- ripple_backdrop_animate_route
- OpenJMU/lib/pages/home/AddButtonPage.dart
- OpenJMU/lib/pages/MainPage.dart
前置条件
想要实现效果,首先有几点前置条件需要明确:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
- 路由需要做成透明路由,否则高斯模糊无法作用在上一个路由之上;
- 根据生命周期,动画的执行必须要在第一次
build
后立即执行,而不能在initState
或didChangeDependencies
里执行,否则会存在context
为空或触发时机错误的问题; - 关闭动画必须要在
pop()
前执行,否则widget
已经被取消挂载(this.mounted == false)
。
实现过程
下面是具体的实现过程,将配合上述条件进行说明。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
透明跳转路由
网上有非常多的透明路由实例,包括法法路由里也包含了透明路由,此处不再赘述,直接贴上代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
@required this.builder,
RouteSettings settings,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
@override
bool get opaque => false;
@override
Color get barrierColor => null;
@override
String get barrierLabel => null;
@override
bool get maintainState => true;
@override
/// 这里时长设置为0,是因为我们的布局一开始
/// 并不包含任何内容,所以直接砍掉跳转时间。
Duration get transitionDuration => Duration.zero;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: result,
);
}
}
复制代码
构建完成后,直接push
就OK。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
Navigator.of(context).push(TransparentRoute(
builder: (context) => AddingButtonPage(),
));
复制代码
扩散动画
在widget
中实现运行动画,首先需要加入TickerProviderStateMixin
,并且声明一个controller
和动画(Animation
)本身。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
class _DemoPageState extends State<DemoPage>
with TickerProviderStateMixin {
/.../
Animation<double> _backDropFilterAnimation;
AnimationController _backDropFilterController;
复制代码
在随后的功能中,我们首先对controller
进行初始化,设定一个动画时长。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
复制代码
这时我们开始思考扩散大小的问题:以底部为中心,半径逐渐放大的圆,当半径达到多少时能完全覆盖可视范围呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
答案:
√ (width² + (height * 2 + padding.top)²) / 2
根号(二倍高的平方加宽的平方)的一半文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
是不是一个非常熟悉的公式?没错,它就是“勾股定理”~文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
贴上以dart:math
简单实现的勾股定理:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
import 'dart:math' as math;
double pythagoreanTheorem(double short, double long) {
return math.sqrt(math.pow(short, 2) + math.pow(long, 2));
}
复制代码
这里利用一张图片说明半径的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
为了让模糊控件能完整的覆盖视图区域,扩散的圆的半径必须大于以视图长的两倍和宽及其顶点连接而成的斜边的长度,而不能只是视图的高度。padding.top
是状态栏的高度,也要加入到高度中。 所以,我们就确定了圆形的终止半径,且起始半径为0。这个时候可以写出第一个Tween
了,用于确定圆形半径的变化范围。MediaQuery
用于获取视图长短边。顺便定义一个曲线,实现曲线过渡效果。Flutter的Curves
里内置了许多曲线,在这我选用了Curves.easeInOut
。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
/// 视野区域的大小(Size)
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;
/// 动画曲线
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
/// 放大动画的设定档
Animation<double> _backDropFilterAnimation = Tween(
begin: 0.0, end: r * 2
).animate(_backDropFilterCurve);
复制代码
此处终止值是两倍半径的原因是圆形的绘制是以圆形的外正方形大小来进行的绘制的,所以此处大小需要设置为两倍半径,以达到真正的半径效果。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
一个动画的设定档完成了,要想让动画动起来,需要把动画执行的值和一个变量绑定,并且执行动画。所以我们给这个动画加上监听后执行setState
以更新大小,并且执行动画。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
/// 保存半径的变量
double _backdropFilterSize = 0.0;
/// 监听动画执行
_backDropFilterAnimation.addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
/// 正向执行动画
_backDropFilterController.forward();
复制代码
至此,放大动画已经完成了设定,接下来我们创建布局与该动画进行绑定。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
高斯模糊布局
刚刚在设定动画时我们已经知道,圆形的最终大小是远远超过视图可视大小的,在Flutter中想要实现这样的相对布局或绝对布局,我们需要用到Stack
。这时需要注意,Stack
的溢出属性(overflow
)需要设置为显示,否则圆形只能扩大到视图最大宽度。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
Stack(
overflow: Overflow.visible,
children: <Widget>[],
);
复制代码
我们开始来考虑高斯模糊的区域大小。已知圆形的半径为对角线长度,那么以此设定的区域应该是多大呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
再次拿出一张图来看看我们的扩散圆形相对于视图应该处于什么位置:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
Positioned
使用的是绝对布局,在此处,它的参考系是视图区域。那么我们可以很轻易的判断顶部和横向的溢出,用于计算大小。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;
/// 顶部溢出大小
final double topOverflow = r - s.height;
/// 横向溢出大小
final double horizontalOverflow = r - s.width;
return Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: - horizontalOverflow,
right: - horizontalOverflow,
top: - topOverflow,
bottom: - r,
/.../
复制代码
以此设定范围,就是圆形扩大到最大半径时外正方形的大小。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
在Flutter中实现高斯模糊非常简单,只需要使用BackdropFilter
即可,通常来说需要在外包裹ClipRect
用来解决模糊区域的问题,而我们的需求是圆形,所以在这里应该使用ClipRRect
。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
import 'dart:ui' as ui;
Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: - horizontalOverflow,
right: - horizontalOverflow,
top: - topOverflow,
bottom: - r,
child: SizedBox(
/// 高宽与变量绑定
width: _backdropFilterSize,
height: _backdropFilterSize,
/// 使用圆角ClipRRect达到圆形效果
child: ClipRRect(
/// 圆角的大小,使用最大值则所有时候都为圆形
borderRadius: BorderRadius.circular(r * 2),
child: BackdropFilter(
/// XY用于设定模糊程度
filter: ui.ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
/// 使用空格占位,否则模糊背景不显示
child: Text(" "),
),
),
),
),
],
);
复制代码
将高斯模糊控件放入布局中,我们便完成了圆形的定位。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
设定可放置内容的区域
实现了背景模糊,接下来就是将内容放置在布局中合理的大小区域。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
我们的圆形上半部分位于可视区域,所以我们在背景中,使用Align
,利用溢出大小和已知的可视区域大小,便可以确定内容放置的位置。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(...),
Align(
/// 区域相对顶部居中对齐,在可视区域附近
alignment: Alignment.topCenter,
child: Container(
/// 推出顶部溢出部分,使得区域顶部对齐视图顶部
margin: EdgeInsets.only(top: topOverflow),
/// 将可视区域大小设定为控件大小
width: s.width,
height: s.height,
/// 设置constraint,防止子控件发生意料之外的溢出
constraints: BoxConstraints(
maxWidth: s.width,
maxHeight: s.height,
),
child: child ?? SizedBox(),
),
);
],
);
复制代码
至此,我们可以很方便地在模糊区域内放置内容了,不需要使用时再去设置布局。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
整体执行
动画部分完成,我们将动画部分封装起来,加入到首次完成build
后执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
import 'package:flutter/scheduler.dart';
class _AddingButtonPageState extends State<AddingButtonPage> with TickerProviderStateMixin {
@override
void initState() {
/// 使用scheduler,将动画加入到build后进行
SchedulerBinding.instance.addPostFrameCallback((_) => backDropFilterAnimate(context));
super.initState();
}
void backDropFilterAnimate(BuildContext context) async {
final Size s = MediaQuery.of(context).size;
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
_backDropFilterAnimation = Tween(
begin: 0.0,
end: pythagoreanTheorem(s.width, s.height) * 2,
).animate(_backDropFilterCurve)
..addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
_backDropFilterController.forward();
}
/.../
复制代码
至此,一个底部扩散模糊动画跳转页面的动画就这样轻松如意的完成啦~文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
结语
根据几个月的潜水经验,大多数人觉得Flutter制作动画困难是因为看不懂Animation
的各种属性和操作,甚至文档都生涩难懂,可其实真正写出来后,动画部分也只有少量代码,很容易就可以理解其中的含义。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html
作者:AlexV525
链接:https://juejin.im/post/5d5d1a136fb9a06ada54b66d
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15835.html