一直以来,项目组的小伙伴对于某安的设计和交互十分喜好,从首页布局到用户页样式到加号扩散动画,都想用到项目里来。鉴于他们强烈的热爱,已经实现了部分布局看齐。最近,终于轮到了要实现点击底部加号后出现扩散动画,并出现几项操做项的动画的时候了。git
阅读这篇文章前,你须要对Flutter有必定的了解,包括生命周期、高斯模糊、动画、MediaQuery
等相关知识,固然,全部内容均可以经过搜索找到~github
效果图: bash
交互过程主要分为如下三步:async
完整demo及组件已上传至项目,走过路过留个star~ide
想要实现效果,首先有几点前置条件须要明确:布局
build
后当即执行,而不能在initState
或didChangeDependencies
里执行,不然会存在context
为空或触发时机错误的问题;pop()
前执行,不然widget
已经被取消挂载(this.mounted == false)
。下面是具体的实现过程,将配合上述条件进行说明。post
网上有很是多的透明路由实例,包括法法路由里也包含了透明路由,此处再也不赘述,直接贴上代码。动画
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。ui
Navigator.of(context).push(TransparentRoute(
builder: (context) => AddingButtonPage(),
));
复制代码
在widget
中实现运行动画,首先须要加入TickerProviderStateMixin
,而且声明一个controller
和动画(Animation
)自己。this
class _DemoPageState extends State<DemoPage>
with TickerProviderStateMixin {
/.../
Animation<double> _backDropFilterAnimation;
AnimationController _backDropFilterController;
复制代码
在随后的功能中,咱们首先对controller
进行初始化,设定一个动画时长。
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
复制代码
这时咱们开始思考扩散大小的问题:以底部为中心,半径逐渐放大的圆,当半径达到多少时能彻底覆盖可视范围呢?
答案:
√ (width² + (height * 2 + padding.top)²) / 2
根号(二倍高的平方加宽的平方)的一半
是否是一个很是熟悉的公式?没错,它就是“勾股定理”~
dart:math
简单实现的勾股定理:
import 'dart:math' as math;
double pythagoreanTheorem(double short, double long) {
return math.sqrt(math.pow(short, 2) + math.pow(long, 2));
}
复制代码
这里利用一张图片说明半径的问题。
padding.top
是状态栏的高度,也要加入到高度中。
因此,咱们就肯定了圆形的终止半径,且起始半径为0。这个时候能够写出第一个Tween
了,用于肯定圆形半径的变化范围。MediaQuery
用于获取视图长短边。顺便定义一个曲线,实现曲线过渡效果。Flutter的Curves
里内置了许多曲线,在这我选用了Curves.easeInOut
。
/// 视野区域的大小(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);
复制代码
此处终止值是两倍半径的缘由是圆形的绘制是以圆形的外正方形大小来进行的绘制的,因此此处大小须要设置为两倍半径,以达到真正的半径效果。
一个动画的设定档完成了,要想让动画动起来,须要把动画执行的值和一个变量绑定,而且执行动画。因此咱们给这个动画加上监听后执行setState
以更新大小,而且执行动画。
/// 保存半径的变量
double _backdropFilterSize = 0.0;
/// 监听动画执行
_backDropFilterAnimation.addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
/// 正向执行动画
_backDropFilterController.forward();
复制代码
至此,放大动画已经完成了设定,接下来咱们建立布局与该动画进行绑定。
刚刚在设定动画时咱们已经知道,圆形的最终大小是远远超过视图可视大小的,在Flutter中想要实现这样的相对布局或绝对布局,咱们须要用到Stack
。这时须要注意,Stack
的溢出属性(overflow
)须要设置为显示,不然圆形只能扩大到视图最大宽度。
Stack(
overflow: Overflow.visible,
children: <Widget>[],
);
复制代码
咱们开始来考虑高斯模糊的区域大小。已知圆形的半径为对角线长度,那么以此设定的区域应该是多大呢?
再次拿出一张图来看看咱们的扩散圆形相对于视图应该处于什么位置:
Positioned
使用的是绝对布局,在此处,它的参考系是视图区域。那么咱们能够很轻易的判断顶部和横向的溢出,用于计算大小。
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,
/.../
复制代码
以此设定范围,就是圆形扩大到最大半径时外正方形的大小。
在Flutter中实现高斯模糊很是简单,只须要使用BackdropFilter
便可,一般来讲须要在外包裹ClipRect
用来解决模糊区域的问题,而咱们的需求是圆形,因此在这里应该使用ClipRRect
。
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(" "),
),
),
),
),
],
);
复制代码
将高斯模糊控件放入布局中,咱们便完成了圆形的定位。
实现了背景模糊,接下来就是将内容放置在布局中合理的大小区域。
咱们的圆形上半部分位于可视区域,因此咱们在背景中,使用Align
,利用溢出大小和已知的可视区域大小,即可以肯定内容放置的位置。
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(),
),
);
],
);
复制代码
至此,咱们能够很方便地在模糊区域内放置内容了,不须要使用时再去设置布局。
动画部分完成,咱们将动画部分封装起来,加入到首次完成build
后执行。
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();
}
/.../
复制代码
至此,一个底部扩散模糊动画跳转页面的动画就这样轻松如意的完成啦~
根据几个月的潜水经验,大多数人以为Flutter制做动画困难是由于看不懂Animation
的各类属性和操做,甚至文档都生涩难懂,可其实真正写出来后,动画部分也只有少许代码,很容易就能够理解其中的含义。
最后欢迎加入Flutter Candies,一块儿生产可爱的Flutter小糖果 (QQ群:181398081)