安卓艺术开发探索学习笔记之View的事件体系和工作原理

View是Android中所有控件的基类。View是一种界面层的控件的一种抽象,它代表了一个控件。还有ViewGroup,被翻译为控件组,也就是说它内部可以包含许多个控件,即一组View。在Android的设计中,ViewGroup也是继承了View,这就意味View本身就可以是单个控件也可以是由多个控件组成的一组控件。

View的位置参数

View的位置主要由它的四个顶点来决定,分别对应于View的四个属相:top,left,right,bottom。其中top是左上角纵坐标,left是左上角横坐标,right是右上角横坐标,bottom是右上角纵坐标。这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标。
这里写图片描述
获取方式:
Left = getLeft();
Right= getRight();
Top = getTop();
Bottom = getBottom();
从Android3.0开始,View增加了额外的几个参数:x,y,translationX,translationY。
x,y是View左上角的坐标,translationX,translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值是0。
x = left+translationX;
y = top+translationY;

注意,在View平移的过程中,top和left表示的是原始左上角的位置信息,其值不会发生改变,此时发生改变的是x,y,translationX和translationY。

MotionEvent

在手指解除屏幕后所产生的的一系列事件汇总,典型的事件类型有如下几种:

  • ACTION_DOWN:手指刚接触屏幕
  • ACTION_MOVE:手指在屏幕上移动
  • ACTION_UP:手指从屏幕上松开的一瞬间
    同时通过MotionEvent对象我们可以得到点击事件发送的x和y坐标。为此,系统提供了两组方法:getX/Y和getRawX/Y。
    getX/Y返回的是相对于当前View左上角的x和y。getRawX/Y返回的是相对于手机屏幕左上角的x和y坐标。

TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离。也就是说当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为用户是在进行滑动操作。可以通过ViewConfiguration.get(getContext())。getScaleTouchSlop()来得到这个常量。

VelocityTracker,GestureDetector和Scroller

  1. VelocityTracker:
    速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
    首先在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

接着,当我们先知道当前的滑动速度时,这个时候可以采用下面的方法来获取当前的速度

velocityTracker.ComputeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();

在获取速度之前必须先计算速度,即必须先调用computeCurrentVelocity方法。这里的速度是指一段时间内手指所滑过的像素数,比如将时间间隔设为1000ms时,在1s内,手指在水平方法从左到右滑过100像素,那么水平速度就是100。当手指从右往左的时候,水平方法就是负数。

速度 = (终点位置-起点位置)/时间段
最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存

velocityTracker.clear();
velocityTracker.recycle();
  1. GestureDetector:
    手势检测,用于辅助检测用户的单击,滑动,长按和双击等行为。
    首先创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListener从而你能够监听双击行为
GestureDetector mGestrueDetector = new GestrueDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongPressEable(false);

接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现:

boolean consume = mGestureDetector.onTouchEvent(event);
return consum;

然后我们就可以选择地实现OnGestrueListener和OnDoubleTapListener中的方法了:

方法名 描述 所属接口
onDown 手指轻轻触摸屏幕的一瞬间,由一个ACTION_DWON触发 OnGestureListener
oShowPress 手指轻轻触摸屏幕,尚未松开或拖动,由一个ACTION_DOWN触发。注意,它强调的是没有松开或者拖动的状态 OnGestureListener
onSingleTapUp 手机(轻轻触摸屏幕后)松开,伴随着一个MotionEvent ACTION_UP而触发,这是单击行为 OnGestureListener
onLongPress 用户长久的按着屏幕不放,即长按 OnGestureListener
onFling 用户按下触摸屏,快速滑动后松开,由一个ACTION_DOWN多个ACTION_MOVE和一个ACTION_UP触发,这是快速滑动行为。 OnGestureListener
onDoubleTap 双击,由2次连续的单击组成,它不可能和onSingleTapConfirmed共存 OnDoubleTapListener
oSingleTapConfirmed 严格的单击行为。如果触发了它,那么后面不可能再紧跟着另一个单击行为,即这只可能是单击,而不可能是双击中的一个单击 OnDoubleTapListener
onDoubleTapEvent 表示发生了双击行为,在双击的期间,ACTION_DOWN,ACTION_MOVE和ACTUON_UP都会触发此回调。 onDoubleTapListener

3. Scroller:
弹性滑动对象,英语实现View的弹性滑动。当使用View的scrollerTo/scrollerBy方法来进行滑动的时候,其过程是瞬间完成的,这个没有过渡效果的滑动,用户体验不好。这个时候就可以使用Scroller来实现由过渡效果的滑动。Scroller本身无法View弹性滑动,它需要和View的computeScrooll方法配合使用才能共同完成这个功能。

Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY){
    int scrollX = getScollX();
    int delta = destX - scrollX;
    //1000ms内滑向dest,效果就是慢慢滑动
    mScroller.startScroll(scrollX,0,delta,01000);
    invalidate();
}

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

View的滑动

  1. 使用scrollTo/scrollBy
    先看源码:
/**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }


/**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

从源码上可以看到,scrollBy实际上是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo实现了基于所传递参数的绝对滑动。
在源码中,可以看到有两个变量,mScrollX和mScrollY。
在滑动过程中mSrcollX的值总是等于View左边缘的View内容左边缘在水平方向的距离。mScrollY的值总是等于View上边缘和VIew内容上边缘在竖直方向的距离。View的边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中内容的边缘,scrollTo和scrollBy只能改变View的内容的位置而不能改变View在布局中的位置。
mScrollX和mScrollY的单位为像素,从左向右滑动,mScrollX为负值,从上往下滑动,mScrollY为负值。

2.使用动画:
View动画并不能真正改变View的位置,所以这样会带来一个很严重的问题,就是当View发生移动的时候,在新位置无法触发这个View的单击事件,在原位置可以。
用属性动画可以解决这个问题

3.改变布局参数:
改变布局参数,即改变LayoutParams。

MarginLayoutParams params = (MarginLayoutParams)mButton.getlayoutParams();
params.width+=100;
params.leftMargin += 100;
mButton.requestLayout();
//或者mButton.setLayoutParams(params);
  • scrollTo/scrollBy:操作简单,适合对View内容的滑动
  • 动画:操作简单,主要使用与没有交互的View和实现复杂的动画效果
  • 改变布局参数,操作稍微复杂,适用于有交互的View

一个全屏幕滑动的例子:

int mLastX=0;
    int mLastY=0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x-mLastX;
                int deltaY = y-mLastY;
               int TranslationX =(int) mTextView.getTranslationX()+deltaX;
               int TranslationY = (int)mTextView.getTranslationY()+deltaY;
               mTextView.setTranslationX(TranslationX);
               mTextView.setTranslationY(TranslationY);
               break;

            case MotionEvent.ACTION_UP:
                break;

        }
        mLastX = x;
        mLastY = y;

        return true;
    }
}

弹性滑动

  1. 使用Scroller
 Scroller scroller = new Scroller(mContext);


    private void smoothScrollTo(int destX,int destY){
        int scrollX = getScrollX();
        int deltaX = destX-scrollX;
        scroller.startScroll(scrollX,0,deltaX,0,1000);
        invalidate();
    }
 @Override
    public void computeScroll() {
        if(mSrcoller.computeScrollOffset()){
            scrollTo(mSrcoller.getCurrX(),mSrcoller.getCurrY());
            postInvalidate();
        }
    }

这是Scroller的典型使用方法。
当我们调用它的startScroll方法的时候,其实只是保存了我们传递的几个参数。
当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY。然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法来进行第二次重绘,这一次重绘的过程和第一次一样,还是会导致computeScroll方法调用,然后继续向Scroller获取当前的scrollX和scrollY,并通过scrolTo方法滑动到新的位置,如此反复,知道整个滑动过程结束。
在computeScroll方法中有用到computeScrollOffset方法,这个方法会根据时间的流逝来计算当前的scrollX和scrollY的值,它返回true表示滑动还未结束,false则表示滑动已经结束。

2.通过动画:
3.使用延时策略
通过发送一系列延时消息从而达到一种渐进式的效果。具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。

View的事件分发机制

所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个Motionevent产生以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。
点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent,onInterceptTouch和onTouchEvent

  • dispatchTouchEvent:
    用来进行事件的分发,如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件
  • onInterceptTouchEvent
    在上诉方法内部调用,用来判断是否拦截某个事件,如果当前View拦截某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
  • onTouchEvent:
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列总,当前View无法再次接收到事件。

大致的传递规则:
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回ture就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用。如果这个VIewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着资源色的dispatchTouchEvent方法就会被调用,如此返回直到事件被最终处理

当一个View需要处理事件时,如果它设置了OnTouchListener,阿么OnTouchListener中的onTouch方法就会被回调,这时事件如何处理还要看onTouch的返回值,如果发挥false,则当前View的onTouchEvent方法就会被调用,如果返回true,那么onTouchEvent方法将不会被调用。因此,给VIew设置的OnTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法汇总,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。因此,OnClickListener其优先级最低,即处于时间传递的尾端。

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View。

  1. 同一个事件序列是指手指解除屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗,因为一旦一个元素拦截了某个事件,那么同一个事件序列内的所有事件都直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
  3. 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会被调用。因为当一个View决定拦截一个事件后,那么系统会把同一个事件序内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当且View而已持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理
  6. ViewGroup默认不拦截任何事件
  7. View没有onInterceptTouchEvent犯法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
  8. View的onTouchEvent默认会消耗事件。除非它是不可点击的。View的longClickable属性默认为false,clickable属性要分情况
  9. View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true
  10. onClick会发生的前提是当前的View是可点击的,并且它收到了down和up的事件
  11. 事件传递过程是外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以再子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外,因为ACTION_DOWN会重置FLAG_DISALLOW_INTERCEPT这个标志位,导致子View中设置这个标志位无效。因此在面对ACTION_DOWN事件的时候,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。

具体分析可以看源码


View的滑动冲突

三种常见的滑动冲突:
这里写图片描述

场景1处理规则:
当用户左右滑动的时候,需要让外部的VIew拦截点击事件,当用户上下滑动的时候,需要让内部View拦截点击事件。
可以根据滑动过程中两个点之间的坐标来得出是水平滑动还是竖直滑动。

场景2处理规则:
根据具体的业务要求来进行相应的处理

场景3处理规则:
也是根据具体的业务要求来进行相应的处理

1,外部拦截法:
伪代码

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int)ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_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;
    }

ACTION_DOWN这个事件,父容器必须返回false,因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会交给父容器处理。

2,内部拦截法
伪代码

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int)ev.getY();
        switch (ev.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;
        return super.dispatchTouchEvent(event);
    }

在父容器中,一定不能拦截ACTION_DOWN,记得修改

初始ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure,layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中放置位置,draw则负责将View绘制在屏幕上。

这里写图片描述

performTraversals会一次调用performMeasure,performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure,layout和draw这三大流程,其中在performMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到了子元素中,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。其他同理。

measure过程决定了View的宽/高,Measure完成以后,可以通过getMeasureWidth和getMeasurehHight方法来获取View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高。
Layout过程决定了VIew的四个顶点的坐标个实际View的宽./高,完成以后需,可以通过getTop。getBottom,getLeftt,getRight拿到View的四个顶点的位置,并可以通过getWidth和getheight方法来拿到View的最终宽/高
Draw过程决定了View的显示,,只有draw方法完成以后View的内容才能呈现在屏幕上。

这里写图片描述
如图所示,DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(和Activity主题有关)。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏中,内容栏的id是content。DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才能传递给我们的View。

理解MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode是测量模式,SpecSize是指在某种测量模式下的规格大小。

SpecMode有三类:

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式
  • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么要看不同View的具体实现。它对应于LayoutParams中的wrap_content

注意:LayoutParams需要和度容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。对个顶级View和普通的View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的layoutParams来共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同确定。

LayoutParams中的宽/高的参数来划分:

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
  • LayoutParams._WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
  • 固定大小:精确模式,大小为LayoutParams中指定的大小

当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。
当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
当View的宽高是warp_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且其大小不会超过父容器的剩余空间。

View的工作流程

View的工作流程主要是只measure,layout,draw这三大流程,measure确定View的测量宽/高,layout确定View的最终宽高和四个顶点的位置,draw将View绘制到屏幕上。

layout方法的大致流程:
首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop和mBottom这四个值,View的四个顶点一旦确定,那么View 在父容器中的位置也就确定了,接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位值,和onMeasure类似,onLayour的具体实现同样和具体的布局有关,所以View和ViewGroup没有真正实现onLayout方法。

Draw过程:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

猜你喜欢

转载自blog.csdn.net/qq_36391075/article/details/79106845