Android 事件分发实例之可拖动的ViewGroup

前言

最近项目中有一个视频小窗口,为什么不是悬浮窗,是因为在固定的父窗口可以随意拖动,可以点击进入房间或者点击关闭按钮关闭小窗口。效果图如下:


视频小窗口.png

看到上方的效果图,大概也知道需要自定义ViewGroup,既需要处理ViewGroup的移动事件又同时兼容子类的点击事件。其实之前也写过一篇关于事件分发的文章,但是觉得网上太多这类文章了就没有发表,网上有大把关于事件分发的文章,我刚开始参加工作那会,由于感觉事件分发一直模糊不清,就去网上看到各种文章解析,看的时候觉得还好,就跟着文章看一下源码,顺着流程走一遍,但是发现遇到实际情况还是感觉有心无力。但是现在再看网上关于事件分发的文章,我发现了一些问题,有部分人对于事件分发了解并不是很清晰,还有一部分是只对分发、拦截、回传、反拦截等源码说一遍,但是事件下面细分的ACTION_DOWN、ACTION_MOVE、ACTION_UP并不是很清楚,不过最多的都是只是说理论,可能正在遇到实际问题还会感觉束手无策。接下来几篇文章我会根据实际项目遇到的例子来说明事件分发。

功能实现

首先要分析整个功能,需要自定义ViewGroup继承现有的各种ViewGroup,剩下的就是关于自定义VieGroup功能点的实现,首先滑动事件分配给ViewGroup,让之在父窗口之内随意滑动,其次是ViewGroup中的其他子控件的点击事件要分发下去,讲到这里大家也明白了,肯定涉及到事件分发冲突问题,需要判断当前事件是滑动还是点击事件,如果是滑动需要拦截,如果是点击分发给子类就行。

分析功能点

  • 整个ViewGroup可以在父窗口随意滑动
  • 点击整个条目又可以跳转
  • 点击关闭按钮可以关闭当前的ViewGroup

判断是否是滑动

判断是否是滑动分为两部分,一是系统能检测到的最小滑动距离(常量为8dp),系统提供一个API,所以我们可以直接利用此API来判断是否是滑动,。二是滑动过程要拦截ACTION_MOVE,响应自己的onTouchEvent方法处理ViewGroup的滑动。

//获取系统检测最小滑动距离
ViewConfiguration.get(mContext).getScaledTouchSlop(); 

//判断是否是滑动
if (Math.abs(dx) > minTouchSlop || Math.abs(dy) > minTouchSlop) {
                    interceptd = true;
                } else {
                    interceptd = false;
                }

拦截事件

先看拦截事件的处理,我下面分析,拦截事件处理如下:

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean interceptd = false;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                interceptd = false;
                break;

            case MotionEvent.ACTION_MOVE:
                //计算移动距离 判定是否滑动
                float dx = event.getX() - mDownX;
                float dy = event.getY() - mDownY;

                if (Math.abs(dx) > minTouchSlop || Math.abs(dy) > minTouchSlop) {
                    interceptd = true;
                } else {
                    interceptd = false;
                }

                break;

            case MotionEvent.ACTION_UP:
                interceptd = false;
                break;
        }

        return interceptd;
    }

关于拦截事件的分析:
首先,只有判定为滑动的时候才需要拦截事件,由于拦截事件是在分发事件里面调用的,并且由于dispatchTouchEvent内部及其复杂,所以处理滑动冲突,只需要处理onInterceptTouchEvent和onTouchEvent即可,除了特别情况,一般情况无需dispatchTouchEvent事件。拦截事件onInterceptTouchEvent只要返回true就会把事件拦截掉,这样ACTION_DOWN、ACTION_UP直接返回false就行,ACTION_MOVE事件的时候,如果X、Y轴其中之一的移动距离大于系统能检测的最小滑动距离就判定为滑动。

注意事项:
1、系统对于一个事件里面的三个子事件的优先级不同,ACTION_DOWN优先级是最高的。如果拦截事件里面拦截了ACTION_DOWN事件,其他的事件都会被拦截
2、requestDisallowInterceptTouchEvent(true)会告诉父类不要拦截事件,除了ACTION_DOWN事件以外,因为ACTION_DOWN在分发的过程中,会重置requestDisallowInterceptTouchEvent方法的标志位,因此如果父类设置了拦截ACTION_DOWN,即使子类调用requestDisallowInterceptTouchEvent(true)强制请求事件也是无法响应的。

消费事件

拦截事件已经处理了在滑动的时候拦截,所以在onTouchEvent事件里就可以直接处理滑动事件了

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:

                if (mDownX >= 0
                        && mDownY >= mRootTopY
                        && mDownX <= mRootMeasuredWidth
                        && mDownY <= (mRootMeasuredHeight + mRootTopY)) {

                    float dx = event.getX() - mDownX;
                    float dy = event.getY() - mDownY;

                    float ownX = getX();
                    //获取手指按下的距离与控件本身Y轴的距离
                    float ownY = getY();
                    //理论中X轴拖动的距离
                    float endX = ownX + dx;
                    //理论中Y轴拖动的距离
                    float endY = ownY + dy;
                    //X轴可以拖动的最大距离
                    float maxX = mRootMeasuredWidth - getWidth();
                    //Y轴可以拖动的最大距离
                    float maxY = mRootMeasuredHeight - getHeight();
                    //X轴边界限制
                    endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
                    //Y轴边界限制
                    endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;
                    //开始移动
                    setX(endX);
                    setY(endY);
                }

                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        return true;
    }

关于消费滑动事件的分析:
首先要判断需要在父窗口内随意滑动,所以需要下面这一判断条件,具体父窗口的位置在接下来会说明。

if (mDownX >= 0
                        && mDownY >= mRootTopY
                        && mDownX <= mRootMeasuredWidth
                        && mDownY <= (mRootMeasuredHeight + mRootTopY))

接着要判断最后的位置不能超过父类所在的区域

//X轴边界限制
                    endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
                    //Y轴边界限制
                    endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;

最后通过使用setX(endX)来设置位置,为什么不使用其他几种方式设置位置呢?,可能有人使用过layout(int l, int t, int r, int b)这个方法来移动View,那肯定会遇到翻页或者刷新,原本移动的View又恢复到原来位置了,那是因为只要父类调用requestLayout();方法,子类就会重新布局,这涉及到关于view的测量、布局、绘制过程了,还有一种方式是通过属性动画也可以改变位置,属性动画通过反射属性改变view的真实值。
肯定还有疑问就是为什么onTouchEvent方法直接返回true,而不像onInterceptTouchEvent需要各个方法判断返回值的方式,因为滑动事件的处理要交给父类处理,需要返回true才能去消费事件而不至于分发给子类消费。

注意事项:
1、切记要使用event.getX()来或者坐标,相对于父窗口的位置,而不是使用event.getRawX(),event.getRawX()是相对于整个屏幕坐标
2、在ACTION_UP事件里也可以添加贴边动画,增强体验

确定父窗口

在onInterceptTouchEvent方法和dispatchTouchEvent方法里面都可以获取到父窗口的大小,dispatchTouchEvent是必定要走的方法,所以放在onInterceptTouchEvent方法中有利于了解事件的拦截。下面是onInterceptTouchEvent方法中的ACTION_DOWN事件

case MotionEvent.ACTION_DOWN:
                interceptd = false;
                //测量按下位置
                mDownX = event.getX();
                mDownY = event.getY();
                //测量父类的位置和宽高
                if (!mHasMeasuredParent) {
                    ViewGroup mViewGroup = (ViewGroup) getParent();
                    if (mViewGroup != null) {
                        //获取父布局的高度
                        mRootMeasuredHeight = mViewGroup.getMeasuredHeight();
                        mRootMeasuredWidth = mViewGroup.getMeasuredWidth();
                        int top = mViewGroup.getTop();
                        //获取父布局顶点的坐标
                        mRootTopY = mViewGroup.getTop();;
                        mHasMeasuredParent = true;
                    }
                }

                break;

说明:mHasMeasuredParent参数为了防止每次都去获取,但是如果你的父窗口也是动态改变的,去掉此判定条件

写在最后

至此,关于在父窗口可以随意拖动的ViewGroup功能已经完成,通过实际的例子也发现了,事件分发并不简单的关于一个方法的处理,有时候会涉及到好几个方法结合起来使用。网上的文章虽然多,但是大部分都是讲述事件主干线,对于单个事件分析不透彻,当然如果本人中结论如果有误,请及时下方评论纠正,在此感谢。由于接下来我会根据实际项目事件分发一系列的文章,所以准备把demo上传到GitHub上,本文的demo如下。

随意拖动的ViewGroup

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

推荐阅读更多精彩内容