View的事件体系

View基础知识

 

什么是View

Android中的控件主要分为容器控件和普通控件,它们都继承View父类,容器控件中可以容纳多个控件(容器控件与普通控件)。这种关系最终形成View树的结构


View的位置参数

View的位置主要由4个顶点来决定,分别是:top,left,right,bottom。其中top是控件左端横坐标,right是控件右端横坐标,top是控件顶部纵坐标,bottom是控件底部纵坐标。可以通过getter方法获得

那么,控件的宽高就为

Width = right – left;Height = bottom – top

从Android3.0开始,View增加了额外的参数:x,y,translationX和tranlationY,这四个参数都是相对于父容器的坐标;x,y表示View的左上角坐标,translationX与translationY是表示View相对于父容器的偏移量(属性动画平移);两者的换算关系:

X = left + translationX;Y = top + translationY;

注意:在View平移过程中,top和left是原始左上角的位置信息,其值并不会改变,改变的是x,y,translationX和tranlationY

 

MotionEvent和TouchSlop

在手机触摸屏幕后产生的一系列事件,典型的事件类型:

ACTION_DOWN, ACTION_MOVE, ACTION_UP

例如:点击事件, DOWN-> UP

滑动事件:DOWN->MOVE->UP

通过MotionEvent对象提供的方法我们可以获取到事件发生的坐标位置:getX/getY和getRowX/getRowY;前者获取的坐标是相对于View自身,后者坐标的是相对于手机屏幕左上角;


TouchSlop是系统所能识别出的被认为是滑动的最小距离,如果用户在屏幕滑动的两点之间距离小于这个常量,那么系统就认为是在进行滑动操作。这是一个常量,与设备有关,不同设备上可能不同

ViewConfiguration.get(this).getScaledTouchSlop()

意义:在处理滑动时可以把该值作为临界值

VelocityTracker、GestureDetector和Scroller

VelocityTracker用于追踪手指在滑动过程中的速度(包括水平与垂直)

// 获取对象

VelocityTracker tracker = VelocityTrakcher.obtain();

// 添加事件

tracker.addMovement(event);

//计算速度

tracker.computeCurrentVelocity(times);

int xVelocity = (int) tracker.getXVelocity();

int yVelocity = (int) tracker.getYVelocity();

// 不用时重置并回收

tracker.clear();

tracker.recycle();

GestureDetector作用主要是进行手势的设定与检测,可辅助检测用户的单击,滑动,双击等行为

Scroller主要用于实现View的弹性滑动


View的滑动

实现View滑动的三种方式:

1.使用ScrollTo/ScrollBy

ScrollBy是基于当前位置的相对滑动

ScrollTo是基于参数的绝对滑动

ScrollBy内部其实也是调用的scrollTo方法

scrollTo(mScrollX + x,mScrollY + y);

mScrollX表示View左边缘到View内容左边缘的距离

mScrollY表示View上边缘到View内容上边缘的距离

注意:两方法改变的是View内容位置无法改变View在布局中的位置

(也就是说无法scroll到其他控件的区域中)

SrcollTo/By是瞬间完成,可以配合Scroller或Handler实现弹性滑动

利用Handler即每次延时发送一个消息调用SrcollTo/By方法

利用Scroller实现弹性滑动原理:

当我们构造一个Scroller对象并调用startScroll方法时,其实Scroller内部什么事也没做(只是保存了传递的参数)

那么View是如何实现滑动呢

调用startScroll后接着调用invalidate方法,该方法会导致View重绘,在重绘时会调用computeScroll方法,该方法由我们覆写调用computeScrollOffset。computeScrollOffset方法会根据插值器类型以及时间的流逝计算当前的scrollX与scrollY,接着我们就可以通过Scroller对象getter方法获取这两个值并调用scrollTo方法;调用scrollTo方法后再次调用invalidate方法触发下一次滑动直至滑动结束

computeScrollOffset的返回值是boolean值,true表示滑动未结束

总结:Scroller本身不能实现View的滑动,需要配合computeScroll方法才能完成弹性滑动效果,通过不断的让View重绘,而每次重绘距离起始时间会有一个时间间隔,通过这个时间间隔获得View当前需要滑动的位置,然后通过scrollTo进行滑动


2.使用动画

动画可以分为:View动画(帧动画,补间动画)与属性动画

两种动画的区别本质在于:后者能够改变控件的位置

原因分析:之前有提到translationX/Y,这两个属性是在android3.0新增的,而属性动画只支持android3.0+,低版本需要使用nineoldandroids库进行兼容。translationX/Y是表示控件相对于父容器的偏移位置(类似margin,两者相互独立)。属性动画即通过这两个值来改变控件位置的,也就是说设置translationX/Y是能够改变控件位置的,但是不会改变控件LayoutParams中的margin属性,改变的是控件本身android:translationX/Y属性。

margin属性会改变控件的顶点坐标(lrtb),而translationX/Y属性是不会改变LayoutParams的,改变的是x与y。magin属性是属于父容器的属性,而translationX/Y是属于控件本身的(理解)。

那么对于属性动画的复位,我们可以直接用view. setTranslationX(0);

可以通过以下几种方式获取控件位置:

view.getLocationInWindow(pos);//控件在其父窗口中的坐标位置

view.getLocationOnScreen(pos);//控件在其整个屏幕上的坐标位置

    view.getLocalVisibleRect();

view.getGlobalVisibleRect();

注:通过View动画 + updateLayoutParams的方式也可实现改变位置

Android3.0以下通过兼容库实现的属性动画本质还是View动画


3.改变布局参数:适合于有交互的View

layoutParams=(RelativeLayout.LayoutParams) v.getLayoutParams();


View的事件分发机制

三个核心方法:

dispatchTouchEvent(MotionEvent event)

onInterceptTouchEvent(MotionEvent event)

onTouchEvent(MotionEvent event)

三者关系(伪代码):

public void diapatchTouchEvent(Motion event){

boolean consume = false;

if(onInterceptTouchEvent(event)){

consume = true;

}else{

if(haveChild)

consume = child. dispatchTouchEvent(event);

}

return consume;

}


事件的传递:事件的捕获过程与事件的冒泡过程

捕获(传递)过程:Activity -> Window -> View(过程中被拦截将终止)

冒泡(处理)过程:View -> Activity(过程中被消费将终止)

当一个点击事件产生后,对于一个ViewGroup来说首先会调用dispatchTouchEvent方法,如果这个ViewGroup的onInteceptTouchEvent方法返回true表示要拦截当前事件,那么该事件会交给这个ViewGroup处理,它的onTouchEvent方法会被调用,如果onInterceptTouchEvent方法返回false表示不拦截事件,这时事件会继续传递给子元素,子元素的dispatchTouchEvent被调用,如此反复直至事件最终被处理

当一个View需要处理事件时,并且它设置了onTouchListener,那么onTouch方法会先被调用,如果onTouch方法返回false会继续调用onTouchEvent方法,否则onTouchEvent方法将不会被调用,也就是说onTouch优先级比onTouchEvent高。我们常见的onClick优先级是最低的,onClick会在onTouchEvent中处理;

在事件传递与处理的过程中,如果某个View的onTouchEvent方法返回false,那么事件会传递给父容器的onTouchEvent;如果最后所有View的onTouchEvent方法均返回false(所有元素都不处理该事件),那么该事件最终会传递给Activity,即Activity的onTouchEvent方法被调用(一层一层向上抛的过程,类似冒泡)


关于事件传递机制的一些总结:

1. 同一个事件序列是指从手指触摸屏幕开始到手指离开屏幕,即从down开始,中间包含n个move,最后以up结束

2. 正常情况下一个事件序列只能够被一个View拦截且消耗。因为一旦某个元素拦截了某事件,那么同一个事件序列的后续事件都会直接交由它处理,因此同一个事件序列的事件不能分别由两个View处理。(特殊手段:通过调用其他View的onTouchEvent方法)

3. 当View决定拦截事件后,那么同一事件序列的后续事件都会由它处理,不会再调用onInterceptTouchEvent询问是否拦截

4.当View一旦开始处理事件(即执行onTouchEvent方法),若它不消耗ACTION_DOWN事件,那么同一事件序列的后续事件都不会再交由它处理,并且将该DOWN事件重新交由父容器处理

5.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent方法不会被调用,并且该View能够持续收到后续事件,最终消失的点击事件由Activity处理

6. ViewGroup默认不拦截任何事件

7. View没有onInterceptTouchEvent方法,不具有拦截功能,一旦事件传递给它,它的onTouchEvent方法会被调用

8. View的onTouchEvent方法默认返回true(除非它是不可点击的,clickable与longClickable均为false)。View的longClickable默认都为false,clickable视情况而定,如Button的clickable默认为true,而TextView的clickable默认为false

注: setOnClickListener时会自动设置clickable为true

setOnLongClickListener时会自动设置longClickable为true

9. View的enable属性不影响onTouchEvent方法的默认返回值

10. OnClick发生的前提的View可点击并且收到DOWN与UP事件

11.通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父容器的事件分发过程(除ACTION_DOWN事件外)

12.在View消耗ACTION_DOWN事件后,父容器仍然可拦截后续事件

注意:上述提及的可拦截事件的View均指的是ViewGroup

源码解析:

Activity#dispatchTouchEvent 事件分发源头

Window# superDispatchTouchEvent事件传递给DecorView(抽象)

- Window可控制顶级View的外观和行为策略

- Window的唯一实现类是PhoneWindow

PhoneWindow # superDispatchTouchEvent事件传递DecorView(实现)

- DecorView是PhoneWindow的内部类,代表顶级View

- DecorView继承自FrameLayout,会调用其dispatchTouchEvent

ViewGroup#dispatchTouchEvent  ViewGroup的事件分发

- ACTION_DOWN事件会导致状态重置

- (actionMasked == MotionEvent.ACTION_DOWN

|| mFirstTouchTarget != null)避免多次执行拦截方法

-遍历子View将事件向下分发

- dispatchTransformedTouchEvent调用子View的事件分发

若找到接收事件的子View会跳出循环并赋值mFirstTouchTarget

-如果ViewGroup没有子元素或者没有任何子元素处理事件将调用View的事件分发super.dispatchTouchEvent(event);


View的滑动冲突

在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。

主要有三个场景:

场景1:内外两层滑动方向不一致

(如:ViewPager嵌套Fragment,Fragment存在ListView,ViewPager 内部已进行了滑动冲突的处理)

场景2:内外两层滑动方向一致

(如:ViewPager与SlideMenu同时存在)

场景3:上述两种场景的嵌套

(如:网易云音乐首页界面)


滑动冲突的处理规则

原理:主要利用事件分发机制

分析:针对场景1情况,我们可以通过某些滑动信息来决定将事件交给谁处理,如:水平滑动距离与垂直滑动距离的比较、水平速度与竖直速度或者通过路径与水平/垂直方向的角度大小;针对场景2,只能根据业务需求或者自定义控件的效果去决定由谁处理事件;而场景3是场景1与场景2的嵌套,我们只需要分别处理好中间层与外层,以及中间层与内层两个滑动冲突即可。

方式:外部拦截法,内部拦截法

外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理该事件则将其拦截(符合事件分发机制)

外部拦截法伪代码如下:

public void boolean onInterceptTouchEvent(MotionEvent event){

    boolean intercepted = false;

    int x = (int) event.getX();

    int y = (int) event.getY();

    switch(event.getAction()){

        case MotionEvent.ACTIO_DOWN:

            intercepted = false;

            break;

        case MotionEvent.ACTION_MOVE:

            if(父容器需要拦截当前事件){

                intercepted = true;

            }else{

                intercepted = false;

            }

            break;

        case MotionEvent.ACTION_UP:

            intercepted = false;

            break;

        default:

            break;

    }

    mLastXIntercept = x;

    mLastYIntercept = y;

    return intercepted;

}

针对不同的滑动冲突,只需修改if条件即可。在DOWN事件时必须返回false,因为一旦返回true,后续事件直接交由父容器处理,那么事件根本无法传递给子元素;在MOVE事件中进行具体判断决定是否拦截事件;UP事件中必须返回false,主要2个原因:1.UP事件本身没有太大意义,其作为事件序列的最后一个事件必定会传递给父容器2.若UP事件返回true那么子元素将处理不了click事件

内部拦截法是指将父容器是否拦截事件交由子元素决定,这种方式与事件分发机制不一致,需配合requestDisallowInterceptTouchEvent

内部拦截法伪代码如下:

// 子元素

public boolean dispatchTouchEvent(MotionEvent event){

    int x = (int) event.getX();

    int y = (int) event.getY();


    switch(event.getAction()){

        case MotionEvent.ACTION_DOWN:

            parent.requestDisallowInterceptTouchEvent(true); // 不允许拦截

            break;

        case MotionEvent.ACTION_MOVE:

            int deltaX = x - mLastX;

            int deltaY = y - mLastY;

            if(父容器需要拦截事件){

                parent.requestDisallowInterceptTouchEvent(false); // 允许拦截

            }

            break;

        case MotionEvent.ACTION_UP:

            break;

        default:

            break;

    }

    mLastX = x;

    mLastY = y;

}

// 父容器

public boolean onInterceptTouch(MotionEvent event){

    int action = event.getAction();

    if( action == MotionEvent.ACTION_DOWN ){

        return false;

    }else{

        return true; // 默认拦截除ACTION_DOWN以外所有事件

    }

}


首先,父容器默认拦截除DOWN事件以外其他事件。父容器具体是否要拦截事件由子元素决定,子元素的dispatchTouchEvent方法中,DOWN事件中默认不允许父容器拦截,MOVE事件中根据具体条件决定父容器是否拦截事件,UP事件无须关注

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

推荐阅读更多精彩内容