国际版ViewDragHelper的点击事件处理

  在上一篇ViewDragHelper的介绍后,已经完成了自定义控件SwipeLayout的滑动,这一篇,我们来处理它的点击事件。之前提到过,它有两个子view,最开始显示的是surfaceLayout,隐藏在右边的是bottomLayout。当你给surfaceLayout设置点击事件时,你会发现,surface确实可以点击,但向左滑动却什么反应都没有,还是在响应点击事件。原因是什么呢,通过对事件分发的认识,可以得出结论:由于给surfaceLayout设置了点击事件,导致这个子view的变成可点击的了,于是你手指的down事件从SwipeLayout向子view分发时,SwipeLayout并没有拦截这个down,而surfaceLayout作为ViewGroup,默认也不拦截,但是surface已经没有子view在给它分发了,它拦不拦截,这个down都得归它处理,由于surface设置了点击事件,所以它能消费了这个down事件。这么一来,SwipeLayout 如果不在拦截方法里做些处理,后面纷至而来的move事件,都会传递surface,那么SwipeLayout的onTouchEvent就怎么都不会调用,肯定也就滑不起来了。那为什么surface没设置点击事件时,swipeLayout可以滑动呢,这是因为surface不设置点击事件,那它就不会消耗down事件的,那么这个down事件会以冒泡的形式,再向swipeLayout传递,而swipeLayout的onTouchEvent始终返回true,所以swipe消耗了down,之后的move便会一直传递给它。

  照这样看来,得down者,得move!但我们的点击事件是必须要设置的,这样就导致,surface一定会先于父view消耗这个down,好,down给surface没关系,但move事件父View表示我要抢过来。要不然因为你的点击导致我都滑不起来,我还有毛用。由于我们把SwipeLayout的拦截事件方法全部交给了viewDragHelper的shouldInterceptTouEvent ()这个方法,那么看看它什么情况下会拦截事件。

 

  一顿switch判断后,最终会判断mDragState是不是等于拖动。那好说,我们就看这个变量在哪里被赋值。经过一番搜索,可以找到以下几个调用关系:

  

-------------------------------------------------------------------------------------------------------------------------------------------------------

------------------------------------------------------------------------------------------------------------------------------------------------------------------

  

--------------------------------------------------------------------------------------------------------------------------------------------------

从下往上看的话,你会发现在viewDragHelper的拦截方法中,当你一步步走完tryCaptureViewForDrag时,mDragState会被赋值等于STATE_DRAGGING。根据前面提过,子view是一定要消耗down的,接下来的move,从外往内传递,而且子view也没有设置不允许父view拦截的标志,那么父view的拦截方法就会一直调用,也就是shouldInterceptTouchEvent会一直调用。上面action =move是一直能进来的,那tryCaptureViewForDrag为啥不走呢,原因很明了了:就是前面的pastSlop一直等于false嘛!从pastSlop 的赋值情况来看,它的结果跟checkTouchSlop有直接的关系:接着看图:

  在上一篇博客中,我们重写了mCallback的几个回调,但是并没有重写getViewHorizontalDragRange这个回调,看来有的回调不重写就会留坑。默认这个方法返回0,所以checkTouSlop也就返回false,pastSlope返回false,后面的方法不执行,move动作始终不被swipeLayout拦截,也就滑不起来。那我重写这个回调,让它返回值大于0,是不是就行了呢,当然可以!当checkHorzontal为true,系统会判断你手指从点击到滑动的这段距离是否超过系统认为的最小距离,如果超过,系统认为你是在滑动,那么后面发送是连锁反应导致swipeLayout拦截下move事和up事件,并在processTouchEvent里处理,这个方法根据不同的action调用callback,例如move会回调水平移动,以及位置改变,up会回调onViewReleased。下面是我重写的getViewHorizontalDragRange这个回调,

@Override
        public int getViewHorizontalDragRange(@NonNull View child) {
            return helper.getTouchSlop();
        }

 

helper.getTouSlop的值就是上图中的mTouchSlop。这个值默认等于8,它还与动画的执行时间有关系,最好别设得太大。到这里,在运行下程序,surface的点击以及swipeLayout的滑动都可以了,但你滑着会发现还有一些问题,当swipelayout处于关闭状态,你向右滑会触发surface的点击事件,为什么呢,难道我右滑时,swipeLayout没有拦截成功这个move吗?当你这么猜想时,你可以在swipeLayout的拦截方法里打个日志,记录下

shouldInterceptTouchEvent的返回结果,你会发现,你的猜想就是对的!看来,有些回调你就算实现了,坑还是会有的。我们在分析下拦截里的move到了干了什么好事:

就上面的4个圈,给我们挖的坑,第一个圈里面,由于我们重写了上面的那个回调,现在它的返回值是true,没问题,第二个圈在前一篇博客里,记不记得我们在这个回调里设置了边界,当开始处于关闭状态,你向右滑,我们做了限制,让surface返回还是0.所以newLeft=0,第3个圈是刚重写的,它的值等于大于0,没问题,出问题的是,我们的newLeft = oldLeft,oldLeft是你点击view的左上角顶点的横坐标,它就是等于0的,所以,在这里直接break了。你看

圈里面提前break,跳出循环,导致tryCaptureViewForDrag 也不会执行,这样move、up事件还是会下发给surface的。这个问题怎么解决呢?其实很简单,你只要强制把返回结果改为true,后面的事交给processTouchEvent去搞定就完了。因为它就是负责将你的手势转为相应的回调去操作子view,拦截只是个不让你进入美丽花园的巨人,童话故事就是这么说的。这是修改后的拦截代码

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = helper.shouldInterceptTouchEvent(ev);
        float curX = ev.getX();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = curX;
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(curX - startX) > helper.getTouchSlop()){
                    result = true;
                }
                break;
        }

        return result;
    }

 

第一行代码可以保证你把系统默认的拦截都走一遍,后面的就是判断用户滑动的距离超过了系统认定的最小滑动距离,我们就拦截下来。就这么简单的处理,你在向右滑,就还是被swipeLayout拦截,子view的点击事件就不会生效,你单纯去点击,当然也不会进入我们的判断,swipeLayout就正常的分发。至此,点击事件的坑就踩到这里了。看到这里,你会发现,废话那么多,才加了10行左右的代码,看别人的博客的话,别人直接告诉你,重写某某回调,点击事件就有了,那当然很省事,但这个探索的过程,我认为是很值得的,因为我经常闲的蛋疼。