仿QQ列表左右滑动效果

640?wx_fmt=jpeg


今天这篇是 李政 的第二篇投稿,他之前的《浅谈WebView的页面跳转》着实帮助一些朋友解决了问题。现在我的公众号里有不少作者都是回头客了,希望之前投稿过的作者们能多多回来混个脸熟,没投稿过的朋友们也可以踊跃尝试一下,加入这个大圈子当中。


另外,本文尝试做出一些改变,这次使用视频来演示效果,视频很小,只有2M不到,期待大家的反馈。


李政 的博客地址:http://blog.csdn.net/lz8362


前言


ListView 可以说是最常用的控件了,所谓在平凡中创建不平凡,各种 ListView 的衍生版本层出不穷。


在商业应用中,一个 item 上堆放的信息不能太多,item 本身的作用在于提供给用户快速选择的权利,信息堆放太多,反而无法让用户快速决定,所以简洁明了才是真。


但是对于一些在场景中必要的功能按键,就需要找到合适的放置位置,尤其是现在大家公司里的美工们都是缺乏激情的MM,多给她们提供一些布局选择,你脱单的希望就增大了一些


QQ列表的效果一开始并没有吸引我,直到我发现我的测试手机(联想S650)中通讯录列表的item也是可以左右滑动。我的好奇心才被激起了。下面就来讲讲怎么实现这个功能。


先来个演示视频,看看效果:




布局样式及构建


我们把布局分成三部分:左侧布局,中央布局,右侧布局。中央布局就是在应用中用来正常展示,左侧和右侧布局是隐藏布局,样式如下:

640?wx_fmt=png

左侧布局,中央布局,右侧布局都是独立的布局,他们之间互不干扰


我们如何把它们组合起来呢,这就需要重写 onMeasure() onLayout() 方法了,所以我们来自定义一个 FrameLayout 来放置这三个布局。因为我们需要的是一个滑动的效果,所以给这个自定义控件添加一个 Scroller 来管理滑动。这样基本属性和对象我们就搭建出来了:


640?wx_fmt=png


之所以设置 isLeftExist isRightExist,是为了提高控件的利用率,缺少哪一个布局,就禁止相反方向的滑动。 也是对程序逻辑的一种保护。


这个布局控件继承了 FrameLayout,在xml编辑器中布局时,我们都知道 FramgLayout 的特点,控件的顺序是从左向右依次放置的,所以我们在重写构造函数时,要调用 addView 把左中右三个布局按从左到右的顺序添加上。


640?wx_fmt=png


Scroller 作为滑动管理器,我们也需要在构造函数中声明,创建时我们可以选择传或者不传滑动插值器的类型,默认不传的话,就是匀减速插值器 DecelerateInterpolator,这里我设置为反弹插值器BounceInterpolator


640?wx_fmt=png


接下来,我们先重写 onMeasure() 方法,onMeasure() 方法是用来计算布局的大小的,也是最先被调用的方法,只有知道的布局的大小,设备才能正常绘制出我们的布局。


在这里我们不需要特别声明三部分布局的高宽,只需要声明宽是自适应,高为设备适应后的固定尺寸就行。这样做可以保证布局的正常展示,不会出现压缩现象。在声明过程中,我们需要使用 MeasureSpe.makeMeasureSpec 来指定宽高(这个方法是 onMeasure() 原有形参调用的方法,在这里我们要保持数据的一致性,所以也要使用这个方法),它需要两个参数,一个是 大小size,一个是 模式mode ,其中 MeasureSpec.UNSPECIFIED 表示当前模式为自适应,MeasureSpec.EXACTLY 表示当前模式为确定值或者是充满(match_parent)。


640?wx_fmt=png


下一步重写 onLayout() 方法,放置布局。因为手机屏幕左上方的位置是(0,0)点,屏幕左侧为负值,右侧为正值,而我们的左中右三个布局,必须要依次放置,View的layout() 方法中只使用的左上角和右下角的坐标,这样我们就可以得出左中右三布局的位置坐标:


640?wx_fmt=png
640?wx_fmt=png


Scroller滑动设置


首先明白一点,滑动的实际原理,就是布局坐标发生变化,在不断的重绘过程中,产生有规律的位移。这样我们就需要两个要点:


  1. 本次重绘中,需要位移的距离。

  2. 重绘的时机。


很幸运,Scroller 提供给我们了滑动时的位置变化,滑动的样式,滑动的时机,以及滑动的结束判断标记,所以我们只要伺候好 Scroller 就行了。


之前我们已经在构造函数中初始化了一个 Scroller 对象,下面为了让我们的控件可以滚动起来,我们需要重写控件的 computeScroll() 方法,顾名思义,是计算滑动距离的,这个方法在重绘时会被触发,但这个方法在 ViewGroup 中本身是没有具体操作,所以我们看不到什么变化。现在我们需要在这里重新获得当前位移的距离,并调用 postInvalidate() 进行重绘。位移的距离由 Scroller.getCurrX() 提供。这个方法可以实时获得当前移动轨迹下X轴的坐标变化。移动轨迹由 Scroller.startScroller() 触发,它有五个参数:


640?wx_fmt=png


滚动发起后,我们可以通过 computeScrollOffset() 来判断当前滚动是否结束,如果是,则为false,不是为true。有了坐标点的移动轨迹,我们就可以在滚动过程中不停地重新设置布局的坐标,重绘布局,从而实现视觉上的滚动效果。


640?wx_fmt=png


因为我们手指滑动的操作是左右滑动,所以在计算移动距离时,直接让布局移动 Scroller.getCurrX() 个距离就行,而当关闭布局时,直接移动  - 中央布局.getLeft() , 就是代码的 baseX - Scroller.getCurrX() 个距离就行,为什么取负值,因为负值表示是向相反的方向滑动。


ListView的监听


ListView 方面,我们只需要重写 onTouchEvent() 就行了,对用户触摸事件进行区分,同时为了实现上下滑动时恢复原来位置,我们需要分别记录当前视图和历史视图,当前位置和历史位置。在对应的 MotionEventAction() 中作如下操作:


一、MotionEvent.ACTION_DOWN:


  1. 获取当前的视图对应的位置,以及用户的点击点坐标。

  2. 判断是否存在历史视图(即前期是否有未关闭的视图),若存在,先对历史视图进行归位操作。

  3. 将当前视图备份到历史视图中。


二、MotionEvent.ACTION_MOVE:


  1. 判断滑动手势,是上下滑动还是左右滑动,如果是上下滑动,直接中断:


    640?wx_fmt=png


    判断原理:比较X轴点间距和2PX倍的Y轴点间距的大小,前者大于后者,是左右滑动;前者小于后者,是上下滑动。



  2. 判断左右滑动的方向,计算滑动距离。

    先判断当前视图的状态标记,如果是关闭的:

    滑动距离 = 手指滑动距离;

    如果是准备展示左视图:

    滑动距离 = 手指滑动距离 - 待展开视图的宽;

    如果是准备展示右视图:

    滑动距离 = 手指滑动距离 + 待展开视图的宽;

    (为什么有加减区别,参看上方stratScroll参数解释)

  3. 执行滑动操作。


三、MotionEvent.ACTION_UP:


作为点击状态的最后一步,这里我们需要根据当前视图的滑动状态,来做一下补偿操作。即判断当前隐藏的视图是否完全显示出来或隐藏掉。详细判断大家可在代码中看到。这里我考虑到的场景有如下几个:


向左滑动:

  • 场景1、隐藏左侧菜单

  • 场景2、弹出右侧菜单

  • 场景3、隐藏左侧菜单后直接弹出右侧菜单

向右滑动:

  • 场景1、隐藏右侧菜单

  • 场景2、弹出左侧菜单

  • 场景3、隐藏右侧菜单后直接弹出左侧菜单


整个控件的开发思路就是这样了,对于滑动操作,没有什么复杂的,Android已经给你提供了 Scroller 来完成这一系列操作。我们可以按照如下的流程图,快速制作出项目中需要的控件:


640?wx_fmt=png


想要下载本文源码请点击下方的 阅读原文 链接。




如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。


欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

640?wx_fmt=jpeg