作为一个炫(pin)酷(ru)的页面,页面中的交互也非常的重要。在本篇,我将进一步说明页面内各个位置的交互细节,从而带着各位做一个不将就的强迫症~

效果图:

完整demo及组件已上传至项目,走过路过留个star~

交互要素

页面中的交互主要包含三个触发位置:点击空白的模糊处,页面会执行退出和退出动画;

点击页面上的返回或关闭按钮,页面会执行退出和退出动画;

元素渐显并带有其他效果。

接下来将逐点说明如何实现。

实现过程

拦截返回操作

我们知道在Flutter中,页面要返回时,会执行Navigator.maybePop的方法,使页面返回。为了拦截路由pop,Flutter提供了WillPopScope来拦截返回行为,我们只需要注册onWillPop方法,就可以在pop前执行代码。bool _popping = false;

Future 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,

),

),

);

}

复制代码

如此我们就轻松愉快地拦截了路由~

退出动画

思考退出动画和跳转动画的关系,我们立马就可以想到,跳转和退出的动画是相反的,也就是说,逆向执行跳转的动画,就能得到一个退出动画。

这时我们来回顾一下上一期的跳转动画: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来判断方向,可以使未完全覆盖的动画从停止处反向执行,不会造成闪烁的情况。

至此,跳转和退出动画已经完美完成。

"X" & 空白处返回

根据效果图,在页面的底部,会提供一个带有旋转动画返回按钮,点击可以返回。

由于我的页面时点击加号触发的,所以这里我引入了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: [

Positioned(

/// 将按钮控件固定在视图底部中央

left: 0.0,

right: 0.0,

bottom: 0.0,

child: popButton(),

),

],

)

复制代码

按钮定位完成,这时我们开始设计动画。按钮一共需要两组动画,一组是旋转,一组是淡入淡出。/// 初始化按钮旋转的角度

final double bottomButtonRotateDegree = 45.0;

/// 旋转动画相关

Animation _popButtonAnimation;

AnimationController _popButtonController;

/// 淡入淡出相关

Animation _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 itemTitles = ["动态", "扫一扫"];

List itemIcons = ["subscriptedAccount", "scan"];

List itemColors = [Colors.orange, Colors.teal];

List itemOnTap = [...];

复制代码

将操作项所有的信息存储在四个数组中。接下来我们创建两组动画共8个数组的相关变量。/// 操作项垂直偏移量

List _itemOffset;

/// 操作项偏移动画

List> _itemAnimations;

/// 操作项偏移动画曲线

List _itemCurveAnimations;

/// 操作项偏移动画控制器

List _itemAnimateControllers;

/// 操作项透明度

List _itemOpacity;

/// 操作项透明度动画

List> _itemOpacityAnimations;

/// 操作项透明度动画曲线

List _itemOpacityCurveAnimations;

/// 操作项透明度动画控制器

List _itemOpacityAnimateControllers;

复制代码

那么,该怎么初始化动画呢?void initItemsAnimation() {

/// 根据操作项内容,初始化动画相关变量

_itemOffset = [for (int i=0; i

_itemAnimations = List>(itemTitles.length);

_itemCurveAnimations = List(itemTitles.length);

_itemAnimateControllers = List(itemTitles.length);

_itemOpacity = [for (int i=0; i

_itemOpacityAnimations = List>(itemTitles.length);

_itemOpacityCurveAnimations = List(itemTitles.length); _itemOpacityAnimateControllers = List(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: [

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小时,在思路非常清晰的情况下,将动画效果实现不是一件难事,这样的动画其实相对不难,接下来可能会有内容揭开、位置自定义等花式的需求,让我们拭目以待~

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐