【译】用Flutter实现一个炫酷的界面动画效果!
在Flutter应用程序中实现超级流畅的动画,如下:
在这篇文章中,我将带您完成在Flutter应用程序中实现流畅动画的步骤。
时间线
这是一个时间轴,显示了应用中发生的所有动画。
登录屏幕和主屏幕之间平滑过渡背后的秘密在于登录屏幕的最后一帧与主屏幕的第一帧完全匹配。
让我们仔细看看登录界面。
在登录屏幕中,当用户单击“登录”
按钮时,动画开始。按钮被挤压成圆形,按钮内的文本被“加载”
动画取代。接下来,按钮内的加载动画消失,按钮在移动到中心时以圆形方式增长。一旦到达中心,按钮就会长方形地变长并覆盖整个屏幕。
按钮覆盖整个屏幕后,应用程序将导航至主屏幕。
这里初始动画不需要任何用户输入。主屏幕出现在屏幕上,带有“淡入淡出”动画。配置文件图像,通知气泡和“+”按钮以圆形方式在屏幕上生长,同时列表垂直滑动到屏幕上。
当用户点击“+”按钮时,按钮内的“+”消失并且在移动到屏幕中心时圆形地增大。一旦到达中心,按钮现在呈矩形增长并覆盖整个屏幕。
以不同的方式查看事物,动画也可以分为进入动画
和退出动画
。
进入动画的时间表如下所示:
退出动画
的时间表是这样的:
以上有足够的理论!让我们开始编码。
动画登录屏幕
主屏幕动画
在此屏幕上,当用户单击Sign In
按钮时动画开始。此屏幕中的动画可以分为以下几部分:
设计按钮
登录界面上有这个按钮组件:
new Container(
width: 320.0,
height: 60.0,
alignment: FractionalOffset.center,
decoration: new BoxDecoration(
color: const Color.fromRGBO(247, 64, 106, 1.0),
borderRadius: new BorderRadius.all(const Radius.circular(30.0)),
),
child: new Text(
"Sign In",
style: new TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.w300,
letterSpacing: 0.3,
),
),
)
请注意窗口小部件的width
属性Container
。这个属性将在按钮的初始动画中扮演重要角色,它被挤压成一个圆圈。
构建动画之前的初始设置。
我们需要告诉应用程序仅在用户单击“ 登录”按钮时才启动动画。为此,我们创建了一个AnimationController
负责管理所有动画的类。
简单地说,AnimationController
控制动画。该类是Flutter动画框架中的一个重要角色。它扩展了动画类,并添加了最基本的元素使其可用。这个类不是动画对象的控制器,而是动画概念的控制器。
AnimationController
的基本功能是在指定的时间内将动画从一个双值转换为另一个双值。
AnimationController
看起来像这样的基本构造函数:
AnimationController({
double value,
this.duration,
this.debugLabel,
this.lowerBound: 0.0,
this.upperBound: 1.0,
@required TickerProvider vsync,
})
AnimationController
需要一个TickerProvider
获得他们的Ticker。如果AnimationController
要从状态创建,则可以使用TickerProviderStateMixin
和SingleTickerProviderStateMixin
类来获取合适的状态TickerProvider。窗口小部件测试框架WidgetTester
对象可以在测试环境中用作股票提供者。
我们在initState
函数内初始化这个构造函数:
void initState() {
super.initState();
_loginButtonController = new AnimationController(
duration: new Duration(milliseconds: 3000),
vsync: this
);
}
我们唯一需要关注的是Duration小部件。此小组件定义登录屏幕动画必须发生的总时间。
这LoginScreenState
是一个有状态小部件,并创建AnimationController
指定3000毫秒的持续时间。它播放动画并构建小部件树的非动画部分。当在屏幕上检测到按钮时,动画开始。动画向前,然后向后。
onTap: () {
_playAnimation();
},
Future<Null> _playAnimation() async {
try {
await _loginButtonController.forward();
await _loginButtonController.reverse();
}
on TickerCanceled{}
}
buttonSqueezeAnimation = new Tween(
begin: 320.0,
end: 70.0,
).animate(new CurvedAnimation(
parent: buttonController,
curve: new Interval(0.0, 0.250)
))
我已经将这个Tween类与CurvedAnimation
小部件一起使用了。Tween指定动画应该begin和的点end。在动画的开始时,width
该按钮的是320.0pixels
,由动画结束按钮被降低到width
的70.0pixels
。按钮的高度保持不变。持续时间由在Interval窗口小部件内调用的CurvedAnimation窗口小部件定义。Interval基本上告诉应用程序在250毫秒内将按钮挤压成一个圆圈。
将按钮挤压成圆形后,我们需要登录文本消失。
我使用条件语句来实现这一点。如果width按钮的大小超过75.0pixels
,则该按钮应包含登录文本。否则,按钮应包含一个CircularProgressIndicator
。
new Container(
width: buttonSqueezeAnimation.value,
height: 60.0,
alignment: FractionalOffset.center,
decoration: new BoxDecoration(
color: const Color.fromRGBO(247, 64, 106, 1.0),
borderRadius: new BorderRadies.all(const Radius.circular(30.0))
),
child: buttonSqueezeAnimation.value > 75.0 ? new Text(
"Sign In",
style: new TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.w300,
letterSpacing: 0.3,
),
) : new CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color>(
Colors.white
),
)
)
接下来,我希望按钮组件缩小。要做到这一点,只需创建一个Tween小部件,告诉按钮从70.0pixels
增长到1000.0pixels
。
但是Tween它本身不能为按钮设置动画。为此,我们需要使用animate()
链接Tween一个CurvedAnimation小部件,小部件包含一个Interval小部件,告诉应用程序在350毫秒之间完成动画。
buttonZoomout = new Tween(
begin: 70.0,
end: 1000.0,
).animate(
new CurvedAnimation(
parent: buttonController,
curve: new Interval(
0.550, 0.900,
curve: Curves.bounceOut,
),
)),
)
我们还需要同时CircularProgressIndicator
从按钮中删除。为此,我们再次使用条件语句。
虽然buttonZoomOut.value
它等于70,但它buttonSqueezeAnimation
会起作用。当Interval上面的代码片段超过0.550时,buttonSqueezeAnimation
停止并buttonZoomOut
开始动画。
同样,如果buttonZoomOut
仍然是70.0像素,则高度更改为60.0像素。否则,高度也等于buttonZoomOut
的值。
现在,如果buttonZoomOut.value
小于300.0,那么我们保持CircularProgressIndicator
按钮内部。否则,我们用null替换它。
这是它的代码:
new Container(
width: buttonZoomOut.value == 70 ? buttonSqueezeAnimation.value : buttonZoomOut.value,
height: buttonZoomOut.value == 70 ? 60.0 : buttonZoomOut.value,
alignment: FractionalOffset.center,
decoration: new BoxDecoration(
color: const Color.fromRGBO(247, 64, 106, 1.0)
borderRadius: new BorderRadius.all(const Radius.circular(30.0))
),
child: buttonSqueezeAnimation.value > 75.0 ? new Text(
"Sign In",
style: new TextStyle(
color: Colors.white,
fontSize: 20.0,
),
) : buttonZoomOut.value < 300.0 ? new CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color>(
Colors.white
),
) : null),
)
最后,我们希望按钮容器增长并覆盖整个屏幕。为此,我们将首先删除整个页面的padding(top和bottom),radius容器也应该可以忽略不计。按钮现在应该增长并覆盖整个屏幕,并带有颜色过渡动画。
封面动画(放慢速度)
new Padding(
padding: buttonZoomOut.value == 70 ? const EdgeInsets.only(
bottom: 50.0,
) : const EdgeInsets.only(
top: 0.0, bottom: 0.0
),
child: new Inkwell(
onTap: () {
_playAnimation();
},
child: new Hero(
tag: "fade",
child: new Container(
width: buttonZoomOut.value == 70 ? buttonSqueezeAnimation.value : buttonZoomOut.value,
height: buttonZoomOut.value == 70 ? 60.0 : buttonZoomOut.value,
alignment: FractionalOffset.center,
decoration: new BoxDecoration(
color: buttonZoomOut.value == 70 ? const Color.fromRGBO(247, 64, 106, 1.0) : buttonGrowColorAnimation.value,
borderRadius: buttonZoomOut.value < 400 ? new BorderRadius.all(const Radius.circular(30.0)) : new BorderRadius.all(const Radius.circular(0.0)),
),
child: buttonSqueezeAnimation.value > 75.0 ? new Text(
"Sign In",
style: new TextStyle(
color: Colors.white,
fontSize: 20.0,
fontWeight: FontWeight.w300,
letterSpacing: 0.3,
),
) : buttonZoomOut.value < 300.0 ? new CircularProgressIndicator(
value: null,
strokeWidth: 1.0,
valueColor: new AlwaysStoppedAnimation<Color>(
Colors.white
),
) : null
),
)
),
)
动画完成后,用户将导航到主屏幕。
这是两个屏幕之间平滑过渡的秘诀。我们需要登录屏幕的最后一帧与主屏幕的第一帧匹配。我还添加了一个监听器,等待按钮组件完全覆盖整个屏幕。完成后,我使用内置的Flutter Navigator将我们带到主屏幕。
buttonController.addListener(() {
if (buttonController.isCompleted) {
Navigator.pushNamed(context, "/home");
}
});
主屏幕动画
就像登录屏幕一样,主屏幕上的动画可以分为以下几部分:
从上面显示的GIF中,我们可以看到当应用程序导航到主屏幕时,首先发生的事情是屏幕渐变为白色。
为此,我们创建一个新的ColorTween
小部件并将其命名为fadeScreenAnimation。在这个小部件中,我们将opacity从1.0
更改为0.0
,使颜色消失。我们将此小部件链接到CurvedAnimation使用animate()
。
接下来,我们创建一个名为的新窗口小部件FadeBox并fadeScreenAnimation
在其中调用。
fadeScreenAnimation = new ColorTween(
begin: const Color.fromRGBO(247, 64, 106, 1.0),
end: const Color.fromRGBO(247, 64, 107, 0.0),
).animate(
new CurvedAnimation(
parent: _screenController,
curve: Curves.ease,
),
);
new FadeBox(
fadeScreenAnimation: fadeScreenAnimation,
containerGrowAnimation: containerGrowAnimation,
),
new Hero(
tag: "fade",
child: new Container(
width: containerGrowAnimation.value < 1 ? screenSize.width : 0.0,
height: containerGrowAnimation.value < 1 ? screebSize.height : 0.0,
decoration: new BoxDecoration(
color: fadeScreenAnimation.value,
),
)
)
这里Hero是Flutter的一个小部件,用于创建hero动画。此动画使对象从一个屏幕“飞行”到另一个屏幕。添加到此窗口小部件的所有子项都有资格使用hero动画。
配置文件图像,通知气泡和添加按钮以循环方式成长。
containerGrowAnimation = new CurvedAnimation(
parent: _screenController,
curve: Curves.easeIn,
);
buttonGrowAnimation = new CurvedAnimation(
parent: _screnController,
curve: Curves.easeOut,
);
new Container(
child: new Column(
children: [
new Container(
width: containerGrowAnimation.value * 35,
height: containerGrowAnimation.value * 35,
margin: new EdgeInsets.only(left: 80.0),
child: new Center(
child: new Text(
"3",
style: new TextStyle(
fontSize: containerGrowAnimation.value * 15,
fontWeight: FontWeight.w400,
color: Colors.white
)
),
),
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: const Color.fromRGBO(80, 210, 194, 1.0),
)
),
],
),
width: containerGrowAnimation.value * 120,
height: containerGrowAnimation.value * 120,
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: profileImage,
)
);
屏幕上半部分的所有元素都有各自的位置。随着概要文件图像和列表块的增长,它们会垂直移动并占据各自的位置。
listTileWidth = new Tween<double>(
begin: 1000.0,
end: 600.0,
).animate(
new CurvedAnimation(
parent: _screenController,
curve: new Interval(
0.225,
0.600,
curve: Curves.bounceIn,
),
),
);
listSlideAnimation = new AlignmentTween(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
).animate(
new CurvedAnimation(
parent: _screenController,
curve: new Interval(
0.325,
0.500,
curve: Curves.ease,
),
),
);
listSlidePosition = new EdgeInsetsTween(
begin: const EdgeInsets.only(bottom: 16.0),
end: const EdgeInsets.only(bottom: 80.0),
).animate(
new CurvedAnimation(
parent: _screenController,
curve: new Interval(
0.325,
0.800,
curve: Curves.ease,
),
),
);
new Stack(
alignment: listSlideAnimation.value,
children: <Widget>[
new Calendar(margin: listSlidePosition.value * 3.5),
new ListData(
margin: listSlidePosition.value * 2.5,
width: listTileWidth.value,
title: "New subpage for Janet",
subtitle: "8-10am",
image: avatar1
),
],
)
这样,主屏幕就会在屏幕上呈现。现在让我们看一下将用户带回登录屏幕的动画。
当用户单击该Add按钮时,AnimationController
会初始化:
void initState() {
super.initState();
_button.Controller = new AnimationController(
duration: new Duration(milliseconds: 1500),
vsync: this
);
}
该按钮从屏幕右下方导航到中心
buttonBottomCenterAnimation = new AlignmentTween(
begin: Alignment.bottomRight,
end: Alignment.center,
).animate(
new CurvedAnimation(
parent: buttonController,
curve: new Interval(
0.0,
0.200,
curve: Curves.easeOut,
),
),
)
当按钮移动到屏幕的中心时,它的大小会逐渐增大并Icon消失。
buttonZoomOutAnimation = new Tween(
begin: 60.0,
end: 1000.0,
).animate(
new CurvedAnimation(
parent: buttonController,
curve: Curves.bounceOut
),
)
当按钮到达中心时,它以圆形方式缩小,转换为矩形并覆盖整个屏幕。
new Container(
alignment: buttonBottomtoCenterAnimation.value,
child: new Inkwell(
child: new Container(
width: buttonZoomOutAnimation.value,
height: buttonZoomOutAnimation.value,
alignment: buttonBottomtoCenterAnimation.value,
decoration: new BoxDecoration(
color: buttonGrowColorAnimation.value,
shape: buttonZoomOutAnimation.value < 500 ? BoxShape.circle : BoxShape.rectangle
),
child: new Icon(
Icons.add,
size: buttonZoomOutAnimation.value < 50 ? buttonZoomOutAnimation.value : 0.0,
color: Colors.white,
),
),
)
)
最后,动画完成后,应用程序需要导航回登录屏幕。
(buttonController.isComplete) {
Navigator.pushReplacementName(context, "/login");
}
以上就是本文的全部内容,感谢阅读,祝编码愉快!
---END---