使用Flutter仿写TikTok的手势交互

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

写在前面

Flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。

Flutter官网:https://flutter-io.cn

抖音,英文名TikTok,一款火遍全球的短视频App。在玩抖音的日子里,最令我感到舒服的就是抖音的手势交互,加上近期都在进行Flutter方面的学习,因此就产生了使用Flutter来仿写TikTok手势交互的想法。

来看看实现的效果

Gif:https://giphy.com/gifs/Y0nMQwaOg14vWmwQDz

Github地址:https://github.com/ditclear/tiktok_gestures

demo下载:

GestureDetector以及Transform

既然是手势交互,那么就必然要检测手势,在Flutter中提供了GestureDetector来帮助开发者,并提供了多个回

调来处理手势。

Property/Callback Description
onTapDown 用户每次和屏幕交互时都会被调用
onTapUp 用户停止触摸屏幕时触发
onTap 短暂触摸屏幕时触发
onTapCancel 用户触摸了屏幕,但是没有完成Tap的动作时触发
onDoubleTap 用户在短时间内触摸了屏幕两次
onLongPress 用户触摸屏幕时间超过500ms时触发
onVerticalDragDown 当一个触摸点开始跟屏幕交互,同时在垂直方向上移动时触发
onVerticalDragStart 当触摸点开始在垂直方向上移动时触发
onVerticalDragUpdate 屏幕上的触摸点位置每次改变时,都会触发这个回调
onVerticalDragEnd 当用户停止移动,这个拖拽操作就被认为是完成了,就会触发这个回调
onVerticalDragCancel 用户突然停止拖拽时触发
onHorizontalDragDown 当一个触摸点开始跟屏幕交互,同时在水平方向上移动时触发
onHorizontalDragStart 当触摸点开始在水平方向上移动时触发
onHorizontalDragUpdate 屏幕上的触摸点位置每次改变时,都会触发这个回调
onHorizontalDragEnd 水平拖拽结束时触发
onHorizontalDragCancel onHorizontalDragDown没有成功完成时触发
onPanDown 当触摸点开始跟屏幕交互时触发
onPanStart 当触摸点开始移动时触发
onPanUpdate 屏幕上的触摸点位置每次改变时,都会触发这个回调
onPanEnd pan操作完成时触发
onScaleStart 触摸点开始跟屏幕交互时触发,同时会建立一个焦点为1.0
onScaleUpdate 跟屏幕交互时触发,同时会标示一个新的焦点
onScaleEnd 触摸点不再跟屏幕有任何交互,同时也表示这个scale手势完成

GestureDetector并不会监听上面所有的手势,只有传入的callbacks非空时,才会监听。所以,如果你想要禁用某个手势时,可以给对应的callback传null。

本文主要关注的的是拖动相关的,比如onPanXXonHorizontalDragXXonVerticalDragXX等等回调事件。

Transform可以在其子Widget绘制时对其应用一个矩阵变换(transformation),Matrix4是一个4D矩阵,通过它我们可以实现各种矩阵操作。

Container(
  color: Colors.black,
  child: new Transform(
    alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
    transform: new Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
    child: new Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: const Text('Apartment for rent!'),
    ),
  ),
);

效果如下:

在Flutter中提供了一些封装好的transform效果供开发者选择,比如:平移(translate)、旋转(rotate)、缩放(scale)。

在了解了这两点之后,我们来逐步分解前文的效果。

交互分解

首先,需要明确的是这些交互效果其实都是通过检测手指的滑动,得到一个x坐标或者y坐标的偏移量,然后配合Transform进行各种不同的变换,明白了这一点,想做到这样的效果并不难。

  • 首页的交互
image

Gif :https://media.giphy.com/media/iFgOIQ7abZx0i98MCA/giphy.gif

这里的交互都是横向的滑动,因此这里主要处理onHorizontalDragXX相关的事件。

然后来看看首页的布局:

Left:拍摄页 Middle:主页 Right:用户页

外层是一个GestureDetector用于处理整个页面的手势,里面用的是一个Stack,类似于Android中的FrameLayout,它包含3个Transform的子Widget。

这里选取拍摄页(left)来具体谈谈.

通过观察可以发现,随着偏移量的改变,这里其实包含两个变化:1.缩放 2. 前景色透明度

缩放可以直接采用前文提到的Transform.scale,前景色可以用foregroundDecoration通过改变Color的透明度来达到效果,看看实现:

/// 左侧Widget
///
/// 通过 [Transform.scale] 进行根据 [offsetX] 缩放
/// 最小 0.88 最大为 1
Transform buildLeftPage(double screenWidth) {
  return Transform.scale(
    scale: 0.88 + 0.12 * offsetX / screenWidth < 0.88 ? 0.88 : 0.88 + 0.12 * offsetX / screenWidth,
    child: Container(
      child: Image.asset(
        "assets/left.png",
        fit: BoxFit.fill,
      ),
      foregroundDecoration: BoxDecoration(
          color: Color.fromRGBO(0, 0, 0, 1 - (offsetX / screenWidth)),
         ),
    ),
  );
}

当我们的手指在横向移动的时候,记录下偏移总量offsetX,然后通过setState进行更新。

onHorizontalDragUpdate: (details) {
  // 控制 offsetX 的值在 -screenWidth 到 screenWidth 之间
  if (offsetX + details.delta.dx >= screenWidth) {
    setState(() {
      offsetX = screenWidth;
    });
  } else if (offsetX + details.delta.dx <= -screenWidth) {
    setState(() {
      offsetX = -screenWidth;
    });
  } else {
    setState(() {
      offsetX += details.delta.dx;
    });
  }
}

通过setState更新偏移量offsetX之后,Flutter便会重新渲染视图,从而达到上图的效果。

  • Hero动画

Gif:https://media.giphy.com/media/Q7LdOFFBM4HB8xGSpg/giphy.gif

Flutter提供了Hero动画来实现这样的过渡效果。Hero指的是可以在路由(页面)之间“飞行”的widget,简单来说Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。

/// tiktok_page.dart
Widget build(BuildContext context) {
        return  Hero(
              tag: "detail",
              //child
            )
 )               
 
 /// detail_page.dart
 Widget build(BuildContext context) {
    return Hero(
      tag: "detail",
      // child
        )
  }

保证tag一致就可以了。

  • 详情页的交互

Gif :https://media.giphy.com/media/h4HMGtcLLcQQmqTZTI/giphy.gif

跟首页一样的思路,只是这里的手势是垂直方向。

布局同样是GestureDetector加上Stack再配合Transform.translate

Hero(
      tag: "detail",
      child: GestureDetector(
        onVerticalDragUpdate: (details){
          // dy 不超过 -screenHeight * 0.6
          dy += details.delta.dy;
          if ((dy < 0 && dy.abs() > screenHeight * 0.6)) {
            dy = -screenHeight * 0.6;
          } else {
            setState(() {});
          }
        },
        child: Stack(
          children: <Widget>[
            Image.asset(
              "assets/detail.png",
              fit: BoxFit.fitWidth,
              width: screenWidth,
              height: screenHeight,
            ),
            Transform.translate(
              offset: Offset(0, dy + screenHeight),
              child: Container(
                  height: screenHeight * 0.6,
                  child: GestureDetector(
                    onTap: () {},
                    child: Image.asset(
                        "assets/comment.png",),
                  )
              ),
            ),
          ],
        ),
      ),
    );

在手指离开屏幕时,根据偏移利用动画进行调整。

onVerticalDragEnd: (_){
          // 滑动截止时,根据 dy 判断是展开还是回缩
          if (dy < 0) {
            if (!isCommentShow && dy.abs() > screenHeight * 0.2) {
              if (dy.abs() > screenHeight * 0.2) {
                animateToTop(screenHeight);
              } else {
                animateToBottom(screenHeight);
              }
            } else {
              if (dy.abs() > screenHeight * 0.4) {
                animateToTop(screenHeight);
              } else {
                animateToBottom(screenHeight);
              }
            }
          }
        },

写在最后

总的来说,这些交互都是依靠着对手势的检测做到的,相比于Android,Flutter有着一切都是Widget的概念,

GestureDetector以及Hero都是Widget而且提供了很多回调函数,再配合数据驱动UI和Flutter优秀的渲染机制,减轻了开发者进行手势交互的难度。

Github地址:https://github.com/ditclear/tiktok_gestures

如果本文对你有帮助,请点赞支持。

参考资料:

==================== 分割线 ======================

如果你想了解更多关于MVVM、Flutter、响应式编程方面的知识,欢迎关注我。

你可以在以下地方找到我:

简书:https://www.jianshu.com/u/117f1cf0c556

掘金:https://juejin.im/user/582d601d2e958a0069bbe687

Github: https://github.com/ditclear

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