近些日子在 UIMovement[1] 上看到了一个比较酷炫的登录页效果,如下:
觉得很酷炫,就自己实现了一下,效果如下:
下面就来一步一步的分析是如何做出来的。
首先还是老套路,看一下都需要做什么事情:
1.首先我们最清晰明了的需求就是点击「注册」弹出 Dialog2.弹出 Dialog 后延迟一段时间弹出 Dialog 里的内容3.Dialog 内说明文字有两种颜色4.点击 「Accepter」按钮会变色缩小回弹并展示 ok图标5.点击「Accepter」按钮时 Dialog 内其他文字都被「白色遮罩」6.「Accepter」按钮 动画结束后 dismiss 掉当前dialog 并把 logo向上移7.跳转到第二页,文字呈波浪形弹出8.文字弹出后显示对话框并弹出键盘
需求了解了,下面就是一步一步的实现效果。
在这里我们需要注意的有一点:
在我们使用 showModalBottomSheet
时,默认的背景是白色的,也就是说我们自己设置的圆角是不管用的,
所以要给这个 BottomSheet
一个背景,这个参数在 showModalBottomSheet
方法中就有:
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) {
Future.delayed(Duration(milliseconds: 50), () {
_animationController.forward();
});
return AnimatedUserAgreement(
animation: _animation,
);
});
)
设置一个 backgroundColor
就ok了。
这里我是写了一个 「AnimatedWidget」,对 Dialog 里面的 Widget 同时执行透明度和位置的动画:
return Container(
height: 270,
padding: EdgeInsets.all(30),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30), color: Colors.white),
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Stack(
children: <Widget>[
SingleChildScrollView(
child: Container(
child: UserAgreementDialog(),
margin: EdgeInsets.only(top: _offsetTween.evaluate(animation)),
),
)
],
),
),
);
可能细心的同学看出来上面的代码有一些问题:
为什么要加一个 SingleChildScrollView
?
因为我这里改变位置使用的动画效果是 「动态改变 Container margin 的值」,
所以如果不使用 ScrollView 的话,会溢出。
有两种颜色这个需求还是比较简单的,使用 「TextSpan」就搞定了。
代码我就不贴了。
重点来了,这个功能是相对来说比较复杂的,但是只要我们了解需求,写起来也是比较简单。
首先我们也是把这个功能点拆分一下:
1.点击按钮的时候会变色2.点击后会变回原来的颜色并缩小成一个圆形3.变成圆形后动画效果展示 ok 图标
也还是一步一步来。
该功能不用考虑太多,既然有点击手势,那必然会使用 GestureDetector
,
然后使用 GestureDetector
的 onTapDown
参数,该参数是在「点击按下」时回调:
onTapDown: (d) {
setState(() {
btnColor = btnColors[1];
});
也没有多余复杂的东西,就是改变按钮的颜色。
如何处理点击后?或者没有点击?
GestureDetector
也帮我们封装好了:
•onTapUp:在点击抬起时回调•onTapCancel:在取消点击时回调
首先我们处理取消点击:
onTapCancel: () {
setState(() {
btnColor = btnColors[0];
});
}
把颜色变回去就行了。
然后处理抬起时的逻辑,在抬起时也有两个逻辑:
1.按钮会缩小成圆形2.缩小成圆形的时候会弹出 ok 图标
首先说一下第一点:
缩小成圆形的时候是有一个回弹效果的,所以不能使用 AnimatedContainer
这种,必须要使用 Animation
才有这种效果。
所以我使用了 AnimatedBuilder
来包装这个 Widget。
然后说一下第二点:
如何在缩小成圆形的时候弹出 ok 图标?
我们可以使用 IndexStack
,在开始缩小动画的时候切换 index,因为 ok 图标开始时的缩放状态是 0,所以页面上是没有图标的,方便我们后续做动画。
Widget 代码如下:
AnimatedBuilder(
animation: _widthAnimation,
builder: (BuildContext context, Widget child) {
return Container(
width: _widthAnimation.value,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(40)),
color: btnColor),
margin: EdgeInsets.only(top: btnMargin),
height: btnHeight,
child: IndexedStack(
alignment: Alignment.center,
index: index,
children: <Widget>[
Text(
'Accepteer',
style: TextStyle(fontSize: 18),
),
ScaleTransition(
scale: _scaleTween.animate(_animation),
child: Image.asset(
'images/ok.png',
width: 35,
height: 35,
),
)
],
),
);
},
),
这个也很简单,Container
默认就有一个参数是:foregroundDecoration
,我们只需要在这个参数里设置上我们想要遮罩的颜色就可以了。
但是也需要注意一点,如果最开始使用的遮罩颜色为「透明色」,那么会整体变黑一下,这个具体的原因我也没研究明白,有知道的大佬可以告知一下。
这样按钮点击后的效果就全部完成,代码如下:
onTapUp: (d) {
Future.delayed(Duration(milliseconds: 60), () {
setState(() {
foregroundColor = Colors.white70;
btnColor = btnColors[0];
index = 1;
});
_widthController.forward();
Future.delayed(Duration(milliseconds: 200), () {
_controller.forward().then((va) {
Navigator.pop(context);
});
});
});
}
这个相对来说就更简单了,我们只需要在 logo 的上方套一个 AnimatedContainer
,
然后监听 dialog 是否已经 dismiss,如果已经 dismiss 那么则调整 margin 的值就好了。
代码如下:
setState(() {
logoMargin = 100;
});
这样正好 dialog 会有一个下移的动画,而 logo 上移,就达到了我们想要的效果。
如何把文字呈波浪形弹出?
我们最先想到的肯定就是动画,因为也只有动画才有这种回弹的效果,
那这么多文字,每一个都要设置动画?
答案是肯定的。
既然知道了,那我们也只能按部就班的做了。
可以看到,每一个文字都是由透明转为不透明,并且还会更改位置,
那我们还是先来封装一个 AnimatedWidget
。
代码如下:
class AnimatedStrWidget extends AnimatedWidget {
final Tween<double> _opacityTween = Tween(begin: 0, end: 1);
final Tween<Offset> _offsetTween =
Tween(begin: Offset(0, 3), end: Offset(0, 0));
final Widget child;
AnimatedStrWidget(
{Key key, @required Animation<double> animation, @required this.child})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Opacity(
opacity: _opacityTween.evaluate(animation) < 0
? 0
: _opacityTween.evaluate(animation) > 1
? 1
: _opacityTween.evaluate(animation),
child: SlideTransition(
position: _offsetTween.animate(animation),
child: child,
),
);
}
}
这里也有两点需要注意:
1.透明度不能有负数并且不能大于1,因为我们这个效果是要有回弹的效果,所以要做判断。2.Tween<Offset> 这里的值是整个高度的倍数,所以不要以为是像素值。
封装好以后我们就可以愉快的玩耍了:
void startAnim() async {
for (int i = 0; i < strs.length; i++) {
Future.delayed(
Duration(
milliseconds: i * 100,
), () {
_strController[i].forward();
});
}
}
文字弹出效果时间为 600ms,这里设置每隔100ms做一个动画,
这样的效果是比较好的,更像波浪形弹出。
主动弹出键盘我们应该都有所了解,使用 FocusNode
,
这里我们也是只需要判断最后一个动画何时做完,然后把隐藏的键盘弹出,并且把键盘弹出就ok了。
代码如下:
_strPositionAnimation[strs.length - 1].addStatusListener((status){
if(status == AnimationStatus.completed){
setState(() {
opacity = 1;
FocusScope.of(context).requestFocus(myFocusNode);
});
}
});
实现这个页面耗费了我一个晚上的时间,不得不说,东西还是不少的。
想要实现这样酷炫的登录页,还是比较复杂。
这里我实现的还不是很完美,看起来对比原图有些「着急」。
不过无所谓了,就是改变一下动画持续时间的事。
还是那句话,梳理好需求,什么都好做。
代码已上传至 GitHub:https://github.com/wanglu1209/WFlutterDemo