Flutter | 一个超级酷炫的登录页是怎样炼成的

12,533 阅读7分钟

近些日子在 UIMovement 上看到了一个比较酷炫的登录页效果,如下:

觉得很酷炫,就自己实现了一下,效果如下:

下面就来一步一步的分析是如何做出来的。

需求分析

首先还是老套路,看一下都需要做什么事情:

  1. 首先我们最清晰明了的需求就是点击「注册」弹出 Dialog
  2. 弹出 Dialog 后延迟一段时间弹出 Dialog 里的内容
  3. Dialog 内说明文字有两种颜色
  4. 点击 「Accepter」按钮会变色缩小回弹并展示 ok图标
  5. 点击「Accepter」按钮时 Dialog 内其他文字都被「白色遮罩」
  6. 「Accepter」按钮 动画结束后 dismiss 掉当前dialog 并把 logo向上移
  7. 跳转到第二页,文字呈波浪形弹出
  8. 文字弹出后显示对话框并弹出键盘

开始实现

需求了解了,下面就是一步一步的实现效果。

1. 点击「注册」弹出 Dialog

在这里我们需要注意的有一点:

在我们使用 showModalBottomSheet 时,默认的背景是白色的,也就是说我们自己设置的圆角是不管用的,

所以要给这个 BottomSheet 一个背景,这个参数在 showModalBottomSheet 方法中就有:

showModalBottomSheet(
  context: context,
  backgroundColor: Colors.transparent,
  builder: (context) {
    Future.delayed(Duration(milliseconds: 50), () {
      _animationController.forward();
    });
    return AnimatedUserAgreement(
      animation: _animation,
    );
  });
)

设置一个 backgroundColor 就ok了。

2. 弹出 Dialog 后延迟一段时间弹出 Dialog 里的内容

这里我是写了一个 「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 的话,会溢出。

3. Dialog 内说明文字有两种颜色

有两种颜色这个需求还是比较简单的,使用 「TextSpan」就搞定了。

代码我就不贴了。

4. 点击 「Accepter」按钮会变色缩小回弹并展示 ok图标

重点来了,这个功能是相对来说比较复杂的,但是只要我们了解需求,写起来也是比较简单。

首先我们也是把这个功能点拆分一下:

  1. 点击按钮的时候会变色
  2. 点击后会变回原来的颜色并缩小成一个圆形
  3. 变成圆形后动画效果展示 ok 图标

也还是一步一步来。

1. 点击按钮的时候会变色

该功能不用考虑太多,既然有点击手势,那必然会使用 GestureDetector

然后使用 GestureDetectoronTapDown 参数,该参数是在「点击按下」时回调:

onTapDown: (d) {
  setState(() {
    btnColor = btnColors[1];
  });

也没有多余复杂的东西,就是改变按钮的颜色。

2. 点击后会变回原来的颜色并缩小成一个圆形

如何处理点击后?或者没有点击?

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,
            ),
          )
        ],
      ),
    );
  },
),

5. 点击「Accepter」按钮时 Dialog 内其他文字都被「白色遮罩」

这个也很简单,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);
      });
    });
  });
}

6. 动画结束后 dismiss 掉当前dialog 并把 logo向上移

这个相对来说就更简单了,我们只需要在 logo 的上方套一个 AnimatedContainer

然后监听 dialog 是否已经 dismiss,如果已经 dismiss 那么则调整 margin 的值就好了。

代码如下:

setState(() {
  logoMargin = 100;
});

这样正好 dialog 会有一个下移的动画,而 logo 上移,就达到了我们想要的效果。

7. 跳转到第二页,文字呈波浪形弹出

如何把文字呈波浪形弹出?

我们最先想到的肯定就是动画,因为也只有动画才有这种回弹的效果,

那这么多文字,每一个都要设置动画?

答案是肯定的。

既然知道了,那我们也只能按部就班的做了。

可以看到,每一个文字都是由透明转为不透明,并且还会更改位置,

那我们还是先来封装一个 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做一个动画,

这样的效果是比较好的,更像波浪形弹出。

8. 文字弹出后显示对话框并弹出键盘

主动弹出键盘我们应该都有所了解,使用 FocusNode

这里我们也是只需要判断最后一个动画何时做完,然后把隐藏的键盘弹出,并且把键盘弹出就ok了。

代码如下:

_strPositionAnimation[strs.length - 1].addStatusListener((status){
  if(status == AnimationStatus.completed){
    setState(() {
      opacity = 1;
      FocusScope.of(context).requestFocus(myFocusNode);
    });
  }
});

总结

实现这个页面耗费了我一个晚上的时间,不得不说,东西还是不少的。

想要实现这样酷炫的登录页,还是比较复杂。

这里我实现的还不是很完美,看起来对比原图有些「着急」。

不过无所谓了,就是改变一下动画持续时间的事。

还是那句话,梳理好需求,什么都好做。

代码已上传至 GitHub:github.com/wanglu1209/…

另我个人创建了一个「Flutter 交流群」,可以添加我个人微信 「17610912320」来入群。

推荐阅读:

Flutter | WReorderList 一个可以指定两个item互换位置的组件

Flutter | 如何实现一个水波纹扩散效果的 Widget

Flutter | 自定义一个 Stepper 步骤组件

img