在上一期,咱们已经完成了点开动画的编写和执行,若是有仔细看完的小伙伴会发现,其中的动画效果不止扩散这么简单,本篇就来继续研究其他的动画交互。git
做为一个炫(pin)酷(ru)的页面,页面中的交互也很是的重要。在本篇,我将进一步说明页面内各个位置的交互细节,从而带着各位作一个不将就的强迫症~github
效果图: 数组
完整demo及组件已上传至项目,走过路过留个star~bash
页面中的交互主要包含三个触发位置:markdown
接下来将逐点说明如何实现。app
咱们知道在Flutter中,页面要返回时,会执行Navigator.maybePop
的方法,使页面返回。为了拦截路由pop
,Flutter提供了WillPopScope
来拦截返回行为,咱们只须要注册onWillPop
方法,就能够在pop
前执行代码。async
bool _popping = false;
Future<bool> willPop() async {
/// 等待返回动画的执行
await backDropFilterAnimate(context, false);
/// 判断_popping从而避免重复触发pop
if (!_popping) {
_popping = true;
await Future.delayed(Duration(milliseconds: _animateDuration), () {
Navigator.of(context).pop();
});
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: WillPopScope(
/// 绑定willPop方法
onWIllPop: willPop,
child: wrapper(
context,
child: widget.child,
),
),
);
}
复制代码
如此咱们就轻松愉快地拦截了路由~ide
思考退出动画和跳转动画的关系,咱们立马就能够想到,跳转和退出的动画是相反的,也就是说,逆向执行跳转的动画,就能获得一个退出动画。布局
这时咱们来回顾一下上一期的跳转动画:post
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();
}
复制代码
要想以相反的方向执行动画,咱们加入一个参数bool forward
:
void backDropFilterAnimate(BuildContext context, bool forward)
使用forward
来控制begin
和end
,达到执行的效果。同时对forward
进行判断,若是为false
则尝试暂停动画:
void backDropFilterAnimate(BuildContext context, bool forward) {
/.../
if (!forward) _backDropFilterController?.stop();
_backDropFilterAnimation = Tween(
/// 三元运算赋值
begin: forward ? 0.0 : _backdropFilterSize,
end: forward ? pythagoreanTheorem(s.width, s.height) * 2 : 0.0,
).animate(_backDropFilterCurve)
..addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
/.../
}
复制代码
看到这里可能会有小伙伴问了,AnimateController
明明提供了reverse
方法用于反向,为何还要使用一个bool
来控制动画执行方向呢?
缘由在于当使用reverse
时,控制器会将begin
和end
对调来执行动画,但当咱们执行退出动画时,圆形不必定已经彻底覆盖,因此经过使用forward
来判断方向,能够使未彻底覆盖的动画从中止处反向执行,不会形成闪烁的状况。
至此,跳转和退出动画已经完美完成。
根据效果图,在页面的底部,会提供一个带有旋转动画返回按钮,点击能够返回。
因为个人页面时点击加号触发的,因此这里我引入了bottomHeight
,用来肯定加号的位置。从效果图能够看到个人底部导航栏,它的高度咱们假设是60.0
,那按钮的位置如何定义呢?
final double bottomHeight = 60.0;
/.../
Widget popButton() {
return SizedBox(
/// 此处假设为60.0
width: widget.bottomHeight,
height: widget.bottomHeight,
child: Center(
/// 套手势监听,并设定监听行为
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Icon(
Icons.add,
color: Colors.grey
),
onTap: willPop,
),
),
);
}
复制代码
将它放入布局中:
Stack(
/.../
children: <Widget>[
Positioned(
/// 将按钮控件固定在视图底部中央
left: 0.0,
right: 0.0,
bottom: 0.0,
child: popButton(),
),
],
)
复制代码
按钮定位完成,这时咱们开始设计动画。按钮一共须要两组动画,一组是旋转,一组是淡入淡出。
/// 初始化按钮旋转的角度
final double bottomButtonRotateDegree = 45.0;
/// 旋转动画相关
Animation<double> _popButtonAnimation;
AnimationController _popButtonController;
/// 淡入淡出相关
Animation<double> _popButtonOpacityAnimation;
AnimationController _popButtonOpacityController;
void popButtonAnimate(context, bool forward) {
/// 与背景相同,判断正反执行
if (!forward) {
_popButtonController?.stop();
_popButtonOpacityController?.stop();
}
/// 转换按钮实际旋转角度
final double rotateDegree =
widget.bottomButtonRotateDegree * (math.pi / 180);
///
_popButtonOpacityController = _popButtonController = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
Animation _popButtonCurve = CurvedAnimation(
parent: _popButtonController,
curve: Curves.easeInOut,
);
_popButtonAnimation = Tween(
begin: forward ? 0.0 : _popButtonRotateAngle,
end: forward ? rotateDegree : 0.0,
).animate(_popButtonCurve)
..addListener(() {
setState(() {
_popButtonRotateAngle = _popButtonAnimation.value;
});
});
/// 设定透明度最小值为0.01,防止背景显示错误
_popButtonOpacityAnimation = Tween(
begin: forward ? 0.01 : _popButtonOpacity,
end: forward ? 1.0 : 0.01,
).animate(_popButtonCurve)
..addListener(() {
setState(() {
_popButtonOpacity = _popButtonOpacityAnimation.value;
});
});
_popButtonController.forward();
_popButtonOpacityController.forward();
}
复制代码
按钮动画构建完成,咱们将它放到背景动画中一块儿执行:
Future backDropFilterAnimate(BuildContext context, bool forward) async {
/.../
/// 使用相同的forward控制方向
popButtonAnimate(context, forward);
/.../
}
复制代码
至此,按钮的动画会跟着背景一块儿联动了,十分完美~
但,别着急结束,咱们还有内容的动画定制没有完成,若是不须要如效果图通常的元素动画,能够出门右转~
从效果图咱们能够看到,两个操做项是依次淡入出现,而且带有必定的垂直位移。这时问题出现了:个人操做项数量不肯定,难道每个操做项我都要专门写一个动画吗?
答案是:对了一半。为何这么说?咱们确实须要写操做项的动画,但咱们不须要重复地去写每个操做项,只须要经过封装操做项的内容,将动画全部相关内容也组成数个List
,问题就简单了不少。
以效果图为例,我有两个操做项,先进行声明。
List<String> itemTitles = ["动态", "扫一扫"];
List<String> itemIcons = ["subscriptedAccount", "scan"];
List<Color> itemColors = [Colors.orange, Colors.teal];
List<Function> itemOnTap = [...];
复制代码
将操做项全部的信息存储在四个数组中。接下来咱们建立两组动画共8个数组的相关变量。
/// 操做项垂直偏移量
List<double> _itemOffset;
/// 操做项偏移动画
List<Animation<double>> _itemAnimations;
/// 操做项偏移动画曲线
List<CurvedAnimation> _itemCurveAnimations;
/// 操做项偏移动画控制器
List<AnimationController> _itemAnimateControllers;
/// 操做项透明度
List<double> _itemOpacity;
/// 操做项透明度动画
List<Animation<double>> _itemOpacityAnimations;
/// 操做项透明度动画曲线
List<CurvedAnimation> _itemOpacityCurveAnimations;
/// 操做项透明度动画控制器
List<AnimationController> _itemOpacityAnimateControllers;
复制代码
那么,该怎么初始化动画呢?
void initItemsAnimation() {
/// 根据操做项内容,初始化动画相关变量
_itemOffset = <double>[for (int i=0; i<itemTitles.length; i++) 0.0];
_itemAnimations = List<Animation<double>>(itemTitles.length);
_itemCurveAnimations = List<CurvedAnimation>(itemTitles.length);
_itemAnimateControllers = List<AnimationController>(itemTitles.length);
_itemOpacity = <double>[for (int i=0; i<itemTitles.length; i++) 0.01];
_itemOpacityAnimations = List<Animation<double>>(itemTitles.length);
_itemOpacityCurveAnimations = List<CurvedAnimation>(itemTitles.length); _itemOpacityAnimateControllers = List<AnimationController>(itemTitles.length);
/// 遍历操做性,初始化每个动画内容
for (int i = 0; i < itemTitles.length; i++) {
/// 垂直偏移动画的设定
_itemAnimateControllers[i] = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
_itemCurveAnimations[i] = CurvedAnimation(
parent: _itemAnimateControllers[i],
curve: Curves.ease,
);
/// 垂直偏移量设置为20
_itemAnimations[i] = Tween(
begin: -20.0,
end: 0.0,
).animate(_itemCurveAnimations[i]) ..addListener(() {
setState(() {
_itemOffset[i] = _itemAnimations[i].value;
});
});
/// 透明度动画的设定
_itemOpacityAnimateControllers[i] = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
_itemOpacityCurveAnimations[i] = CurvedAnimation(
parent: _itemOpacityAnimateControllers[i],
curve: Curves.linear,
);
_itemOpacityAnimations[i] = Tween(
begin: 0.01,
end: 1.0,
).animate(_itemOpacityCurveAnimations[i])
..addListener(() {
setState(() {
_itemOpacity[i] = _itemOpacityAnimations[i].value;
});
});
}
}
/// 操做项动画的执行
void itemsAnimate(bool forward) {
for (int i = 0; i < _itemAnimateControllers.length; i++) {
/// 每一个操做项依次增长延时,造成连续效果
Future.delayed(Duration(milliseconds: 50 * i), () {
if (forward) {
_itemAnimateControllers[i]?.forward();
_itemOpacityAnimateControllers[i]?.forward();
} else {
_itemAnimateControllers[i]?.reverse();
_itemOpacityAnimateControllers[i]?.reverse();
}
});
}
}
复制代码
建立操做项的widget
,将动画值进行绑定:
Widget item(BuildContext context, int index) {
return Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: 0.0, right: 0.0,
/// 绑定垂直偏移
top: _itemOffset[index],
child: Opacity(
/// 绑定透明度
opacity: _itemOpacity[index],
child: ...
),
),
],
);
}
复制代码
最后将动画初始化放进initState
,动画执行添加至跳转动画。
@override
void initState() {
initItemsAnimation();
/.../
}
Future backDropFilterAnimate(BuildContext context, bool forward) async {
/.../
if (forward) {
/// 以跳转动画二分之一的延时执行,效果更佳
Future.delayed(
Duration(milliseconds: _animateDuration ~/ 2),
() { itemsAnimate(true); },
);
} else {
itemsAnimate(false);
}
}
复制代码
一切就绪,保存就能够看到精美的动画效果了~
这个动画我的耗时大约2小时,在思路很是清晰的状况下,将动画效果实现不是一件难事,这样的动画其实相对不难,接下来可能会有内容揭开、位置自定义等花式的需求,让咱们拭目以待~
最后欢迎加入Flutter Candies,一块儿生产可爱的Flutter小糖果 (QQ群:181398081)