自定义侧滑删除带你详解事件分发一二

自定义侧滑删除带你详解事件分发一二

android中的事件分发算是我第一次详细的了解android的某种机制,那时我是一直查博客,打印日志,来详细的分析了每个事件分发的流程,也得益于当时学的比较认真,所以我现在的记忆还是很清晰。记得当时为什么会学事件分发,就是因为我们当时的一个项目里面,商品详情页面是模仿京东的,我是从那个控件开始详细了解android事件分发的。今天就通过一个非常简单的自定义侧滑删除控件来了解事件分发的机制。

先上效果:


event_1.gif

侧滑删除大家都不陌生,实现的方式也是多种多样,结合今天的主题,我们就以一个自定义的LinearLayout来实现侧滑删除。如果要自定义滑动相关的自定义控件,就一定要对View的坐标关系有比较清晰的了解,比如调用了scrollTo(),scrollBy()后变化的是scrollX和scrollY,而left,right,top,bottom,x,y等坐标并没有改变,因为根据官方文档知道,scrollTo()和scrollBy()移动的是View的内容,而并没有影响到View相对于父View的左上角的相对关系。还有MotionEvent中的getRawX(),getRawY(),这些是相对整个屏幕左上角的坐标,至于该使用相对父View的还是使用相对的整个屏幕的左上角的就要视情况而定。只要用的一套对应的就行。

我们想象一下,这个自定义View可以让它继承LinearLayout,相比于直接继承View这样我们就能省去很多事,省去Measure和Layout的步骤。这个时候我们就能专注于处理滑动和事件分发。学习View的事件分发一般都是用来处理滑动冲突的,并且一般都会结合Scroller和VelocityTracker。侧滑删除控件主要就是侧滑吗,我们想办法把这个LinearLayout滑动起来就好了,这样就把原先看不到View显示出来了。好了基本的思路就是这样,很简单。那么下面就来看一下事件分发吧。

android中事件分发我们一般只考虑View和ViewGroup就可以了,先说一下大概的流程,首先事件分发一定会先传到Activity,然后这个Activity会持有一个Window,这时就会把这个事件传递给相应的Window,这个Window里面有一个DecorView,这个时候这个事件就传递到了View层。一个事件传递到ViewGroup首先会进入DispatchEvent方法,这个时候首先会有一个标志位,这个标志位就是这个DispatchEvent的返回值,用来告诉上一层的View是否处理这个MotionEvent。首先这个标志位会先置为false,然后问这个ViewGroup是否需要拦截这个MotionEvent,这个时候就是进入了onInterceptEvent(),这个函数只有在ViewGroup内会被调用,这个函数的返回值就是告诉这个ViewGroup是否把这个事件向下传,自己不处理。还是选择自己处理不再向下传。这个函数返回false的话,就表示不拦截这个事件,会把这个事件向下传递。如果返回的是true,这个时候这个ViewGroup就会变成了View,表示它自己会处理这个事件。不再向下传递。当一个事件传递到View的时候,首先会进入dispatchEvent()方法,然后会进入onTouchEvent()方法,在onTouchEvenet里面会调用onTouch和onClick方法,前提是你设置了这两个方法。

好了,有了上面的基础我们就开始动手吧,侧滑删除吗,我就让这个LinearLayout滑动就行了。首先会判断在什么时候我需要拦截这个事件,应该就是当用户是在左右滑动的时候,我们会认为用户是想使用侧滑删除的功能,那么这时候应该拦截这个事件,也就是让我们的LinearLayout来处理。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    boolean intercepted = false;
    int x = (int) ev.getX();
    if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
        return true;
    }
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            mLastX = (int) ev.getX();
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (Math.abs(x - mLastX) > mTouchSlop) intercepted = true;
            else intercepted = false;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            intercepted = false;

            break;
    }
    mLastX = x;
    return intercepted;
}

mTouchSlop:是系统认定是滑动的最小值。
mScroller:是用来处理滑动的,用这个处理滑动会有一个动画的效果,没那么突然,虽然它的内部实现不是动画。
    可以看到在这个方法里面只有当用户在X轴方向的滑动分距离大于系统认为的最小滑动的 时候才会拦截事件。事件拦截写来以后就会交给onTouchEvent处理。那么这时我们就会在这个函数处理相关的滑动。



@Override
public boolean onTouchEvent(MotionEvent event) {

    getParent().requestDisallowInterceptTouchEvent(false);
    if (!mScroller.isFinished()) {
        mScroller.abortAnimation();
        return true;
    }
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastX = (int) event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            int dx = (int) (event.getX() - mLastX);
            mLastX = (int) event.getX();
            scrollBy(-dx, 0);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            int scrollX = getScrollX();
            if (scrollX > MAXWIDTH / 3) {
                mScroller.startScroll(scrollX, 0, MAXWIDTH - scrollX, 0, 200);
            } else {
                mScroller.startScroll(scrollX, 0, -scrollX, 0, 200);
            }
            invalidate();
            break;
    }
    mLastX = (int) event.getX();
    return true;
}

    mScroller.startScroll(scrollX, 0, -scrollX, 0, 2000);这个函数表示
的意思就是从(scrollX,0)点滑动到(scrollX + (-scrollX),0 + 0)点,并且
耗费的时间是200ms,如果使用Scroller来处理滑动就一定需要重写
computeScroll(),并且一定别忘记了postInvalidate(),因为Scroller只是负
责计算在相应的时间这个x应该变化到什么值。它只是这个功能,所以我们拿到这个值后需要自己去scrollTo,并且需要调用 postInvalidate();这样他才会继续调用
computeScroll()来完成滑动,这样不停的重新绘制,显示出来的就是动画效果。


 @Override
public void computeScroll() {
    super.computeScroll();
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

上面就算侧滑删除的功能实现了,可以看出非常简单,整个文件就一百多行代码,项目非常简单,但是也能体现我们对事件分发各个环节的掌握,把对的代码写在对的地方。

下面总结一下事件分发的知识点

1.同一个事件序列是指从手指接触屏幕的那一刻起到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以DOWN事件开始,中间含有数量不定的MOVE事件,最终以UP事件结束。

2.正常情况下,一个事件序列只能被一个View拦截且消耗,因为一个元素拦截了某次事件那么同一个事件序列内的所有事件都会直接交给它处理。当然不包括父View从中间拦截。

3.某个View决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用,这个很好理解,就是说,当一个View决定拦截一个事件后,那么系统就会把同一个事件序列内的其他方法都直接交给他来处理。因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。

4.某个View一旦开始处理事件,如果它不消耗DOWN事件,(onTouchEvent返回了fasle),那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理。即父元素的onTouchEvent会被调用。

5.如果View不消耗除DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件追踪这些消失的点击事件会传递给Activity处理。

6.ViewGroup默认不拦截任何事件,android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。

7.View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

8.View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。

9.View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态的,只要它的clickable或者longclickable又一个为true,那么它的onTouchEvent就返回true。

10.onClick会发生的前提是当前View是可点击的,并且他收到了down和up的事件。

11.事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发子View,通过requestDisAllowInterceptTouchEvent方法可以再子元素中干预父元素的事件分发过程。当时DOWN事件除外。

12.CANCEL事件,官方文档讲的是当前手势被释放,你将不会接收到其他事件,应该像UP一样对待它,那到底什么情况会触发这个事件呢?当前控件(子控件)收到前驱事件(DOWN或者MOVE)后,它的父控件突然插手拦截了事件的传递,这时当前控件就会收到CANCEL,收到此事件后,不管子控件此时返回true或者false,都认为这一个动作已经完成,不会再回传到父控件的onTouchEvent中处理,同时后续事件会通过dispatchEvent方法直接传递到父控件这里来处理。

13.父控件不拦截DOWN事件,如果子控件的onTouchEvent返回了true,在MOVE时父控件拦截,此时CANCEL会传递到子控件,并且子控件只会收到这一个CANCEL事件,后续的事件序列都会进入到父控件的onTouchEvent,且不再走父控件的onInterceptTouchEvent方法,因为此时父控件相当于View,mTarget为空时直接进入onTouchEvent方法,所以此时在onTouchEvent可以监听到后续的事件是因为没有控件会处理这个事件。如果父控件的onTouchEvent返回false,此时会上传到父控件的父控件,最终Activity会处理。如果父控件的onTouchEvent返回true表明后续事件会由这个父控件来处理。

14.最后一点时触摸区域的问题,如果触摸区域在子控件内,同时父控件没有拦截事件传递,则不管子控件是否拦截此事件都会传递到子控件的onTouchEvent中处理,可以看成一种责任吧,因为我点的就是你,你父亲没有拦截说明它不想处理了,那就会传递到你这里。

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

推荐阅读更多精彩内容