前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >怎样在Android上实现一个iOS多任务列表效果

怎样在Android上实现一个iOS多任务列表效果

原创
作者头像
帅气的程序员
修改2020-08-13 16:20:20
3.4K1
修改2020-08-13 16:20:20
举报
文章被收录于专栏:Android开发之路Android开发之路

| 导语 苹果在iOS 7的时候就引入了卡片列表进行多任务切换,往上滑动就可以移除掉某个app,到了最新的iOS 13,其多任务列表也是在这种卡片列表样式的基础上进行了优化;Android阵营的华为,小米等厂商也是陆续地引入这种多任务列表样式。 那怎样在Android上实现一个iOS多任务列表效果呢?

一. 实现效果

先看看iOS的多任务列表长啥样。

再来看看华为的多任务列表。

最后来看看本篇实现的效果。

二. 实现方案

实现这样一个iOS多任务列表,需要具备以下几个基本能力:

       1)横向列表

       2)卡片堆叠效果

       3)滑动移除动画

       4)支持大量数据绑定,每个卡片都有独立的容器管理

纵观Android标准的控件库,能想到的就只有ViewPager比较合适,其首先满足第1点,ViewPager又是直接使用Adapter来管理数据,然后通过Fragment来管理每个item,满足第4点(这一点很重要,Adapter+Fragment这种成熟的设计,会让调用代码很简洁),剩下的2,3点都是UI层面的效果,应该改改ViewPager的源码就可以实现了吧,微笑。

三. 基于ViewPager的代码实现

       如果对实现细节感兴趣,请继续往下看,以下内容超过2000字!!!

基于ViewPager现有能力,要仿照iOS多任务列表效果,还需要修改以下几点:

       1)ViewPager默认的item排列是横向顺序排列,需要变成卡片叠加排列

       2)ViewPager不管你滑动地多快,他只会切换到前一个或后一个item,需要变成可以根据滑动速度滚动不同的距离(可以理解成fling效果)

       3)需要支持上下滑动item以移除,移除后,其后面的item要有补齐上来的动画效果

1. 卡片叠加效果

我们要实现的卡片叠加效果大概分两步,第1步是让item的宽高缩放到一个卡片的大小,第2步是卡片之间有重叠,而且重叠部分会随着滑动过程在变化(如果是华为那种多任务列表,这一步就省略了)。

      下面看下实现细节。

1.1. 卡片宽高

我们知道,正常情况我们在Fragment返回的View是铺满ViewPager宽高的,上下的空隙我们可以设置padding来实现,左右的是不是也可以设置padding来实现呢?

       实践一下,如下效果:

左右的效果果然不符合我们预期,item的宽度是变小了,但左右的padding一直空白着,经过一番尝试,最终通过一个属性解决了这个问题:

viewPager.setClipToPadding(false);

       这个是ViewGroup的基础接口,默认是true,设为false后,就可以允许内容区显示在padding区域内,不止是ViewPager,平时的listview,scrollview这类滚动控件,都是可以通过这个接口来避免上述问题,内部实现原理这里不展开。

       另外再谈一个问题,设置ViewPager的padding,影响到的应该是整个ViewPager的内容区域(即所有item view加起来的区域)大小,为什么作为ViewPager的一个item view也受影响?ViewPager的源码有一个接口如下:

       getClientWidth这个接口在ViewPager里被频繁使用到,包括在onMeasure里对child View进行measure计算的时候,可以看出,ViewPager在一开始设计的时候,就是假设一个item 的宽度(即ClientWidth)是measureWidth-paddingLeft-paddingRight的,微笑。

1.2. 卡片重叠

       ViewPager提供的接口已经可以支持这种效果,有两种方法。

       第一种比较简单,直接调viewPager.setPageMargin,给一个负值,卡片就会重叠在一起,但重叠的区域大小不会随着滚动而变化,显然不是我们想要的;

       第二种是使用PageTransformer,滚动过程中,ViewPager会回调transformPage(View page, float position),在这里面做想要的变化就行,PageTransformer具体的使用方法这里不细讲,网上有很多例子,放到我们这个场景下,变化逻辑是,item从右边往左边移动过程中,item view逐渐放大,x方向的偏移也会逐渐增大,具体代码在demo的DefaultPageTransformer里面。需要注意的是,setPageTransformer方法的第一个参数(reverseDrawingOrder)需要是true,因为我们的效果是后面的item被叠在前面item的后面,而ViewPager默认的实现是后面的item盖在前面item上面。

这里讲一下ViewPager是怎么调用PageTransformer的,只有一处地方回调,如下:

可以看到,ViewPager在onPageScrolled方法里都会对每一个child调用mPageTransformer.transformPage来进行View变化,这里重点看transformPos这个值,也即transformPage回调方法里的position参数,transformPos = (child.getLeft() - scrollX) / getClientWidth(),getClientWidth也即item view的宽度,这么算什么意思?

       有一点抽象,以当前显示在ViewPager的最左边item A为例,A的left紧贴着ViewPager的left,这时候child.getLeft() – scrollX = 0,即transformPos=0,再假设紧挨着A后面的item是B,B的left应该是A.left+A.width,所以B的transformPos=(B.getLeft() - scrollX) / getClientWidth()=(A.left+A.width-A.left) / A.width = 1,所以从B的位置滚动到A的位置,position也从1变化到0,其他位置的position以此类推,当item已经在View Pager显示范围左边时(超出屏幕外),这个值就是负的。

2. 快速滚动

第1点的实现,到目前还不需要修改ViewPager的源码,但到了第2点这里,就需要在ViewPager的源码基础上来修改我们想要的逻辑了。

先看现有ViewPager在onTouchEvent里对于Up事件是怎么处理的。

重点看标红的,第一步先调determineTargetPage算出最终要滚动到的page位置,第二步调setCurrentItemInternal滚动到最终位置;determineTargetPage的逻辑比较简单,可以自己看看源码里的实现,主要就是根据当前的滑动方向,确定要滚动到上一个item还是下一个item,而我们现在想要快速滑动松手后,可以滚动到更远位置,是不是直接修改determineTargetPage的逻辑就行了?

这里直接看下实现后的代码:

标红的部分是这次新加的,大概的逻辑是,根据当前的速度,在一个最大可滑行距离MAX_FLING_ITEM范围内,算出一个最终目标page距离,这只是一个比较简单的实现方法,可以根据需求的需要做相应的算法调整。

3. 移除动画

要做到iOS多任务列表的移除效果,需要分两步,第一步是对要移除的item做上下滑动动画;第二步是item滑出去后,其后面的item要做偏移动画补齐到当前空白的位置。

3.1. item上下滑动动画

这一步实现原理比较简单,就是在ViewPager的onTouchEvent里对move事件做上下滑动检测,满足条件时对当前的item view做上下移动即可,当up事件到达时,再根据速度和偏移条件,判断是否真要滑动移除,要的话再触发相应的动画。

原理虽简单,但实现起来代码还是不少,这里不详细展开,可以看看MultCardViewPager新加的tryRemoveAItem方法。

3.2. 对移除item后面的item做补齐动画

在第一步的item移除动画结束后,需要开始对后面的item做补齐动画,逻辑在removeItemViewAndAnimate方法里,如下:

       这个方法主要做的事情是找出移除item的所有后续item,如果存在后续item,则调animateRestView触发补齐动画,这个方法这里不详细讲,需要关注的是,获取后续item需要通过mDrawingOrderedChildren来获取,而不能通过getChildAt来获取,因为ViewGroup的child数组存放View的顺序并不完全对应屏幕显示item的从左到右顺序(为什么?因为ViewPager可以先往后滑,再往前滑,这时候前面的item可能是刚创建出来的,addView的时候肯定就存在child数组的最后面,但事实上这个view是显示在屏幕的最前面),而mDrawingOrderedChildren可以理解为ViewPager自己保存的一个和当前显示顺序相同的数组,直接拿来用就行了。

       下面看看animateRestView怎么做补齐动画。

第一步先初始化Animator的相关参数,之所以通过updateListener来做动画,是因为后面显示的item可能有多个,在onAnimationUpdate里对所有要做偏移动画的View调整位置即可;第二步是需要不断地调整View的位置,注意的是,这里再分两步,第一步是调整View的left,为什么是setLeft,而不是setTranslationX,因为这里假设移除item已经移除掉了,后面的view就应该调整当前坐标位置了,严格来说,并不是在做偏移动画,而是在调整位置,第二步需要调mPageTransformer.transformPage来确定最终的变化,因为我们这种场景下,item的位置受PageTransformer影响。

动画做完了,是不是就结束了?

       不是的,还涉及到一个数据问题,我们上面移除item都是在View(ViewPager)层做的,可以说,只是展示效果上实现了移除一个item,但真实的数据是在Adapter里,需要在动画结束后回调给Adapter,让Adapter移除掉相应的数据,最后调notifyDataSetChanged同步数据。这里新增了OnItemRemoveListener作为回调Listener,具体可以参考源码。

四. 总结

最后总结一下,本篇介绍了如何基于ViewPager,实现了一个类似iOS多任务列表效果,主要目的在于验证方案的可行性,即如何在已有控件的基础上快速复用来实现我们要的效果,虽然效果实现出来了,但对比iOS的效果,仍然有不少地方需要优化,比如提高动画的细腻程度和流畅度(这方面Android和iOS相比真有差距);另外,细心的同学可能会发现,iOS的多任务列表是从右边开始,而我们的实现效果(或者说ViewPager)是从左边开始的,要实现成从右边开始,理论上可以实现,即把ViewPager所有和X坐标相关的操作都给他反过来就是了,目测需要改动的地方不少,先不折腾了,实现了的同学可以分享下哈。

更新:

       偶然看到androidx包下多了个ViewPager2,吃惊,看一下代码,注释如下:

       可以看到,ViewPager2已经支持了从右到左的布局了,也支持竖向布局,其源码实现是封装了RecyclerView,但接口几乎和ViewPager一致,也解决了RecyclerView不能直接使用Fragment的问题,腻害呀!

       目前ViewPager2还是处于beta版,估计还有一些bug,期待后续正式上线

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. 实现效果
  • 二. 实现方案
  • 三. 基于ViewPager的代码实现
  • 四. 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档