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要从状态创建,则可以使用TickerProviderStateMixinSingleTickerProviderStateMixin类来获取合适的状态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,由动画结束按钮被降低到width70.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),
)

最后,我们希望按钮容器增长并覆盖整个屏幕。为此,我们将首先删除整个页面的paddingtopbottom),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");
  }
});

动画主屏幕

就像登录屏幕一样,主屏幕上的动画可以分为以下几部分:

主屏幕动画(慢动作)

从上面显示的G​​IF中,我们可以看到当应用程序导航到主屏幕时,首先发生的事情是屏幕渐变为白色。

淡出屏幕

淡入淡出动画(慢动作)

为此,我们创建一个新的ColorTween小部件并将其命名为fadeScreenAnimation。在这个小部件中,我们将opacity1.0更改为0.0,使颜色消失。我们将此小部件链接到CurvedAnimation使用animate()

接下来,我们创建一个名为的新窗口小部件FadeBoxfadeScreenAnimation在其中调用。

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");
}

这就是所有人!

你可以在这里查看这个应用程序的完整代码

转:https://blog.geekyants.com/flutter-login-animation-ab3e6ed4bd19

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容