基础巩固(四)View体系与事件分发

Android窗口机制

  • PhoneWindow:每个Activity都包含一个Window对象,而PhoneWindow是Window的唯一实现类,是Android中最基本的窗口系统,也是Activity与整个View系统交互的接口。
  • DectorView:顶层视图,本质上是一个FrameLayout,将要显示的具体内部呈现在PhoneWindow上。DectorView是当前Activity所有View的祖先,它并不会向用户呈现任何东西,主要有以下几个功能:
    • Dispatch ViewRoot分发来的key、touch、trackball等外部事件;
    • DecorView有一个直接的子View,我们称之为System Layout,这个View是从系统的Layout.xml中解析出的,它包含当前UI的风格,如是否带title、是否带process bar等。可以称这些属性为Window decorations。
    • 作为PhoneWindow与ViewRoot之间的桥梁,ViewRoot通过DecorView设置窗口属性。可以这样获取 View view = getWindow().getDecorView();
    • DecorView只有一个子元素为LinearLayout。代表整个Window界面,包含通知栏,标题栏,内容显示栏三块区域。DecorView里面TitleView:标题,可以设置requestWindowFeature(Window.FEATURE_NO_TITLE)取消掉ContentView:是一个id为content的FrameLayout。我们平常在Activity使用的setContentView就是设置在这里,也就是在FrameLayout上。

在这里插入图片描述

ViewRoot

DecorView是Android视图树的根节点,那么ViewRoot又是什么呢?

ViewRoot的定义是 连接器,对应于ViewRootImpl类。
它的作用在于:

  • 连接WindowManager和DecorView。
  • 完成View的绘制流程

ViewRoot负责与WMS交互通信,调整窗口大小以及布局。同时向DecorView派发输入事件,完成三大绘制流程measure、layout、draw。

Window、WindowManager、ViewRoot、Activity、DecorView之间的关系

在这里插入图片描述

  • Window是承载器,承载视图View的显示,phonewindow是它的唯一实现类。
  • WindowManager是Window的管理者,WindowManager通过ViewRoot实现对窗口的管理。
  • ViewRoot是WindowManager与DecorView之间的连接器,它是实现类为ViewRootImpl,它负责与WMS(窗口管理服务器)之间交互通信,向DecorView发送输入事件,完成View的绘制流程:measure、layout、draw。
  • Activity是我们最终看到的可与用户交互的界面,每个activity对应一个phonewindow,承载视图,DecorView是视图树的树根,也称顶层视图,它本质是一个framelayout,帧布局,它内部有一个线性布局的viewgroup,包含action_mode_bar(状态栏)、titlebar(标题栏)、content(内容)以及最底部的导航栏
    在这里插入图片描述

在这里插入图片描述

View

View是UI界面的基本构建块,它占据了一块矩形区域,负责绘图和事件处理。View同时也是android上其它UI控件的基类,可以用来创建其它交互式的UI组件(比如Button, TextView, 等等)。View的子类 ViewGroup 则是各自layout布局的基类,ViewGroup是一个不可见的容器,用于容纳其它的View(或其它的ViewGroup),而且还定义了相关的布局属性。
在这里插入图片描述

View的生命周期

在屏幕上渲染的View必须经历这些生命周期方法才能正确地在屏幕上绘制。
在这里插入图片描述

  • 构造函数,有四种:
    • View(Context context):当从代码动态的创建View时就会用到这个简易的构造方法。这里的参数 context 是运行视图的上下文,通过它可以访问到当前的主题(theme), 资源(resources)等东西。
    • View(Context context, @Nullable AttributeSet attrs):从XML布局里加载View时调用的构造方法。当从XML文件里创建View同时也为这个View指定了一些相应的属性时,就会调用此方法。这个版本的构造函数使用的默认样式是0,因此唯一应用的属性值是上下文主题和给定AttrubiteSet中的属性值。
    • View(Context context, @Nullable AttributeSet attrs, int defStyleAttr):从XML文件执行加载并从主题属性(defStyleAttr) 中应用基本样式。参数 defStyleAttr 是当前主题的一个属性,其中包含了对样式资源的引用,这个样式资源为View的属性提供了一个默认值,不查找默认值可以将其设置为0。
    • View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes):从XML文件执行加载,并从主题属性或样式资源中应用特定于类的基本样式。View的这个构造方法允许View在加载时使用它自己的基本样式,与上个构造方法类似。参数 defStyleRes 是样式资源的资源标识符,为View提供默认值,仅当 defStyleAttr 为0或在主题中找不到时使用。如果不查找默认值,则可以为0。

在这里插入图片描述

View的生命周期主要由三部分组成:

  • Attachment / Detachment (关联/分离)
  • Traversals(遍历:Measure、Layout、Draw)
  • State Save / Restore (状态保存/ 恢复)

Attachment / Detachment

  • onAttachedToWindow()
    当View关联到窗口时调用。在这个阶段,View知道它处于活动的状态并且具有可绘制的表面。因此,我们在此阶段就可以开始分配任何资源或设置listeners。
  • onDetachedFromWindow()
    当View与窗口分离时将调用此方法。此时,View不再具有可绘制的表面。在此阶段,你需要停止任何已执行的任务或清理任何已分配的资源。当我们在ViewGroup类上调用 removeView() 或者Activity被销毁时将调用此方法。
  • onFinishInflate():当布局加载器(LayoutInflater)从XML文件里完成了所有子View的加载时,将会调用此方法,在Measure(测量)之前。

Traversals

之所以把此阶段称为“遍历”,是因为View的视图层次就像是从父节点(ViewGroup)到子节点的树状结构。因此,每个方法都是从父节点开始,一直遍历执行到最后一个节点:
在这里插入图片描述

在这里插入图片描述
每个节点的绘制流程又可以分为3个阶段:measure、layout、draw。

在onMeasure方法中View会对其所有的子元素执行measure过程,此时measure过程就从父容器“传递”到了子元素中,接着子元素会递归的对其子元素进行measure过程,如此反复完成对整个View树的遍历

onLayout与onDraw过程的执行流程与此类似。

  • measure:measure过程决定了View的测量宽高,这个过程结束后,就可以通过getMeasuredHeight和getMeasuredWidth获得View的测量宽高了。
  • layout: layout过程决定了View在父容器中的位置和View的最终显示宽高,getTop等方法可获取View的top等四个位置参数(View的左上角顶点的坐标为(left, top), 右下角顶点坐标为(right, bottom))。 getWidth和getHeight可获得View的最终显示宽高(width = right - left;height = bottom - top)。
  • draw:draw过程决定了View最终显示出来的样子,此过程完成后,View才会在屏幕上显示出来。

State Save / Restore

onSaveInstanceState():
当Activity调用了onSaveInstanceState()方法后,便会对它的View Tree进行保存,而进一步对每一个子View调用其onSaveInstanceState()方法来保存状态。onSaveInstanceState()方法返回Parcelable对象,也即是序列化对象,是Android提供的一种序列化方式。

如果是保存自定义view的状态:

  • 首先构建一个类SaveState继承自BaseSavedState
  • 在onSaveInstanceState()中,利用构建的这个类,把当前的位置保存进SavedState
@Override
protected Parcelable onSaveInstanceState() {
    Parcelable parcelable = super.onSaveInstanceState();
    SavedState ss = new SavedState(parcelable);
    //把当前的位置保存进SavedState
    ss.currentPosition = mCurrentPosition;
    return ss;
}

static class SavedState extends BaseSavedState{

    //当前的ViewPager的位置
    int currentPosition;

    public SavedState(Parcel source) {
        super(source);
        currentPosition = source.readInt();
    }

    public SavedState(Parcelable superState) {
        super(superState);
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        super.writeToParcel(out, flags);
        out.writeInt(currentPosition);
    }

    public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>(){

        @Override
        public SavedState createFromParcel(Parcel source) {
            return new SavedState(source);
        }

        @Override
        public SavedState[] newArray(int size) {
            return new SavedState[size];
        }
    };
}

状态恢复:
onRestoreInstanceState(Parcelable)方法内,根据传递进来的Parcelable参数,我们可以拿到我们之前保存的数据,再根据需要进行赋值或者调用某些方法来恢复状态就行了。

@Override
protected void onRestoreInstanceState(Parcelable state) {
    SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    //调用别的方法,把保存的数据重新赋值给当前的自定义View
    mViewPager.setCurrentItem(ss.currentPosition);
}

invalidate()和requestLayout()

如果view的内容发生变化,会调用invalidate(),返回到生命周期中的dispatchToDraw(),重新绘制。

如果view的边界发生变化,则会调用requestLayout(),返回到测量步骤,重新执行测量、布局和绘制。

在这里插入图片描述

View的生命周期与Activity的生命周期的关联

自定义一个类继承View,放到activity的layout中,打印log观察各个函数调用情况:

Activity   onCreate
View       LifeView(Context context, @Nullable AttributeSet attrs)
View       onFinishInflate
Activity   onStart
Activity   onResume
View       onAttachedToWindow
View       onMeasure
View       onSizeChanged
View       onLayout
View       onDraw
View       onWindowFocusChanged  true
View       onMeasure
View       onLayout
View       onDraw
Activity   onPause
View       onWindowFocusChanged  false
Activity   onStop
Activity   onDestroy

可以看出,在activity可见(onResume)后,view与window关联起来。 先测量,决定自身大小,决定在父容器中的位置和大小,然后绘制到屏幕上。根据上面的log,我们也可以看出View的主要绘制流程是measure、layout、draw。

Activity创建时如何关联View

在Activity的onCreate()中会执行setContent()

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

setContentView()指定一个布局文件,表示要在Activity上展示这个布局。 该方法有两个主要作用:

  • 1、将自定义的布局(View)加入到ViewTree里,而ViewTree的根就是DecorView。
  • 2、将Window(PhoneWindow)和DecorView 关联。

也就是说当Activity 处在"Create"状态时,整个ViewTree已经被创建了。

但是需等到onResume()的时候,view才依附到window,onAttachedToWindow,然后开始绘制流程:measure、layout、draw。

常用属性

ID

View可能会有一个与之关联的数字id。通常来说这些是在layout xml文件中分配的id。view的ID并不需要全局独一无二的,而是要在它所属的树里是唯一的。

Android坐标系

Android坐标系的定义为:

  • 屏幕的左上角为坐标原点
  • 向右为x增大方向
  • 向下为y增大方向
    在这里插入图片描述

一个view占据一个方形的位置。view的位置用左上角的点来表示。位置和尺寸的单位是像素(pixel)。
view的位置由四个顶点决定:
在这里插入图片描述
视图的位置相对于父控件而言,四个顶点的位置描述分别由四个与父控件相关的值决定:

  • 顶部(Top):视图上边界到父控件上边界的距离
  • 左边(Left):视图左边界到父控件左边界的距离
  • 右边(Right):视图右边界到父控件左边界的距离
  • 底部(Bottom):视图下边界到父控件上边界的距离
    在这里插入图片描述

Position

调用getLeft()和getTop()可以获得view的坐标。getLeft()返回view的left值,或者说是x值。getTop()返回top值,或者说是y值。这些方法返回的坐标是view在它的父view中的位置。例如,假设getLeft()返回20,表示这个view在它父view左边缘往右20个像素的位置。

另外,getRight()方法能返回view的right值。getBottom()方法返回bottom值。 getRight() == getLeft() + getWidth()

MotionEvent中get()和getRaw()的区别

//get() :触摸点相对于其所在组件坐标系的坐标
 event.getX();       
 event.getY();

//getRaw() :触摸点相对于屏幕默认坐标系的坐标
 event.getRawX();    
 event.getRawY();

在这里插入图片描述

angle和radian

自定义view中常常是将一些简单的形状通过计算,从而组合到一起形成的效果。

这会涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识。

在这里插入图片描述

Size, padding 和 margins

view的尺寸(size)有宽(width)和高(height)。一个view实际上有两对宽高值。

第一对宽高是测量宽高(measured width/height)。这个尺寸==表示一个view想要在父view里要多大。==通过getMeasuredWidth() 和 getMeasuredHeight()方法可以得到测量宽高。

第二对宽高可以理解为实际宽高。这个宽高有可能与测量宽高不同。通过getWidth()和getHeight()可拿到宽高值。

为了测量尺寸,view需要考虑padding值。padding值的单位是像素(px),分为左上右下(left,top,right,bottom)。 padding可用于将view的内容偏移特定数量的像素。 例如,左padding为2像素的时候,会把view的内容从左向右推2个像素。 可以用setPadding(int, int, int, int)或者setPaddingRelative(int, int, int, int)方法设置padding值。 用getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom(), getPaddingStart(), getPaddingEnd()获取对应的padding值。

view可以设定padding值,但没有margin值。ViewGroup能支持margin值。

color

事件分发

  • 事件分发的本质是将点击事件(motionEvent)传递到某个具体的View处理的整个过程。

  • Android的UI界面由Activity、ViewGroup、View及其派生类组成,事件就是在Activity、ViewGroup以及View之间传递。

  • 事件分发的顺序:Activity -> ViewGroup -> View。即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View。

  • 事件分发过程由哪些方法协作完成?

    • dispatchTouchEvent()
    • onTouchEvent()
    • onInterceptTouchEvent()(只存在于ViewGroup中)
      在这里插入图片描述

Activity的事件分发

源码分析

/**
  * 源码分析:Activity.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) {

    // 仅贴出核心代码

    // ->>分析1
    if (getWindow().superDispatchTouchEvent(ev)) {

        return true;
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
        // 否则:继续往下调用Activity.onTouchEvent

    }
    // ->>分析3
    return onTouchEvent(ev);
  }

/**
  * 分析1:getWindow().superDispatchTouchEvent(ev)
  * 说明:
  *     a. getWindow() = 获取Window类的对象
  *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类
  *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
  */
  @Override
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return mDecor.superDispatchTouchEvent(event);
      // mDecor = 顶层View(DecorView)的实例对象
      // ->> 分析2
  }

/**
  * 分析2:mDecor.superDispatchTouchEvent(event)
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return super.dispatchTouchEvent(event);
      // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
      // 即将事件传递到ViewGroup去处理,详细请看后续章节分析的ViewGroup的事件分发机制

  }
  // 回到最初的分析2入口处

/**
  * 分析3:Activity.onTouchEvent()
  * 调用场景:当一个点击事件未被Activity下任何一个View接收/处理时,就会调用该方法
  */
  public boolean onTouchEvent(MotionEvent event) {

        // ->> 分析5
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        
        return false;
        // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
    }

/**
  * 分析4:mWindow.shouldCloseOnTouch(this, event)
  * 作用:主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
  */
  public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

  if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
          && isOutOfBounds(context, event) && peekDecorView() != null) {

        // 返回true:说明事件在边界外,即 消费事件
        return true;
    }

    // 返回false:在边界内,即未消费(默认)
    return false;
  } 

流程描述

当一个点击事件发生时,从Activity的事件分发开始,流程如下:

  1. 用户触碰到屏幕
  2. 执行Activity.dispatchTouchEvent(),分发事件,在这个函数中首先会执行 getWindow().superDispatchTouchEvent(ev),这里getWindow()得到的结果是phonewindow,前面已经介绍过每个activity都对应一个phonewindow,这个phonewindowactivityview体系交互的接口层对象。phonewindow会将点击事件分发到view体系的顶层对象DecorView
  3. DecorView会继续将事件传递给自己的父类分发事件。 DecorView本质上是一个帧布局framelayout,而所有的layout的基类都是ViewGroup,至此实现了事件从Activity分发到ViewGroup
  4. 如果ViewGroup的事件分发结果返回为true(点击事件被viewgroup消费了),则直接结束。否则会执行Activity.onTouchEvent()
  5. Activity.onTouchEvent(),该函数内部会执行shouldCloseOnTouch,判断点击事件是否在Window边界外,如果在边界外,事件算被Activity消费,返回true。否则返回false,不算被消费,但依旧结束事件分发。

流程框图

在这里插入图片描述

在这里插入图片描述

ViewGroup的事件分发

前已述及,Activity将事件分发给ViewGroup后,如果ViewGroup没有消费事件,才会轮到Activity执行onTouchEvent(),处理事件。那么ViewGroup是如何分发事件的呢?

源码分析

/**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) { 

  // 仅贴出关键代码
  ... 

  if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
  // 分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
    // 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
    // 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
        // a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
        // b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
        // c. 关于onInterceptTouchEvent() ->>分析1

  // 分析2
    // 1. 通过for循环,遍历当前ViewGroup下的所有子View
    for (int i = count - 1; i >= 0; i--) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                || child.getAnimation() != null) {  
            child.getHitRect(frame);  

            // 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
            if (frame.contains(scrolledXInt, scrolledYInt)) {  
                final float xc = scrolledXFloat - child.mLeft;  
                final float yc = scrolledYFloat - child.mTop;  
                ev.setLocation(xc, yc);  
                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                // 3. 条件判断的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
                if (child.dispatchTouchEvent(ev))  { 

                // 调用子View的dispatchTouchEvent后是有返回值的
                // 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即该子View把ViewGroup的点击事件消费掉了

                mMotionTarget = child;  
                return true; 
                      }  
                  }  
              }  
          }  
      }  
    }  

  ...

  return super.dispatchTouchEvent(ev);
  // 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
  // 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
  // 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
  // 具体请参考View事件分发机制中的View.dispatchTouchEvent()

  ... 

}

/**
  * 分析1:ViewGroup.onInterceptTouchEvent()
  * 作用:是否拦截事件
  * 说明:
  *     a. 返回false:不拦截(默认)
  *     b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
    // 默认不拦截
    return false;

  } 
  // 回到调用原处

流程描述

  1. 事件从Activity传递到ViewGroup时,首先会决定是否拦截事件。执行onInterceptTouchEvent()。(注:只有viewgroup有拦截事件的权利)
  2. 默认情况是不拦截,如果要实现拦截,需手动重写拦截函数。
  3. 拦截后是不允许事件继续向子View传递的,会调用父类的事件分发函数即View.dispatchTouchEvent(),因为ViewGroup的父类为View。相当于ViewGroup自己消费该事件,调用自身的onTouch() -> onTouchEvent() -> performClick() -> onClick,这个过程与下边要讲的view的事件分发处理是一致的。
  4. 如果允许事件继续向子View传递,遍历所有的子View,寻找被点击的子View,如果找到,则执行该子View的dispatchTouchEvent()。至此,事件由ViewGroup传递到了View
  5. 如果找不到被点击的事件,与被拦截的效果是一致的,会执行ViewGroup父类的事件分发函数View.dispatchTouchEvent().

流程框图

在这里插入图片描述
在这里插入图片描述

View的事件分发

ViewGroup调用父类的dispatchTouchEvent()与找到ViewGroup的被点击的子View,调用该子view的dispatchTouchEvent(),都涉及到View的事件分发处理,从dispatchTouchEvent()函数开始。

源码分析

/**
  * 源码分析:View.dispatchTouchEvent()
  */
  public boolean dispatchTouchEvent(MotionEvent event) {  

       
        if ( (mViewFlags & ENABLED_MASK) == ENABLED && 
              mOnTouchListener != null &&  
              mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 

        return onTouchEvent(event);  
  }
  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
  //   1. (mViewFlags & ENABLED_MASK) == ENABLED
  //   2. mOnTouchListener != null
  //   3. mOnTouchListener.onTouch(this, event)
  // 下面对这3个条件逐个分析

/**
  * 条件1:(mViewFlags & ENABLED_MASK) == ENABLED
  * 说明:
  *    1. 该条件是判断当前点击的控件是否enable
  *    2. 由于很多View默认enable,故该条件恒定为true(除非手动设置为false)
  */

/**
  * 条件2:mOnTouchListener != null
  * 说明:
  *   1. mOnTouchListener变量在View.setOnTouchListener()里赋值
  *   2. 即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空)
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  

} 

/**
  * 条件3:mOnTouchListener.onTouch(this, event)
  * 说明:
  *   1. 即回调控件注册Touch事件时的onTouch();
  *   2. 需手动复写设置,具体如下(以按钮Button为例)
  */
  button.setOnTouchListener(new OnTouchListener() {  
      @Override  
      public boolean onTouch(View v, MotionEvent event) {  
   
        return false;  
        // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
        // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
        // onTouchEvent()源码分析 -> 分析1
      }  
  });

/**
  * 分析1:onTouchEvent()
  */
  public boolean onTouchEvent(MotionEvent event) {  

    ... // 仅展示关键代码

    // 若该控件可点击,则进入switch判断中
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

        // 根据当前事件类型进行判断处理
        switch (event.getAction()) { 

            // a. 事件类型=抬起View(主要分析)
            case MotionEvent.ACTION_UP:  
                    performClick(); 
                    // ->>分析2
                    break;  

            // b. 事件类型=按下View
            case MotionEvent.ACTION_DOWN:  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  

            // c. 事件类型=结束事件
            case MotionEvent.ACTION_CANCEL:  
                refreshDrawableState();  
                removeTapCallback();  
                break;

            // d. 事件类型=滑动View
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  

                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        removeLongPressCallback();  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  

        // 若该控件可点击,就一定返回true
        return true;  
    }  
  // 若该控件不可点击,就一定返回false
  return false;  
}

/**
  * 分析2:performClick()
  */  
  public boolean performClick() {  

      if (mOnClickListener != null) {
          // 只要通过setOnClickListener()为控件View注册1个点击事件
          // 那么就会给mOnClickListener变量赋值(即不为空)
          // 则会往下回调onClick() & performClick()返回true
          playSoundEffect(SoundEffectConstants.CLICK);  
          mOnClickListener.onClick(this);  
          return true;  
      }  
      return false;  
  }  

流程描述

  1. View.dispatchTouchEvent()会首先判断该View是否注册了Touch事件监听,如果注册了监听,直接走监听回调函数onTouch()onTouch()函数处理后,如果返回true,表示事件被消费了,不再向下传递,结束。
  2. 如果onTouch()返回false,或者没有注册Touch事件监听,则由默认的onTouchEvent()处理事件,在点击抬起时,执行performClick()
  3. perform()函数,检测是否注册了点击事件,如果注册了执行事件的回调onClick(),如果没有注册,直接结束。

流程框图

在这里插入图片描述
在这里插入图片描述

事件分发总结

  1. 事件分发从Activity到ViewGroup再到View
  2. Activity事件分发时,先获取到window对象,执行它的事件分发,window会将事件传递给DecorWindow(视图树根),DecorWindow执行它父类的事件分发,DecorView本质上是一个FrameLayout,而所有的Layout布局的基类都是ViewGroup,至此事件分发由Activity传递到ViewGroup.
  3. 如果ViewGroup没有消费事件,则执行Activity自身的事件处理函数onTouchEvent()
  4. ViewGroup对事件的分发时,首先判断是否拦截。默认是不拦截的。如果不拦截,则遍历ViewGroup的每个子View,找到被点击的那个子View,执行该子View的事件分发函数,至此事件由ViewGroup传递到了View。如果没有找到被点击的子view,即点到空白处,那么与被拦截的结果一样,ViewGrop自己消费事件。
  5. View对事件的分发,首先判断是否注册Touch监听,如果有,直接调用监听回调onTouch(),监听回调如果返回true,表示事件被消费,直接结束。如果返回false,则事件未被消费,与没有注册Touch监听的效果一致,由View自身的onTouchEvent()消费事件。在抬起时, onTouchEvent()会执行performClick(),判断是否注册Click监听,如果有,执行onClick(),如果没有直接结束。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值