美文网首页
View的事件体系

View的事件体系

作者: 小甜李子 | 来源:发表于2018-03-26 11:59 被阅读0次

    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事件无须关注

    相关文章

      网友评论

          本文标题:View的事件体系

          本文链接:https://www.haomeiwen.com/subject/srtrcftx.html