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

news2024/12/28 21:28:54

文章目录

  • Android窗口机制
  • ViewRoot
  • Window、WindowManager、ViewRoot、Activity、DecorView之间的关系
  • View
    • View的生命周期
      • Attachment / Detachment
      • Traversals
      • State Save / Restore
      • invalidate()和requestLayout()
    • View的生命周期与Activity的生命周期的关联
      • Activity创建时如何关联View
    • 常用属性
      • ID
      • Position
      • Size, padding 和 margins
  • 事件分发
    • Activity的事件分发
      • 源码分析
      • 流程描述
      • 流程框图
    • ViewGroup的事件分发
      • 源码分析
      • 流程描述
      • 流程框图
    • 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并不需要全局独一无二的,而是要在它所属的树里是唯一的。

Position

一个view占据一个方形的位置。view的位置用左上角的点来表示。位置和尺寸的单位是像素(pixel)。

调用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()

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值。

事件分发

  • 事件分发的本质是将点击事件(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(),如果没有直接结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/594857.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[深度学习]yolov7 pytorch模型转onnx,转ncnn模型和mnn模型使用细节

文章目录 前言1.前置1.1 安装必要的库1.2 .pt 权重转ncnn 和mnn所需要的权重 2、编码C项目1.ncnn2.mnn 总结 前言 yolov7 pytorch模型转onnx&#xff0c;转ncnn模型和mnn模型使用细节&#xff0c;记录一下 git仓库&#xff1a; yolov7 https://github.com/WongKinYiu/yolov7 n…

JQL的语法格式

JQL&#xff08;Jira Query Language&#xff09;的语法格式如下&#xff1a; <field> <operator> <value> 其中&#xff0c; 表示 Jira 中的字段&#xff08;例如 project、assignee、status 等&#xff09;&#xff0c; 表示操作符&#xff08;例如 、!、&…

uni-app路由进阶—不同路由跳转配置的使用

uni-app路由进阶—不同路由跳转配置的使用 uni-app路由进阶—不同路由跳转配置的使用 文章目录 uni-app路由进阶—不同路由跳转配置的使用前言一、配置2个一级导航页面&#xff08;tabBar&#xff09;二、路由配置分类总结 前言 UNI-APP学习系列之uni-app路由进阶—不同路由跳…

SQL注入基本原理

1、什么是Sql注入攻击 SQL注入攻击通过构建特殊的输入作为参数传入Web应用程序&#xff0c;而这些输入大都是SQL语法里的一些组合&#xff0c;通过执行SQL语句进而执行攻击者所要的操作&#xff0c;它目前是黑客对数据库进行攻击的最常用手段之一。 本课程将带你从介绍 Web 应用…

ELK日志采集系统搭建

需求背景 现在的系统大多比较复杂&#xff0c;一个服务的背后可能就是一个集群的机器在运行&#xff0c;各种访问日志、应用日志、错误日志量随着访问量和时间会越来越多&#xff0c;运维人员就无法很好的去管理日志&#xff0c;开发人员排查问题&#xff0c;需要到服务器上查…

赛灵思 ZYNQ UltraScale+ MPSoC Petalinux驱动开发:EMIO-GPIO输入驱动

目录 Zynq UltraScale MPSoC Linux下EMIO-GPIO驱动1、MPSOC GPIO简介2、vivado中EMIO配置3、EMIO设备树修改 Zynq UltraScale MPSoC Linux下EMIO-GPIO驱动 声明&#xff1a;本文是学习赛灵思 Zynq UltraScale MPSoC 5EV过程中写的笔记&#xff0c;便于以后复习&#xff0c;参考…

基于Faster R-CNN实现目标检测

目录 1. 作者介绍2. Faster RCNN基本框架3.模型训练及测试3.1 数据集3.2 环境配置3.3 训练参数3.4 训练参数3.5 代码展示3.6 问题及分析 参考&#xff08;可供参考的链接和引用文献&#xff09; 1. 作者介绍 杨金鹏&#xff0c;男&#xff0c;西安工程大学电子信息学院&#x…

Mybatis-puls——入门案例和概述和CURD功能实现

前言 虽然但是&#xff0c;现在MyBatis_puls并不支持springboot3.x版本。 MyBatis_puls就像SpringBoot是为了快速开发Spring程序一样&#xff0c;这个是为了快速开发MyBatis程序。基于SpringBoot使用MP的开发流程 按照下面这个模板造就对了。 SpingBoot——SB整合MB的web项…

Profinet通信协议基础知识

目录 1、Profinet是PI退出的开放式以太网标准: 2、Profinet的参考模式 3、Profinet的应用领域

若依框架快速搭建(一)

若依框架开发 若依框架介绍前期准备相关工具IDEAwebstromNavcatMavenRedis 前后端项目搭建 后端搭建前端搭建源码下载 若依框架介绍 若以管理系统的网址&#xff1a;http://ruoyi.vip/ 代码为开源代码 主要分为四部分&#xff0c;第一个是整体项目&#xff0c;第二个是前后端…

UFS 3 - UFS RPMB

UFS 1-UFS RPMB 1 RPMB介绍2 RPMB Well Known Logical Unit Description3 Requirements3.1 RPMB Resources3.2 Algorithm and Key for MAC Calculation3.3 RPMB Message Components3.4 Request Message Types3.5 Response Message Types3.6 RPMB Operation Result 4 Implementa…

Springboot +spring security,前后端分离时的security处理方案(二)

一.简介 在前后端分离这样的开发模式下&#xff0c;前后端的交互都是通过 JSON 来进行数据传递的&#xff0c;无论登录成功还是失败&#xff0c;都不会有服务端跳转或者客户端跳转之类的操作。 也就是说无论登录成功还是失败&#xff0c;服务端都会返回一段登录成功或失败的 …

Go 1.19 排序算法

插入排序&#xff08;InsertionSort&#xff09; 插入排序是一种简单直观的排序算法&#xff0c;它的基本思想是将待排序的元素插入到已经排好序的序列中&#xff0c;从而得到一个新的有序序列。插入排序的具体过程如下&#xff1a; 从第一个元素开始&#xff0c;认为它已经是…

RK3568 i2s TDM数据抓取

1. I2S接口 I2S协议只定义三根信号线:时钟信号SCK、串行数据信号SD、左右声道选择信号WS。 SCK 时钟信号,Serial Clock,也可能称BCLK/Bit Clock或SCL/Serial Clock。 WS 左右声道选择信号,Word Select,也称帧时钟,也可能称LRCLK/Left Right Clock。 SD 串行数据信号,Ser…

计算机网络第一章——计算机网络系统结构(下)

提示&#xff1a;总角之宴&#xff0c;言笑晏晏。信誓旦旦&#xff0c;不思其反。反是不思&#xff0c;亦已焉哉。 文章目录 1.2.1 分层结构&#xff0c;协议&#xff0c;接口和服务为什么要有分层&#xff1f;怎么分层正式认识分层结构概念总结 1.2.2 OSI 参考模型ISO参考模型…

Markdown 格式文章的图床

chatGPT 奖励模型示意图&#xff1a; chatGPT RLHF(基于人类反馈的强化学习) 模型示意图&#xff1a; 强化学习过程示意图&#xff1a;

《HashMap的数据结构》

目录 HashMap概述&#xff1a; 数据结构的组成&#xff1a; 一个键值对是如何存入该结构中&#xff1a; HashMap中链表和红黑树的用途和转换方式 &#xff1a; HashMap概述&#xff1a; HashMap是基于哈希表的Map接口实现的&#xff0c;它存储的内容是键值对<key,value&g…

Web安全:文件包含漏洞测试(防止 黑客利用此漏洞.)

Web安全&#xff1a;文件包含漏洞测试. 文件包含的漏洞是 程序员在开发网站的时候&#xff0c;为了方便自己开发构架&#xff0c;使用了一些包含的函数&#xff08;比如&#xff1a;php开发语言&#xff0c;include() , include_once() , require_once() 等等 &#xff09;&a…

书单 | 数据治理的30本书

随着数字经济时代的到来&#xff0c;数据的价值不断被发掘。党的十九届四中全会首次将“数据”列为生产要素&#xff0c;充分凸显了数字经济时代数据对于经济活动和社会生活的巨大价值。开展数据治理的理论探索和实践创新&#xff0c;有利于全面释放数据价值助力数字经济发展&a…

校园高校共享单车管理系统nodejs+vue+express

设计的管理员的详细功能见下图&#xff0c;管理员登录进入本人后台之后&#xff0c;管理单车和区域&#xff0c;审核租赁订单和还车订单&#xff0c;收取租赁费用&#xff0c;查看单车租赁统计信息。 vue的文件结构其实就是一个index.html 中间的内容&#xff0c;用的是vue&am…