美文网首页android技术
Android中View相关的基础知识

Android中View相关的基础知识

作者: 小杨不想努力了 | 来源:发表于2022-01-19 01:07 被阅读0次

参考资料:
《Android开发艺术探索》
https://www.jianshu.com/p/3d2c49315d68

  1. View的事件分发机制,滑动冲突;ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL

    1. view的分发机制

      点击事件产生后,这个事件被分装成一个类:MotionEvent。系统首先会将事件传递给当前的 Activity,Activity会调用它的 dispatchTouchEvent 方法,将事件交给 PhoneWindow,通过 PhoneWindow 传递给 DecorView,然后再传递给根 ViewGroup,进入 ViewGroup 的 dispatchTouchEvent 方法,执行 onInterceptTouchEvent() ,如果ViewGroup 的 onInterceptTouchEvent() 返回true,表示它要拦截这个事件,false表示不拦截,再不拦截的情况下,此时会遍历 ViewGroup 的子元素,进入子 View 的 dispatchToucnEvent 方法,如果子 view 设置了 onTouchListener,不为null,就执行 onTouch 方法,并根据 onTouch 的返回值为 true 还是 false 来决定是否执行 onTouchEvent 方法,如果是 true,则表示事件被消费了,不会执行onTouchEvent(),如果是 false 则继续执行 onTouchEvent,可见onTouchListener优先级>onTouch>onTouchEvent。在源码中的话可以看到,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,该方法就会返回true消耗这件事,在 onTouchEvent 的 ACTION_UP 事件发生时会触发 performClick(),如果View设置了 onClickListener ,就会调用 performClick() 中的 onClick()。

      注意

      • ViewGroup默认不拦截任何事件

      • View没有onInterceptTouchEvent方法,一旦有点击事件交给他,onTouchEvent()一定会被调用

      • View的onTouchEvent()默认都会消费事件(返回true),除非长短点击都为False

        // 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器
        final boolean intercepted;
         if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
        //disallowIntercept:是否禁用事件拦截的功能(默认是false),即不禁用
        //可以在子View通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改,不让该View拦截事件
         final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        //默认情况下会进入该方法
         if (!disallowIntercept) {
         //调用拦截方法
         intercepted = onInterceptTouchEvent(ev); 
         ev.setAction(action);
         } else {
         intercepted = false;
         }
         } else {
         // 当没有触摸targets,且不是down事件时,开始持续拦截触摸。
         intercepted = true;
         } 
        

        这一段的内容主要是为判断是否拦截。如果当前事件的MotionEvent.ACTION_DOWN,则进入判断,调用ViewGroup.onInterceptTouchEvent()方法的值,判断是否拦截。如果mFirstTouchTarget != null,即已经发生过MotionEvent.ACTION_DOWN,并且该事件已经有ViewGroup的子View进行处理了,那么也进入判断,调用ViewGroup. onInterceptTouchEvent()方法的值,判断是否拦截。如果不是以上两种情况,即已经是MOVE或UP事件了,并且之前的事件没有对象进行处理,则设置成true,开始拦截接下来的所有事件。这也就解释了如果子View的onTouchEvent()方法返回false,那么接下来的一些列事件都不会交给他处理。如果VieGroup的onInterceptTouchEvent()第一次执行为true,则mFirstTouchTarget = null,则也会使得接下来不会调用onInterceptTouchEvent(),直接将拦截设置为true。

    2. 滑动冲突

      https://note.youdao.com/yws/public/resource/328432cea4f2eeddc18f0ca0446558d9/xmlnote/FB40C25074044D4FBD01D94CBF53F043/23669

      1.外部拦截法

      从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不需要则不拦截返回false。其伪代码如下:

      public boolean onInterceptTouchEvent(MotionEvent event) {
       boolean intercepted = false;
       int x = (int)event.getX();
       int y = (int)event.getY();
       switch (event.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;
      }
      在这里,首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器,子元素就没有机会处理事件了。move事件,根据需要决定是否拦截,如果父容器需要拦截就返回false,否则返回true。其次是up事件也返回false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。  
      

      2.内部拦截法

      所有事件都传递给子元素,如果子元素需要就消耗掉,不需要就交给父元素处理,需要子元素配合requestDisallowInterceptTouchEvent方法才能正常工作;此外,父元素需要默认拦截除ACTION_DOWN以外的事件(下文的Flag作用与他相反,一旦子view设置,ViewGroup无法拦截除ACTION_DOWN以外的事件),这样子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截需要的事件。

      因为viewGroup在分发事件时,如果是down事件,会重置这个标记,那么子view设置的就会无效(标记在requestDisallowInterceptTouchEvent方法中设置),down事件不受FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截down事件,那么所有事件都无法传递到子元素去。

      @Override
      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;
      return super.dispatchTouchEvent(event);
      }
      

      然后修改父容器的onInterceptTouchEvent方法:

      @Override
      public boolean onInterceptTouchEvent(MotionEvent event) {
      int action = event.getAction();
      if (action == MotionEvent.ACTION_DOWN) {
      return false;
      } else {
      return true;
      }
      }
      
    3. MotionEvent事件

      1. MotionEvent.ACTION_DOWN

        当屏幕检测到第一个触点按下之后就会触发到这个事件。

      2. MotionEvent.ACTION_MOVE

        当触点在屏幕上移动时触发,触点在屏幕上停留也是会触发的,主要是由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动)。

      3. MotionEvent.ACTION_UP

        当最后一个触点松开时被触发。

      4. MotionEvent.ACTION_CANCEL

        不是由用户直接触发,有系统再需要的时候触发,例如当父view通过使函数onInterceptTouchEvent()返回true,拦截了事件,也就是从子view拿回处理事件的控制权时,就会给子view发一个ACTION_CANCEL事件,这里了view就再也不会收到事件了。可以将其视为ACTION_UP事件对待。

  2. View的绘制流程,如何自定义View

    View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高(几乎所有的情况下测量的宽高就是View最终的宽高),layout来确定View在父容器的放置位置(View的4个顶点和实际的View宽高),而draw则负责将View绘制在屏幕上。

    理解View的测量过程,我们需要先理解一下MeasureSpec。MeasureSpec(32位int值)由两部分组成,一部分是测量模式(SpecMode高2位),另一部分是测量的尺寸大小(SpecSize低30位)。

    SpecMode有三类:

    • UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

    • EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,

    • AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

    那么MeasureSpec又是如何确定的?

    对于顶级View,也就是DecorView,其确定是通过屏幕的大小和自身的布局参数LayoutParams确定的。那么对于View,其MeasureSpec由父布局的MeasureSpec和自身的布局参数LayoutParams来决定。

    img
    • 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循Layoutparams的大小。

    • 当View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间

    • 当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。

    • UNSPECIFIED主要用于系统内部多次Measure的情况,不太需要关注

    measure:

    • View的measure过程由其measure()方法完成,measure()方法是final类型的,子类不能重写。在View的measure()方法中会去调用View的onMeasure()方法来完成测量。有两个重要的方法如下:

      img

      注意:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent.

      解决:只需要给View指定一个默认的内部宽高(mWidth和mHeight),并在wrap_content时设置此宽高即可

    • ViewGroup的measure过程

      从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性。

      img

      单一View的measure过程对onMeasure()有统一的实现,但ViewGroup的measure过程是没有的,因为ViewGroup是一个抽象类,它的子类如:LinearLayout、RelativeLayout、自定义ViewGroup子类等具备不同的布局特性,这导致它们的子View测量方法各有不同,所以onMeasure()的实现也会有所不同,无法对onMeasure()作统一实现,所以其测量过程的onMeasure方法由各个子类去具体实现。

      img

      注意:

      针对Measure流程,自定义ViewGroup的关键在于:根据需求复写onMeasure(),从而实现子View的测量逻辑。复写onMeasure()的步骤主要分为三步:

      1. 遍历所有子View及测量:measureChildren()

      2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:需自定义实现

      3. 存储测量后View宽/高的值:setMeasuredDimension()

    layout:

    测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

    其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。相反Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

    img

    注意:onLayout()用来确定子View的布局,onLayout()是一个可继承的空方法,具体实现和具体布局有关。

    draw:

    View的绘制过程遵循如下几步:

    1. 绘制背景 background.draw(canvas)

    2. 绘制自己(onDraw)

    3. 绘制Children(dispatchDraw)

    4. 绘制装饰(onDrawScrollBars)

    无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

    img

相关文章

网友评论

    本文标题:Android中View相关的基础知识

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