Android——View的工作原理

View的工作原理

1.1 ViewRoot和DecorView简介

1.1.1 ViewRoot相关

  • View的三大流程:1.View的测量流程;2.布局流程;3.绘制流程。

  • ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的。

  • 在ActivityThread中,当Activity对象创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联:

    root = new ViewRootImpl(view.getContext(),display);
    root.setView(view,wparams,panelParentView);
    
  • View的绘制流程是从ViewRoot的performTraversals方法开始的,经过measurelayoutdraw三个流程才将View绘制完成。measure用来测量View的宽和高;layout用来确认View在父容器中的放置位置;draw用来将View绘制在屏幕上。

    performTraversals工作流程

  • performTraversals会依次调用 performMeasure、performLayout、performDraw三个方法,这三个方法分别完成顶级View的measure、layout、draw三大流程。其中在performMeasure中会调用measure方法,measure方法又会调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程,这样measure流程就从父容器流转到子元素中了。子元素重复父容器过程,反复直到整个View树的遍历。performLayout和performDraw的传递流程和performMeasure类似,不同的是:performDraw的传递过程是在draw方法中通过dispatchDraw实现的,本质上无差异。

  • measure过程决定了View的宽/高。measure完成后,可以通过getMeasuredWidthgetMeasuredHeight获取View测量后的宽/高。正常情况下measure过程的View的宽/高就是实际的View的宽/高。

  • layout过程决定了View的四个顶点的坐标和实际的View的宽/高。layout完成后,可以通过getTopgetBottomgetLeftgetRight获取View的四个顶点的位置,且可以通过getWidthgetHeight获取View的最终宽/高。

  • draw过程决定了View的显示。draw完成后,View的内容呈现在屏幕上。

1.1.2 DecorView相关

  • DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,LinearLayout中有上下两部分,标题栏和内容栏(具体与Android版本主题有关)。

  • 在Activity中通过setContentView设置的布局文件就是被加到内容栏中,而内容栏的id是content,所有Activity指定布局的方法叫做setContentView而非setView。

  • 获取content的方法ViewGroup content = findViewById(R.android.id.content)

  • 获取View的方法content.getChildAt(0)

  • DecorView实际上是一个FrameLayout,View层的事件都是经过DecorView后传递给View的。

    顶级View结构DecorView

1.2 理解MeasureSpec

  • MeasureSpec在很大程度上决定一个View的尺寸规格。
  • 一个View的实际尺寸规格,还会受父容器影响。即父容器会影响View的MeasureSpec的创建过程。
  • 系统会将View的LayoutParams根据父容器施加的规则转换成对应的MeasureSpec,再根据这个MeasureSpec来测量出View的宽/高。

1.2.1 MeasureSpec

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

  • MeasureSpec内部的一些常量定义:

    public static class MeasureSpec {
        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) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }
    
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        
        ...
    

    MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,同时提供了打包解包的方法。一组SpecMode和SpecSize可以打包成一个MeasureSpec,一个MeasureSpec也可以拆成原始的SpecMode和SpecSize。(MeasureSpec这里指MeasureSpec代表的int,而非对象本身)

  • SpecMode三种类型

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

1.2.2 MeasureSpec和LayoutParams的对应关系

  • 系统内部是通过MeasureSpec来进行View的测量。正常情况下我们使用View指定的MeasureSpec,但我们也可以给View设置LayoutParams。

  • 在View测量的时候,系统会将LayoutParams在父容器的约束下转换成MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。

  • 注意:MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而决定View的宽/高。另外,对于顶级View(即DecorView)和普通View而言,MeasureSpec的转换过程是不同的:

    • DecorView:其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。
    • 普通View:其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同确定。
  • MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽/高。

  • ViewRootImpl中的measureHierarchy方法中的一段代码,展示了DecorView的MeasureSpec的创建过程:

    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    其中desiredWindowHeightchildWidthMeasureSpec是屏幕尺寸。

  • 再看getRootMeasureSpec方法的实现:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    上述代码可见DecorView的MeasureSpec的产生过程。其遵守如下规则(根据LayoutParams中的宽/高的参数来划分):

    • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小。
    • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
    • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。
  • 对于普通布局中的View来说,View的measure过程由ViewGroup传递而来。ViewGroup的measureChildWithMargins方法:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec,可见子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素自身的LayoutParams有关,此外还和View的margin及paddig有关。

    getChildMeasureSpec的具体代码:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
    
        int size = Math.max(0, specSize - padding);
    
        int resultSize = 0;
        int resultMode = 0;
    
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

    该方法是根据父容器的MeasureSpec和View自身的LayoutParams确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可以的大小为父容器的尺寸减去padding:

    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    
  • 普通View的MeasureSpec的创建规则

    EXACTLY AT_MOST UNSPECIFIED
    dp/px EXACTLY(childSize) EXACTLY(childSize) EXACTLY(childSize)
    match_parent EXACTLY(parentSize) AT_MOST(parentSize) UNSPECIFIED(0)
    wrap_parent AT_MOST(parentSize) AT_MOST(parentSize) UNSPECIFIED(0)

    :1.横排为childparentSpecmode,纵排为LayoutParams;2.parentSize指父容器中目前可使用的大小。

  • 只要提供了父容器的MeasureSpec和子元素的LayoutParams,就可以快速确定出子元素的MeasureSpec。有了MeasureSpec就可以进一步确定出子元素测量后的大小。

1.3 View的工作流程

1.3.1 measure过程

  • measure过程要分两种情况:

    • 原始的View:通过measure方法完成测量过程。
    • ViewGroup:完成自己的测量过程外,遍历去调用所有子元素的measure方法。
  • View的measure过程

    • View的measure过程由其measure方法完成。measure方法是fianl类型,在其方法内会调用View的onMeasure方法。onMeasure的方法实现:
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
    }
    

    setMeasuredDimension方法会设置View宽/高的测量值。再看getDefaultSize方法的实现:

    public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
    }
    

    可见getDefaultSize返回的大小是measureSpec中的specSize,这个specSize就是View测量后的大小。对于UNSPECIFIED情况,View的大小为getDefaultSize第一个参数size,即宽/高为getSuggestedMinimumWidth和getSuggestedMinimumHeight方法的返回值:

    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    
    protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
    

    从getSuggestedMinimumWidth代码可见,如果View没有设置背景,View的宽度为mMinWidth,mMinWidth对应android:minWidth属性值,如果该属性值不指定,mMinWidth默认0;如果View设置了背景,View的宽度为max(mMinWidth, mBackground.getMinimumWidth())。再看DrawablegetMinimumWidth方法

    public int getMinimumWidth() {
     final int intrinsicWidth = getIntrinsicWidth();
     return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    

    getMinimumWidth返回的就是Drawable的原始宽度(前提是这个Drawable有原始宽度),否则就返回0。ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸)。

    • 从getDefaultSize方法的实现看,在非UNSPECIFIED情况下,View的宽/高由SpecSize决定。

    • 直接继承View的自定义控件需要重写onMeasure方法,且要设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。这是因为View在布局中使用wrap_content,那它的SpecMode是AT_MOST模式,它的宽/高等于SpecSize,根据之前的普通View的MeasureSpec的创建规则可知,这种情况下View的宽/高就是父容器当前剩余的空间大小,与match_parent表现一致。解决方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    	int heightSpecsize = MeasureSpec.getsize(heightMeasureSpec);
    	if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth, mHeight);
        }else if(widthSpecMode == MeasureSpec. AT_MOST){
            setMeasuredDimension(mWidth, heightSpecSize);
    	}else if(heightSpecMode == MeasureSpec.AT MOST) {
    		setMeasuredDimension(widthSpecsize, mHeight);
    	}
    }
    

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

    • 对于非wrap_content情况,我们沿用系统的测量值即可。
  • ViewGroup的measure过程

    • ViewGroup除了完成自己的measure过程以外,还会遍历调用子元素的measure方法,各个子元素再递归去执行这个过程。

    • 与View不同的是,ViewGroup是抽象类,没有重写View的onMeasure方法,而是提供一个叫measureChildren的方法:

      protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec){
      	final int size = mChildrenCount;
          final View[] children = mChildren;
          for (int i = 0; i < size; ++i) {
              final View child = children[i];
              if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                  measureChild(child, widthMeasureSpec, heightMeasureSpec);
              }
          }
      }
      

      再看measureChild方法:

      protected void measureChild(View child, int parentWidthMeasureSpec,
                  int parentHeightMeasureSpec) {
          final LayoutParams lp = child.getLayoutParams();
          
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight, lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom, lp.height);
      
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      

      measureChild方法做的就是抽出子元素的LayoutParams,然后通过getChildMeasureSpec创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量。

    • ViewGroup是抽象类,其测量过程的onMeasure交由各子类去实现,比如LinearLayout、RelativeLayout等。之所以ViewGroup不像View一样统一onMeasure方法,是因为ViewGroup的子类的布局特性差异过大。

  • 注意:在极端情况下,系统可能需要多次measure才能确认最终的测量宽/高,在onMeasure中获取测量宽/高可能不准确。因此,在onLayout方法中取获取View的测量宽/高或者最终宽/高。


  • 如果我们想在Activity启动时获取某个View的宽/高,实际上在onCreat、onStart、onResume中均无法正确获取(未完成测量时,获取为0)。这是因为View的measure过程和Activity的生命周期方法不是同步执行的。解决方法有四个:

    • 1.onWindowFocusChanged:View已经初始化完成,宽/高已经准备完成。注意该方法会被多次调用,当Activity的窗口得到焦点和失去焦点时均会被调用。频繁的进行onResume和onPause,也会被调用。

      @Override
      public void onWindowFocusChanged(boolean hasFocus) {
          super.onWindowFocusChanged(hasFocus);
          if(hasFocus){
              int width = view.getMeasuredWidth();
              int height = view.getMeasuredHeight();
          }
      }
      
    • 2.view.post(runnable):通过post将一个runnable投递到消息队列尾部,然后等待Looper调用此runnable的时候,View已经初始化好了。

      protected void onStart() {
          super.onStart();
          view.post(new Runnable() {
              @Override
              public void run() {
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          });
      }
      
    • 3.ViewTreeObserver:ViewTreeObserver的众多回调可以完成这个功能,比如OnGlobalLayoutListener接口,当View树的状态放生改变或者View树内部的View的可见性发生改变时,OnGlobalLayoutListener方法会被调用。

          protected void onStart() {
              super.onStart();
              ViewTreeObserver observer = view.getViewTreeObserver();
              observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                  @Override
                  public void onGlobalLayout() {
                      view.getViewTreeObserver().removeOnDrawListener(this);
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                  }
              });
          }
      
    • 4.view.measure(int widthMeasureSpec,int heightMeasureSpec):通过手动对View进行measure来得到View的宽高。比较复杂。

  • 手动对View进行measure来得到View的宽高,要根据View的LayoutParams区分:

    • match_parent:无法测量。构造此种MeasureSpec需要知道parentSize,无法获取。

    • 具体数值(dp/px):宽高都是100px情况举例:

      int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
      int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
      view.measure(widthMeasureSpec,heightMeasureSpec)
      
    • wrap_content

      int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
      int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
      view.measure(widthMeasureSpec,heightMeasureSpec)
      

      注意:(1<<30)-1,View尺寸使用30位二进制表示,最大是30个1,2^30-1,即(1<<30)-1

1.3.2 layout过程

  • 当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中,onLayout方法又会被调用。layout方法确定View本身的位置,onLayout确定所有子元素的位置:

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
    
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
    
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
    
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
    
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
    
        final boolean wasLayoutValid = isLayoutValid();
    
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated
                // and thus the safest action is to clear focus here.
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }
    
        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }
    

    首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值。View 的四个顶点确定后,View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,onLayout 的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。

  • View的getMeasureWidth(getMeasureHeight)和getWidth(getHeight)的区别:

    public final int getWidth() {
        return mRight - mLeft;
    }
    
    public final int getHeight() {
        return mBottom - mTop;
    }
    

    在View的默认实现中, View的测量宽/高最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程。

  • 特殊情况1:(重写layout)

    public void layout(int l,int t,int ,int b){
    	super.layout(l,t,r + 100,b + 100);
    }
    

    上述代码会导致在View的最终宽/高总是比测量宽/高大100px,但没有实际意义。

  • 特殊情况2:(多次measure)

    过程中可能View的最终宽/高和测量宽/高不一致,但是最终会一致。

1.3.3 draw过程

  • draw的作用是将View绘制到屏幕上。

  • 过程:

    • 1.绘制背景:background.draw(canvas)
    • 2.绘制自己:onDraw
    • 3.绘制children :dispatchDraw
    • 4.绘制装饰:onDrawScrollBars
  • View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw 会遍历调用所有子元素的draw方法,重复直至绘制完成。

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
    
        // Step 1, draw the background, if needed
        int saveCount;
    
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            drawAutofilledHighlight(canvas);
    
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
    
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
    
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
    
            // we're done...
            return;
        }
    ...
    
  • View 有一个特殊的方法setWillNotDraw

    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
    

    如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认View 没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。注意:VicwGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。

1.4 自定义View

1.4.1 自定义View的分类

  • 1.继承View重写onDraw方法

    主要用于实现一些不规则的效果。预期效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。采用这种方式需要自己支持wrap_content,padding也需要自己处理。

  • 2.继承ViewGroup派生特殊的Layout

    主要用于实现自定义布局。需要合适地处理ViewGroup的测量、布局这两个过程;同时处理子元素的测量和布局过程。

  • 3.继承特定的View(比如TextView)

    一般用于扩展某种已有的Viewdev功能。不需要自己支持wrap_content和padding 等。

  • 4.继承特定的ViewGroup

    不需要自己处理ViewGroup的测量和布局这两个过程。与2的区别:方法2更接近View的底层。

1.4.2 自定义View须知

  • 让View支持wrap_content

    直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界布局中使用wrap_content时就无法达到预期效果。

  • 让View支持padding

    直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。直接继承ViewGroup的控件要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

  • 不要在View中使用Handler

    View内部本身就提供了post系列的方法,可以替代handler的作用。

  • View中如果有线程或者动画,需要及时停止

    onDetachedFromWindow是停止线程或者动画的好时机。当包含此View的Activity退出或者当前View被remove时,View 的onDetachedFromWindow方法会被调用;当包含View的Activity启动时,View 的onAttachedToWindow方法会被调用。View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。

  • View带有滑动嵌套情形时,需要处理好滑动冲突

    如果有滑动冲突,要合适地处理滑动冲突,否则将会严重影响View的效果。

1.4.3 添加自定义属性

  • 1.在values目录下面创建自定义属性的XML,比如atrs.xml。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="CircleView">
            <attr name="circle_color" format="color"/>
        </declare-styleable>
    </resources>
    

    除了color,还可以指定reference(资源id)、dimension(尺寸)、string、interger、boolean等。

  • 2.在View的构造函数中解析自定义属性的值并处理。

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        //预加载自定义属性集合CircleView
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
        //解析CircleView属性集合中的circle_color属性,并设置默认颜色值
        mColor=typedArray.getColor(R.styleable.CircleView_circle_color,Color.RED);
        //实现资源
        typedArray.recycle();
        init();
    }
    
  • 3.在布局文件中使用自定义属性。

    <com.virtual.testview.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        app:circle_color="#F00A0A"
        android:background="#000000"/>
    

    注意:使用自定义属性,必须在布局文件中添加schemas声明: xmlns:app="http://schemas.android.com/apk/res-auto"。app是自定义前缀,可以更换其他名字

1.5 参考资料

  • Android开发艺术探索
发布了64 篇原创文章 · 获赞 65 · 访问量 8813

猜你喜欢

转载自blog.csdn.net/qq_33334951/article/details/103236603