View的工作原理和事件体系

View的基础知识

View是所有控件的基类,ViewGroup继承了View, ViewGroup表示一个控件组,内部可以包含多个控件, 例如LineraLayout就是继承的ViewGroup,它里面可以包含多个子控件。即View可以是单个控件也可以是多个控件组成的控件组。

  • View的位置参数
    View在平移过程中,left、top、right、bottom这几个值是不会改变的,改变的是x、y、translationX、
    translationY这几个值

    参数名 解释
    left View的左边界距父容器左边界的距离
    top View的上边界距父容器上边界的距离
    right View的右边界距父容器左边界的距离
    bottom View的底边界距父容器上边界的距离
    x left+translationX
    y top+translationY
    translationX View的左上角X方向相对于父容器的偏移量
    translationY View的左上角Y方向相对于父容器的偏移量
  • MotionEvent和TouchSlop

    • 典型的MotionEvent事件
    名称 解释
    ACTION_DOWN 手指刚接触屏幕
    ACTION_MOVE 手指在屏幕上滑动
    ACTION_UP 手指从屏幕上松开的一瞬间
    • 获取点击位置的坐标
      getX()/getY() 获取的是相对于当前View左上角的坐标
      getRawX()/getRawY() 获取的是相对于手机屏幕左上角的坐标
    • TouchSlop
      系统识别认为是滑动的最小距离
         /**
          * Distance a touch can wander before we think the user is scrolling in dips.
          * Note that this value defined here is only used as a fallback by legacy/misbehaving
          * applications that do not provide a Context for determining densit   configuration-dependent
          * values.
          *
          * To alter this value, see the configuration resourc config_viewConfigurationTouchSlop
          * in frameworks/base/core/res/res/values/config.xml or the appropriate devic resource   overlay.
          * It may be appropriate to tweak this on a device-specific basis in an overla based on
          * the characteristics of the touch panel and firmware.
          */
         private static final int TOUCH_SLOP = 8;
      
  • VelocityTracker、 GestureDetector、 Scroller

    • VelocityTracket
         //获取实例
         private var velocityTracker: VelocityTracker = VelocityTracker.obtain()
        
          //添加事件
          /**
           * Add a user's movement to the tracker.  You should call this for the
           * initial {@link MotionEvent#ACTION_DOWN}, the following
           * {@link MotionEvent#ACTION_MOVE} events that you receive, and the
           * final {@link MotionEvent#ACTION_UP}.  You can, however, call this
           * for whichever events you desire.
           * 
           * @param event The MotionEvent you received and would like to track.
           */
         velocityTracker.addMovement(event)
      
         //在MotionEvent#ACTION_UP的时候计算速率
         /**
          *@param units 单位为毫秒(millisecond),表示速率的单位时间
          *       如值是1000,则在1000毫秒内滑过100像素(px),速率就是100
          *       如值是100,如果在100毫秒内同样滑过100像素,速率也是100
          *       速率 = 终点位置(ACTION_DOWN)- 初始位置(ACTION_DOWN)/(经过的时间/units)
          */
         velocityTracker.computeCurrentVelocity(1000)
         // 获取x、y方向上的速率
         val xVelocity = velocityTracker.xVelocity
         val yVelocity = velocityTracker.yVelocity  
      
         //将实例重置为初始状态
         velocityTracker.clear()
         //回收内存
         velocityTracker.recycle()
      
    • GestureDetector
      Android手势介绍
    • Scroller
      实现View的弹性滑动,详情见下文

View的滑动

实现滑动的方式及弹性滑动

Android动画介绍

  • 滑动
实现滑动的方式
scrollTo()/scrollBy()
View动画
属性动画
LayoutParams
  1. scrollTo()/scrollBy()
     // 只是移动View里面的内容,对于像ImageView之类的单个控件来说,内容就是里面的图片
     // 对于ViewGroup这种控件组,内容就是其中的子View.
     // scrollBy()是相对目前的scrollX, scrollY进行移动
     // scrollTo()则是覆盖之前的scrollX, scrollY进行移动
     // scrollX == View的左边坐标 - View内容的左边坐标,即scrollX为负值时,View内容向右移
     // scrollY == View的顶部坐标 - View内容的顶部坐标, scrollY为负值时,View内容向下移动
     SLIDE_MODE_SCROLL -> {
         scrollTo(-event.x.toInt(), 0)
     }

  1. View动画
    //移动后,点击响应的位置还在原来的区域
    // left,top,right,bottom属性不变
    // x, y属性不变
    SLIDE_MODE_ANIMATION -> {
        val translateTime = (Math.abs(event.x - x) / 100 * 1000).toLong()
        val translateAnimation = TranslateAnimation(x, event.x, y, y).apply {
            duration = translateTime
            fillAfter = true
        }
        this.startAnimation(translateAnimation)
    }

  1. 属性动画
    //移动后,点击响应的位置在移动后的区域
    // left,top,right,bottom属性不变
    // translationX,translationY改变 导致x, y属性改变
    SLIDE_MODE_ANIMATOR -> {
        val translateTime = (Math.abs(event.x - x) / 100 * 100).toLong()
        ObjectAnimator.ofFloat(this, "translationX", x, event.x).setDuratio(translateTime).start()
    }

  1. LayoutParams
    //移动后,点击响应的位置在移动后的区域
    // left,top,right,bottom属性根据情况改变
    // left,top改变 导致x, y属性改变
    SLIDE_MODE_LAYOUT_PARAMS -> {
                  (this.layoutParams    ViewGroup.MarginLayoutParams)?.apply {
                      leftMargin = event.x.toInt()
                      [email protected]()
                  }
              }
   
  • 弹性滑动
  1. 弹性滑动实际上是将一次滑动“微分”成一次次小滑动,并在一个合理的时间段内完成,而不像普通的滑动一次滑动就完成所有工作
  2. 实现弹性滑动一般结合scrollTo()来实现
实现弹性滑动的方式
使用Scroller
使用属性动画
延时策略(Handler#sendMessageDelay())
  1. Scroller
   //初始化时创建实例
   private var mScroller: Scroller = Scroller(context)

   //重写computeScroll实现
   SLIDE_MODE_SMOOTH_SCROLL -> smoothScrollTo(-event.x.toInt())

   private fun smoothScrollTo(destX: Int) {
       val deltaX = destX - scrollX
       val time = Math.abs(deltaX) / 100 * 1000
       mScroller.startScroll(scrollX, y.toInt(), deltaX, y.toInt(), time)
       //重绘
       invalidate()
  }

   //会在draw的时候调用computeScroll()
   override fun computeScroll() {
      //计算当前的滑动偏移
      //滑动未结束则返回true
       if (mScroller.computeScrollOffset()) {
           scrollTo(mScroller.currX, mScroller.currY)
           //scrollTo()之后重绘,达成弹性滑动的效果
           postInvalidate()
       }
   }

  1. 属性动画
   //动画实现弹性滑动
   SLIDE_MODE_ANIMATOR_SCROLL -> {
       lastScrollX = scrollX
       mScrollX = -event.x.toInt() - scrollX
       ValueAnimator.ofInt(0, 1).apply {
           duration = Math.abs(mScrollX) / 100 * 1000.toLong()
           addUpdateListener {
               it.animatedFraction
               [email protected]((it.animatedFraction * mScrollX).toInt() + lastScrollX, 0)
           }
           start()
       }
   }

  1. 延时策略(Handler#sendMessageDelay())

    //通过Handler#sendMessageDelay实现弹性滑动
     SLIDE_MODE_HANDLER_SMOOTH -> {
         lastScrollX = scrollX
         mScrollX = -event.x.toInt() - scrollX
         intervalX = if (mScrollX < 0) -10 else 10
         mHandler.sendMessage(Message.obtain(nul SLIDE_MODE_HANDLER_SMOOTH, intervalX, 0 ))
     }


     //Handler
     private class SlideHandler(private val weakReference: WeakReference) : Handler() {
         private var scrollerX = 0
         override fun handleMessage(msg: Message?) {
             if (msg?.what == SLIDE_MODE_HANDLER_SMOOTH) {
                 weakReference.get()?.run {
                     scrollerX += msg.arg1
                     scrollTo(scrollerX + lastScrollX, 0)
                     mScrollX -= intervalX
                     if (Math.abs(mScrollX) >= Math.abs(intervalX)) {
                         mHandler.sendMessageDelayed(Message.obtain(null,     SLIDE_MODE_HANDLER_SMOOTH, intervalX, 0), 100)
                     } else {
                         scrollerX = 0
                     }
                 }
             }
         }
     }

滑动冲突

滑动冲突场景 处理原则
外部滑动方向和内部滑动方向不一致 根据x方向滑动距离和y方向的滑动距里的差值 或 根据二者的夹角来判断滑动方向,如ViewPager的解决方式
外部滑动方向和内部滑动方向一致 根据具体需求,如可以根据滑动的距离,滑动起始位置来判断具体是哪个部分滑动
以上两者的嵌套
  • 滑动冲突处理方式

    滑动冲突处理方式
    外部拦截法,父容器需要就拦截,不需要就不拦截。重写父容器的onInterceptTouchEvent
    内部拦截法,父容器也需要配合修改,子元素需要就直接消耗,重写dispatchTouchEvent且要配合requestDisallowTouchEvent,参考ViewPager
  1. 外部拦截法-简单示例
   override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
       var intercepted = false
       when(ev?.action){
           //如果拦截了ACTION_DOWN,后续的事件都会在当前View中处理
           //子View中的onClick(在ACTION_UP的时候)之类的事件也不会触发
           MotionEvent.ACTION_DOWN ->{
               intercepted =false
           }
           MotionEvent.ACTION_MOVE ->{
               //是否需要事件
               intercepted = needMotionEvent()
           }
           // 如果在这之前已经拦截了,返回true和false 相差不大
           // 如果之前没有拦截,此处返回了true,那么子View中设置的onClick(ACTION_UP的时候)就会无效
           MotionEvent.ACTION_UP ->{
               intercepted = false
           }
       }
       return intercepted
    }

  1. 内部拦截法(可参考ViewPager)-简单示例
   //子元素
    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
       when(event?.action){
           //ACTION_DOWN在父元素中默认不拦截,如果拦截了ACTION_DOWN的话之后的事件都不会传递
           MotionEvent.ACTION_DOWN ->{
               //父元素不拦截事件
               parent.requestDisallowInterceptTouchEvent(true)
           }
           MotionEvent.ACTION_MOVE ->{
               if (parentNeedEvent()){
                   parent.requestDisallowInterceptTouchEvent(false)
               }
           }
       }
       return super.dispatchTouchEvent(event)
   }
   //父元素
   override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
       return ev?.action != MotionEvent.ACTION_DOWN
   }

View的事件分发

View的事件分发

Note:
1. 事件的传递从Activity开始到View结束,如果被消费了就不再继续传递ACTION_DOWN是事件开始的标识,不会被拦截。例如重写了Activity的onTouchEvent方法并返回true,则ACTION_DOWN之后的事件都不会向下一级分发。
2. 事件的消费从View开始到Activity结束,只能消费一次例如一个自定义View,重写了onTouchEvent方法并返回true,则表示要消费此次事件,这个自定义View会接收ACTION_DOWN之后的事件,而在它的消费链下一级(ViewGroup、 Activity)一些用来消费事件的方法不会被调用(onTouchEvent、一些点击事件)
3. onTouchListener的执行顺序在onTouchEvent之前,如果onTouchListener返回true消费了事件则onTouchEvent不会调用,一些点击事件是在onTouchEvent中处理的。
4. 像onClick(ACTION_UP后执行),onLongClick(ACTION_DOWN一般延时500ms后还是press后执行)都是在onTouchEvent中执行的,设置了这些点击事件则onTouchEvent返回的是true.
5. onLongClick中返回的bool值,true表示消费结束了,其它点击事件不再响应,false则其他点击事件还可以响应.

View的工作原理

基本概念

  • ViewRoot、DecorView、Window
    1. 在ActivityThread#performLaunchActivity()中执行Activity#attach , Activity#attach中创建了PhoneWindow
       mWindow = new PhoneWindow(this, window, activityConfigCallback);
    
    1. 在ActivityThread#handleResumeActivity中调用了Activity#makeVisiable
     void makeVisible() {
         if (!mWindowAdded) {
             ViewManager wm = getWindowManager();
             wm.addView(mDecor, getWindow().getAttributes());
             mWindowAdded = true;
         }
         mDecor.setVisibility(View.VISIBLE);
     }
    
    1. 在Activity#makeVisible中调用了WindowManagerImpl#addView
    2. WindowManagerImpl#addView中调用了WindowManagerGlobal#addView,在WindowManagerGlobal#addView中创建了ViewRootImpl,
      通过ViewRootImpl#setView将Window相关属性和DecorView关联了起来。
       ... 
    
       root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
    
      ...
    
    
    1. ActivityThread#performResumeActivity最终会执行到Activity#onResume,ActivityThread#handleResumeActivity在performResumeActivity之后。也就是说ViewRootImpl的创建在Activity#onResume回调执行之后
  • 绘制流程

    1. 在ViewRootImpl中
        final class TraversalRunnable implements Runnable {
           @Override
           public void run() {
               doTraversal();
           }
       }
       final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
        void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    
        void unscheduleTraversals() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
                mChoreographer.removeCallbacks(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            }
        }
    
        void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
                mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
    
                performTraversals();
    
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    
    1. 在ViewRootImpl#perforTranversals中按顺序调用:
      测量:performMeasure -> View#measure -> View#onMeasure
      布局:performLayout -> View#layout -> View#onLayout
      绘制:performDraw -> View#draw -> View#onDraw
    2. 测量中MeasureSpec的获取
      ViewRootImpl#dispatchResized(将传进来的数据包括屏幕信息用Message包装,用ViewRootHandler发送) ->
      ViewRootHandler(mWinFrame.set((Rect) args.arg1);) ->
      getRootMeasureSpec(mWinFrame.width/mWinFrame.height,LayoutParams)(根据宽高及Laoutparams属性组装MeasureSpec) ->
      performMeasure(widhMeasureSpec,heightMeasureSpec)

    NOTE:

    1. Android在子线程中更新UI的时候会抛出异常,这个是在ViewRootImpl#checkThread()中处理的
      void checkThread() {
          if (mThread != Thread.currentThread()) {
              throw new CalledFromWrongThreadException(
                      "Only the original thread that created a view hierarchy can touch its views.");
          }
      }
    
    1. ViewRootImpl的创建是在Activity#onResume之后才创建的,所以如果是在ViewRootImpl创建之前在子线程中更新UI是不会抛出异常的

    2. 原因: 因为Android的View控件是非线程安全的,所以要进行checkThread(),如果加入线程同步的话会出现两个问题:1. 使逻辑变得复杂;2.锁机制会降低UI访问效率,因为在多个线程的情况下会阻塞一些线程的运行

  • MeasureSpec

    1. MeasureSpec是个32位的int值,高2位表示SpecMode,低30位表示SpecSize
    
       private static final int MODE_SHIFT = 30;
       private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
       /**
        * Measure specification mode: The parent has not imposed any constraint
        * on the child. It can be whatever size it wants.
        */
       public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
       /**
        * Measure specification mode: The parent has determined an exact size
        * for the child. The child is going to be given those bounds regardless
        * of how big it wants to be.
        */
       public static final int EXACTLY     = 1 << MODE_SHIFT;
    
       /**
        * Measure specification mode: The child can be as large as it wants up
        * to the specified size.
        */
       public static final int AT_MOST     = 2 << MODE_SHIFT;
    
       public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) { // Android版本<=17时才为true
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
    
    UNSPECIFIED 父容器不对View有任何限制,要多大给多大,对应于系统内部的测量状态
    EXACTLY 父容器已经测量出View所需的精确大小,这时候View的最终大小就是SpecSize所指定的值。对应于LayoutParams中的match_parent和具体数值的模式
    AT_MOST 父容器指定了一个可用大小SpecSize,View的大小不能大于这个值,最终多大看具体情况,对应于LayoutParams的wrap_content
  • MeasureSpec和LayoutParams的对应关系
    可查看ViewGroup#measureChild
parentLayoutParams parentSpecMode childLayoutParams childSpecMode childSpecSize
match_parent EXACTLY match_parent EXACTLY availableSize
match_parent EXACTLY wrap_content AT_MOST availableSize
match_parent EXACTLY dp EXACTLY childSize
wrap_parent AT_MOST wrap_content AT_MOST availableSize
wrap_parent AT_MOST match_parent AT_MOST availableSize
wrap_parent AT_MOST dp EXACTLY childSize
dp EXACTLY match_parent EXACTLY availableSize
dp EXACTLY dp EXACTLY childSize
dp EXACTLY wrap_content AT_MOST availableSize
UNSPECIFIED match_parent UNSPECIFIED UNSPECIFIED
UNSPECIFIED wrap_content UNSPECIFIED UNSPECIFIED
UNSPECIFIED dp EXACTLY childSize
1. 如果View是采用固定宽高,不管父容器是什么模式,View都是EXACTLY    
2. 如果父容器是AT_MOST模式,View不是采用固定宽高,则View也是AT_MOST模式  
3. 如果View是AT_MOST模式,默认情况下会铺满剩余的所有空间,**这样的话就会于match_parent是一样的效果**,所以自定义View的时候最好对AT_MOST作自定义处理.

measure

  • measure的流程

    1. View#onMeasure()#getDefaultSize()
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          //setMeasureDimension(),设置View的测量宽高值
          setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                  getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
      }
    
      public static int getDefaultSize(int size, int measureSpec) {
          int result = size;
          int specMode = MeasureSpec.getMode(measureSpec);
          int specSize = MeasureSpec.getSize(measureSpec);
    
          //根据测量模式,返回对应的Size
          switch (specMode) {
          case MeasureSpec.UNSPECIFIED:
              result = size;
              break;
          case MeasureSpec.AT_MOST:
          case MeasureSpec.EXACTLY:
              result = specSize;
              break;
          }
          return result;
      }
    
      //mMinWidth是设置的布局属性“minWidth",默认是0
       case R.styleable.View_minWidth:
                  mMinWidth = a.getDimensionPixelSize(attr, 0);
                  break;
      // 如果背景是null的话,取mMinWidth
      // 如果背景不为null的话,取mMinWidth、背景宽度中的最大值  
      // ShapeDrawable没有原始宽度,BitmapDrawable有原始宽度(图片尺寸)          
      protected int getSuggestedMinimumWidth() {
          return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
    
    1. ViewGroup#measureChild()、ViewGroup#onMeasure()
      1. ViewGroup中没有重写View中的onMeasure而是交给具体的ViewGroup去根据各自特性实现
      2. 定义了measureChild(),用于得到childView的MeasureSpec及执行childView.measure(),在具体的ViewGroup#onMeasure中调用。
         protected void measureChild(View child, int parentWidthMeasureSpec,
                   int parentHeightMeasureSpec) {
               final LayoutParams lp = child.getLayoutParams();
               //根据父类的MeasureSpec,Padding,子类的LayoutParams得到传给子类的MeasureSpec
               final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                       mPaddingLeft + mPaddingRight, lp.width);
               final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                       mPaddingTop + mPaddingBottom, lp.height);
       
               child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
           }
      
      
    • 如何在View的测量执行结束后,获取View的宽/高
      由于View的measure和Activity的生命周期不是同步的,所以并不能确定在某个Activity的生命周期内,View的measure已经完成了。有以下几种方式能确切的获取View的宽高
      1. 重写Activity#onWindowFocusChanged()
        onWindowFocusChanged()在窗口焦点改变的时候调用,调用这个方法时表示View已经准备好了,宽高已经能够准确获取。但是会调用多次(频繁地Activity#onResume获取焦点,Activity#onPause失去焦点)
  2. view.post(runnable)  
     通过View#post()将一个**Runnable**添加到消息队列的末尾,等到  Looper调用到此Runnable时,View已经准备完毕了。
     
  3.ViewTreeObserver#onGlobalLayoutListener  
    **View树的状态改变,此方法会被回调多次**
    ```kotlin
       view.viewTreeObserver().addOnClobalLayoutListener{
           //method body
       }
    ```   
  4.view.measure(widthMeasureSpec,heightMeasureSpec)    
   **View的尺寸是30位二进制,故(1 << 30) -1)**
  |              |      |
  |:-------------|:-----|
  | match_parent |如果是childView的话,需要知道parentView中的剩余空间,如果是parentView,则可以作为具体数值的方式处理(屏幕的宽高)|
  | 具体数值(50dp)|` widthMeasureSpec = MeasureSpec.makeMeasureSpec(50, MeasureSpec.EXACTLY)`  `heightMeasureSpec = MeasureSpec.makeMeasureSpec(50, MeasureSpec.EXACTLY)`  `view.measure(widthMeasureSpec,heightMeasureSpec)`|
  | wrap_content |` widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<< 30)-1, MeasureSpec.AT_MOST)`  `heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<< 30)-1, MeasureSpec.AT_MOST)`  `view.measure(widthMeasureSpec,heightMeasureSpec)`|

layout

1. layout()中**setFrame**确定元素四个顶点的位置,调用onLayout确定子元素的位置
2. 测量宽高默认情况下等于最终的宽高,但有些特殊情况
   ```java
      //重写layout方法,改变了四个顶点的值
      public void layout(int l, int t, int r, int b){
          super.layout(l+50, t, r, b);
      }
   ```  
   **还有就是多次measure的情况,在前几次的measure中测量宽高可能和最终宽高不同**

draw

   /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

当确切知道需要绘制内容时,关闭WILL_NOT_DRAW。
ViewGroup中默认启用WILL_NOT_DRAW,View中默认关闭WILL_NOT_DRAW。

  1. 绘制背景
       // Step 1, draw the background, if needed
       if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    
  2. 绘制自身内容
       // Step 3, draw the content
       if (!dirtyOpaque) onDraw(canvas);
    
  3. 绘制children
       // Step 4, draw the children
       dispatchDraw(canvas);
    
  4. 绘制装饰
       // Step 6, draw decorations (foreground, scrollbars)
       onDrawForeground(canvas);
    

自定义View

自定义View

资源

Android开发艺术探索 - 任玉刚

你可能感兴趣的:(View的工作原理和事件体系)