描述
这周做了一个自定义侧滑布局, 继承自LinearLayout。
代码地址:android-SwipeLinearLayout
效果
可以单独使用,也可以在ListView等可滑动的父组件中使用。以在ListView中使用为demo:
解决了item和ListView的滑动冲突, 同时每个item及其上面的控件可以正常点击。
代码比较简单,就不上传到JCenter了。 控件本身就只有一个文件: SwipeLinearLayout.java, 有需要可以直接复制或者修改。
使用
和普通LinearLayout一样使用,内部包含2个子元素即可。
示例:
第一个子元素是未侧滑时就显示的部分, 第二个子元素是会被侧滑出来的部分。
SwipeLinearLayout的orientation随便设置,反正都会当成horizontal处理。
实现
如何进行滑动
这个问题思路很简单。滑动分为2个阶段, 一个阶段就是跟手滑动,另外一个阶段,就是当手指离开后,布局继续滑动。
跟手滑动,那么我们很容易就想到重写onTouchEvent方法,在ACTION_MOVE事件中实现。那手指离开之后呢?首先要明确一点,开始处理的判断,是放在ACTION_UP事件中的。我们可以通过此时布局展开的程度,决定布局是要完全展开,还是缩回初始状态。为了让这种自动的滚动显得自然,我们需要借助Scroller。
Scroller可以看作一种类似插值器一样的东西,可以在系统调用的回调中,为我们提供一个起、终值之间的值。随着时间的增长,这个值逐渐从起点值变成终点值。通过这个值随时间的变化,可以帮助我们实现布局的平滑滚动。
处理滑动的代码如下:
可以看到我们就是在computeScroll()方法中,获得插值,进行滚动的。要注意的是,一定要调用invalidate(),computeScroll() 才会被调用。
关于hasJudged和ingnore标志位, 这两个是跟处理滑动冲突相关的。hasJudged标志位表示: 当前手指滑动的方向(水平or竖直)是否已经判断出,ignore表示是否要忽略这次被传到onTouchEvent里的事件。
我们继续往下看。
处理滑动冲突
处理滑动冲突的目的是,保证布局的左右滑动,和它父组件,如ListView等的竖直滑动,不会相互影响。如果仅仅像上文一样,只实现了onTouchEvent, 那么单独使用该布局,倒是没什么问题。但在ListView的item中使用的时候,你会发现,在你想划开子item的时候,很容易就引起了ListView的上下滑动。而且之后的所有事件, 都会被ListView拦截。这就很尴尬了,SwipeLinearLayout刚被划开一点就不动了。而且这种情况出现的非常频繁,滑动冲突必须处理,即:
touch事件被谁处理,必须由我们说了算。
本次处理滑动冲突,我采用的是内部拦截法。即,在子View的dispatchTouchEvent中,先使用父View的requestDisallowInterceptTouchEvent(true),阻止父View对后续事件进行拦截。然后再通过后续条件判断,是否让父View恢复拦截事件的能力。
在本例中,我们通过比较手指在水平方向和竖直方向移动距离的大小,判断是否调用requestDisallowInterceptTouchEvent(false)恢复父View拦截能力。为了判断更合理, 比较放在了手指移动超过一定距离的时候。
hasJudged, 很好理解,表示:当前手指滑动的方向(水平or竖直)是否已经判断出。那ignore呢,又是什么鬼?
是这样的:当我们判断手指其实是竖直方向滑动的时候,会恢复父View(如ListView)的拦截能力,那后续的滑动,其实都只是ListView的上下滑动了。这个,大家应该都能理解。但大家要注意一点,决定滑动方向的,最后一次ACTION_MOVE事件,依然被传到onTouchEvent里去了。这就会造成,虽然结果判定是对ListView进行上下滑动,但我们依然可以看见,相应的item的SwipeLinearLayout被划出来了一点。这就很难看了。于是我增加了一个ignore标志位,来表示,忽略这次的事件。即:用来决定方向的手指滑动,就只是用来决定方向的,而不会对UI产生任何影响。
你也可能发现,这里并没有直接调用parent的requestDisallowInterceptTouchEvent方法,而是调用了自定义的方法disallowParentsInterceptTouchEvent以及allowParentsInterceptTouchEvent。
看一下这两个方法:
用了递归,原因很简单:你想阻止或者恢复拦截的,并不一定是SwipeLinearLayout的直接父组件。举个例子,SwipeLinearLayout可能只是你的item布局的一个子布局, 那它的父布局就不是ListView。我们要阻止ListView,就只能通过递归的方式,向上搜索,然后调用requestDisallowInterceptTouchEvent(false)。
处理点击事件
一个View被设置了OnClickListener,其onClick方法其实是在OnTouchEvent的ACTION_UP中调用的。所以SwipeLinearLayout的子控件,如果想点击事件生效,就必须得到事件。而为了保证SwipeLinearLayout的滑动,SwipeLinearLayout的又必须对事件进行拦截。所以,可以重写SwipeLinearLayout的处理拦截方法如下:
应该很好理解,当判定了滑动方向的时候(其实就是水平方向, 如果是竖直方向的话,直接就被上层拦截了,到不了这里),返回true, 自己消费touch事件,没判定的话,就返回父类,即LinearLayout的onInterceptTouchEvent。LinearLayou的子控件可以点击吗?当然可以。所以这样写就ok了。
看到的效果如示例动图:
拖动左边白色部分的时候,虽然手指一直在上面,也是从上面离开的,但依然不会出发click事件。但直接点击的话,则会弹出Toast,提示点击了item。
ListView中item的联动
这个描述,说的其实就是图片里显示的,竖直滑动ListView,或者滑动其他的item, 已经展开的item会复原。
这个重点其实不在SwipeLinearLayout上了,具体的逻辑是在与ListView对应的Adapter上。
SwipeLinearLayout中提供了这样一个interface:
onDirectionJudged, 在hasJudged被置为true的时候被调用。在上面的代码中也可以看到。
下面看Adapter中是如何实现这个接口的:
swipeLinearLayouts是Adapter中定义的,一个保存ListView中所有item里的SwipeLinearLayout的列表(由于convertView的复用,其实这个列表的长度是很有限的,不用担心内存等问题)。
看了代码, 实现的逻辑就很清楚了:
竖直方向,直接缩起所有SwipeLinearLayout, 否则,把不是当前滑动的SwipeLinearLayout全部缩起来。
总结
如果只是考虑横向滚动,那么问题就非常简单,只需要重写OnTouchEvent,这点大家肯定都会,我也没必要写这篇博客了。然而为了处理滑动冲突(包括保证子View的点击),我们将dispatchTouchEvent和onInterceptTouchEvent也都重写了。一个是保证自己在滑动的时候,事件不会被上层粗暴拦截,另一个是保证自己在不滑动的时候,事件能够传给内部的子控件。
代码只贴了重点部分, 但其实也差不多了,毕竟代码量也不是很大,重点就在于对于事件的分发与拦截。如果需要查看项目及demo完整代码,可以访问:
android-SwipeLinearLayout