Flutter 手势处理 & Hero 动画

App Store能够说是苹果业内设计的标杆了。git

咱们就来简单的实现一下 App Store的首页里其中的一个小功能。github

先看图:bash

能够看到,这里有两点须要关注一下:markdown

  1. 在点击这个卡片的时候会缩放,松开或者滑动的时候会回弹回去。
  2. 跳新页面的时候有元素共享。

实现结果:app

手势处理

在Flutter中的手势事件分为两层。ide

第一层有原始指针事件,它描述了屏幕上指针(例如,触摸,鼠标和触控笔)的位置和移动。动画

第二层有手势,描述由一个或多个指针移动组成的语义动做。ui

简单的手势处理,咱们使用 Flutter 封装好的GestureDetector来处理就彻底够用。this

咱们这里的图片缩放效果就用GestureDetector来处理。spa

先来看一下GestureDetector 给咱们提供了什么样的方法:

  • onTapDown:按下
  • onTap:点击动做
  • onTapUp:抬起
  • onTapCancel:触发了 onTapDown,但并无完成一个 onTap 动做
  • onDoubleTap:双击
  • onLongPress:长按
  • onScaleStart, onScaleUpdate, onScaleEnd:缩放
  • onVerticalDragDown, onVerticalDragStart, onVerticalDragUpdate, onVerticalDragEnd, onVerticalDragCancel, onVerticalDragUpdate:在竖直方向上移动
  • onHorizontalDragDown, onHorizontalDragStart, onHorizontalDragUpdate, onHorizontalDragEnd, onHorizontalDragCancel, onHorizontalDragUpdate:在水平方向上移动
  • onPanDown, onPanStart, onPanUpdate, onPanEnd, onPanCancel:拖曳(触碰到屏幕、在屏幕上移动)

那咱们知道了这些方法,咱们就能够来分析一下,哪些适合咱们作这个效果:

咱们能够看到,当咱们的手指触碰到卡片的时候就开始缩放,当开始移动或者抬起的时候回弹。

那咱们根据上面 GestureDetector 的方法,能够看到 onPanDown、onPanCancel 彷佛很是适合咱们的需求。

那咱们就能够来试一下:

监听手势的方法有了,那咱们下面就来写动画。

如何让Card 进行缩放呢,Flutter 有一个 Widget,ScaleTransition

照例点开源码看注释:

/// Animates the scale of a transformed widget.
复制代码

对scale进行动画缩放的组件。

那这就结了,直接在 onPanDown、onPanCancel 方法中写上动画就完了:

Widget createItemView(int index) {
  var game = _games[index]; // 获取数据
  // 定义动画控制器
  var _animationController = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 200),
  );  
  // 定义动画
  var _animation =
    Tween<double>(begin: 1, end: 0.98).animate(_animationController);
  return GestureDetector(
    onPanDown: (details) {
      print('onPanDown');
      _animationController.forward(); // 点击的时候播放动画
    },
    onPanCancel: () {
      print('onPanCancel');
      _animationController.reverse(); // cancel的时候回弹动画
    },

    child: Container(
      height: 450,
      margin: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
      child: ScaleTransition(
        scale: _animation, // 定义动画
        child: Stack( // 圆角图片为背景,上面为text
          children: <Widget>[
            Positioned.fill(
              child: ClipRRect(
                borderRadius: BorderRadius.all(Radius.circular(15)),
                child: Image.asset(
                  game.imageUrl,
                  fit: BoxFit.cover,
                ),
              ),
            ),
            
            Padding(
              padding: const EdgeInsets.all(18.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    game.headText,
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.grey,
                    ),
                  ),
                  
                  Expanded(
                    child: Text(
                      game.title,
                      style: TextStyle(
                        fontSize: 30,
                        color: Colors.white,
                      ),
                    ),
                  ),
                  
                  Text(
                    game.footerText,
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.grey,
                    ),
                  ),
                ],
              ),
            )
          ],
        ),
      )),
  );
}
复制代码

这样就能够完成咱们刚上图的动画效果了。

这里有一个须要注意的地方是:

ListView 中必须每个 item 有一个 动画。

否则全部的item公用一个动画的话,点击其中一个,全部的item 都会执行动画效果。

Hero动画

点击缩放效果咱们处理完了,下面就应该来跳转了。

在Android中,5.0之后版本就有了元素共享,能够实现这种效果。

在Flutter当中咱们可使用 Hero 来实现这个效果。

打开官网看介绍:

A widget that marks its child as being a candidate for hero animations.

When a PageRoute is pushed or popped with the Navigator, the entire screen's content is replaced. An old route disappears and a new route appears. If there's a common visual feature on both routes then it can be helpful for orienting the user for the feature to physically move from one page to the other during the routes' transition. Such an animation is called a hero animation. The hero widgets "fly" in the Navigator's overlay during the transition and while they're in-flight they're, by default, not shown in their original locations in the old and new routes.

To label a widget as such a feature, wrap it in a Hero widget. When navigation happens, the Hero widgets on each route are identified by the HeroController. For each pair of Hero widgets that have the same tag, a hero animation is triggered.

If a Hero is already in flight when navigation occurs, its flight animation will be redirected to its new destination. The widget shown in-flight during the transition is, by default, the destination route's Hero's child.

For a Hero animation to trigger, the Hero has to exist on the very first frame of the new page's animation. Routes must not contain more than one Hero for each tag. 复制代码

简单来讲:

Hero动画就是在路由切换时,有一个共享的Widget能够在新旧路由间切换,因为共享的Widget在新旧路由页面上的位置、外观可能有所差别,因此在路由切换时会逐渐过渡,这样就会产生一个Hero动画。

要触发Hero动画,Hero必须存在于新页面动画的第一帧。

而且一个路由里只能有一个Hero 的 tag。
复制代码

说了这么多,怎么用?

// Page 1
Hero(
  tag: "avatar", //惟一标记,先后两个路由页Hero的tag必须相同
  child: ClipOval(
    child: Image.asset("images/avatar.png",
                       width: 50.0,),
  ),
),

// Page 2
Center(
  child: Hero(
    tag: "avatar", //惟一标记,先后两个路由页Hero的tag必须相同
    child: Image.asset("images/avatar.png"),
  ),
)
复制代码

能够看到只须要在你想要共享的widget 前加上 Hero,写上 tag便可。

赶忙试一下:

child: Container(
  height: 450,
  margin: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
  child: ScaleTransition(
    scale: _animation,
    child: Hero(
      tag: 'hero',
      child: Stack(
        children: <Widget>[
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.all(Radius.circular(15)),
              child: Image.asset(
                game.imageUrl,
                fit: BoxFit.cover,
              ),
            ),
          ),
   
复制代码

运行看下:

直接黑屏了是什么鬼?

看到了报错信息:

There are multiple heroes that share the same tag within a subtree.

多个hero widget 使用了同一个标签
复制代码

那咱们改造一下:

child: Container(
  height: 450,
  margin: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
  child: ScaleTransition(
    scale: _animation,
    child: Hero(
      tag: 'hero${game.title}',
      child: Stack(
        children: <Widget>[
          Positioned.fill(
            child: ClipRRect(
              borderRadius: BorderRadius.all(Radius.circular(15)),
              child: Image.asset(
                game.imageUrl,
                fit: BoxFit.cover,
              ),
            ),
          ),
 // ........代码省略
复制代码

咱们使用 ListView 里的数据来填充tag,这样就不会重复了,运行一下:

这跳转的时候文字下面有两个下划线是什么鬼?

查了一下,是由于跳转的时候,Flutter 把源 Hero 放在了叠加层,而叠加层里是没有 Theme的。

简单理解就是叠加层里没有Scaffold,因此就会出现下划线。

解决办法以下:

在textStyle中加入 decoration: TextDecoration.none,

如今就彻底没有问题了:

总结

在初学Flutter 时,咱们确实会出现这样那样的问题。

不要心烦,点开源码,或者去 Flutter 官网找到该类,看一下注释和demo,问题分分钟解决。

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

相关文章
相关标签/搜索