view 绘制流程

performTravseral.png
  • DecorView 是界面上的顶层view. ViewRoot是连接WindowManager和DecorView的纽带.view的绘制流程从ViewRoot的perfromTraversals开始.
  • ViewRoot作为顶级View,通常包括一个竖直方向的LinerLayout.这个LinerLayout分两部分.上边是标题栏.就是屏幕显示时间的那一行,下边是内容栏.id为content,我们平时设置的setContentView(),就是在想contentView里添加子类.通过ViewGroup content =(ViewGroup)findViewById(android.R.id.content);拿到contentView. content.getChildAt(0);拿到我们设置的View;
  • MeasureSpace 用一个32位的int代表测量的数据和类型,高二位代表SpecMode,表示测量模式,低30位 SpecSize表示测量大小.
  • MeasureSpace的三种模式
    • UNSPECIFIED:父容器不做限制,通常用于系统内部.
    • EXACTLY 指定大小,view此时的大小就是SpecSize的值.它对应于LayoutParams中的march_parent和具体指定的值(多少dp,sp).
    • At_MOST 父容器指定一个可用大小即SpecSize,View不能大于这个值.对于wrap_content.
  • MeasureSpace 决定view的宽高,但MeasureSpace由父类和view自己的layoutParams共同决定.
  • DecorView的MeasureSpace决定方式
3.png

2.png

-接着看vp如何测设计子类的MeasureSpace

 //parentWidthMeasureSpec,parentHeightMeasureSpec 是vp自己的measureSpace宽高值
    //widthUsed,heightUsed 是被vp额外用掉的宽高值
    protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {

        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        
        //viewgroup自己的measureSpace,第二个参数是vp所使用的值
        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);
    }
    //得到子类的MeasureSpace 通过父类的specMode和子类Layoutparams的组合得到子类最终的MeasureSpace
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        vp的MeasureSpec
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        
        //vp自己的尺寸减去自己的padding.margin等 最后得到子类能用的最大尺寸
        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: //vp是精确模式
            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);
     }

-最终的匹配结果如下,parentSize值最终父类可用大小,既getChildMeasureSpec中的 size

4.png

-measure 过程

-view 的measures过程

view 的measure方法是final,既子类无法重写该方法,在measure 中又调用了onMeasure方法,只需要看onMeasure方法就可以.

//widthMeasureSpec,heightMeasureSpec为父类vp传过来的子类的MeasureSpace 宽高值
//getSuggestedMinimumWidth()  是建议的最小宽度,一般为view背景的大小和属性android:minWidth大小的最大值.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//子类可以重写这个方法来决定view的measure大小.但是必须调用setMeasuredDimension()方法,不然会抛出异常.
setMeasuredDimension()方法会设置view宽高的测量值.

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;
}
//UNSPECIFIED的情况可以忽略.看剩下两种情况.所以 getDefaultSize的取值其实就是vp传过来的measureSpace的specSize的值.
  • 经过以上代码我们得出结论:直接继承View的自定义空间要重写onMeasure方法并设置wrap_content时自身大小,不然设置wrap_content和设置march_parent的效果一样.
  • 因为view 设置为wrap_content, 对应的MeasureSpec 为SpecMode=AT_MOST,SpecSize=parentSize,在onMeasure中就会把父类可用空间的值设置给view.解决办法如下
5.png

-ViewGroup 的measures过程

  • viewgroup 出来测量自己,还要遍历子veiw测量大小.vp通过measureChildren方法来遍历测量子类大小

    //Gone的子view不会被测量
     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);
            }
        }
    }
    //这个方法和之前的 measureChildWithMarging 方法差不多,只是少了widthUsed,heighUsed参数
     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);
     }
    
  • viewgroup并没有实现具体测量过程,因为他是一个抽象类,需要子类例如LinerLayout等自行实现测量过程.

  • 四种方法拿到view的宽高

    1. Activity/View#onWindowFocusChanged,这个方法的含义标识窗口的交点变化时回调,此时view已经初始化完毕,宽高已经准备好了.此方法会回调多次
    public void onWindowFocusChanged(boolean hasFocus){
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width=view.getMeasuredWidth();
            int hei=view.getMeasuredHeigh();
        }
    }
    
    1. view.post(runnable) 发送一个消息到消息队列的尾部,等Lopper调用此消息时,view已经初始化完成

      view.post(new Runnable{

      @Override
      public void run(){
           int width=view.getMeasuredWidth();
           int hei=view.getMeasuredHeigh();
      }
      

      });

    2. ViewTreeObserver#onGlobalLayoutListener,当view树的状态或者view树的子view可见性发生改变时会回调,方法会被回调多次

      ViewTreeObserver observer=view.getViewTreeObserver();
      observer.addonGlobalLayoutListener(new onGlobalLayoutListener(){

      publci void onGlobalLayout(){
          view.getViewTreeObserver().removeGlobalLayoutListener(this);//先移除监听
          int width=view.getMeasuredWidth();
          int hei=view.getMeasuredHeigh();
      }
      

      });

    3. view.measure(int,int);需要根据view 的layoutParam 来区分

      march_parent :此状态无法得到结果,因为此时父view的测量宽高也不知道
      具体数值 :比如宽高为100

       int width =MeasureSpec.makeMeasureSpec(100,MeasursSpec.EACTLY)
       int heigh =MeasureSpec.makeMeasureSpec(100,MeasursSpec.EACTLY)
       view.measurd(width,heigh);
      

      wrap_content

       int width =MeasureSpec.makeMeasureSpec((1<<30)-1,MeasursSpec.EACTLY)
       int heigh =MeasureSpec.makeMeasureSpec((1<<30)-1,MeasursSpec.EACTLY)
       view.measurd(width,heigh);
      

      View 的最大尺寸为30位二进制,既 2的30次方减1 既(1<<30)-1,在最大化模式下,我们用view理论上的最大值去构造MeasureSpec是合理的.

-layout 过程

  • layout 方法确定view本身的位置,在onLayout方法则会确定所有子元素的位置.

  • viewGroup 在位置被确定后,会在onLayout中便利所有子类比调用其layout方法,在layout方法中onLayout方法又被调用.

  • view 的layout方法中, 通过setFrame(l,t,r,b)来设定view的四个定点的位置.父容器通过onLayout方法来确定子view的位置.

  • view和viewgroup均没实现onLayout方法,需要看具体的layout.

  • view 的layout过程

      public void layout(int l, int t, int r, int b) {
          //如果需要在layout前进行一次measure
          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;
          //主要部分,设置view的左上右下位置.
          boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
          
          //通知监听器,界面layout发生变化
          if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
              onLayout(changed, l, t, r, b); //空实现
              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);
                  }
              }
          }
    
          mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
          mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
      }
    

-vieGroup layout过程

public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b); //其实还是调用了父类,最终调用view 的layout
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
}
  • getwidth getMeasuredWidth 区别

  • getMeasuredWith 形成于measure过程.getWidth 形成于layout过程.

  • 一般情况两者是相同的.除非如下情况

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

-draw 过程

1、对View的背景进行绘制
2、保存当前的图层信息(可跳过)
3、绘制View的内容
4、对View的子View进行绘制(如果有子View)
5、绘制View的褪色的边缘,类似于阴影效果(可跳过)
6、绘制View的装饰(例如:滚动条)

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);
 
  // 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);
 
  // we're done...
  return;
 }
 ...
}


分布讲解
1绘制背景
private void drawBackground(Canvas canvas) {

     //mBackground是该View的背景参数,比如背景颜色,没有背景就不绘制
     final Drawable background = mBackground;
     if (background == null) {
      return;
     }
     
     //根据View四个布局参数来确定背景的边界 mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
     setBackgroundBounds();
     
     ...
     
     //获取当前View的mScrollX和mScrollY值
     final int scrollX = mScrollX;
     final int scrollY = mScrollY;
     if ((scrollX | scrollY) == 0) {
      background.draw(canvas);
     } else {
      //如果scrollX和scrollY有值,则对canvas的坐标进行偏移,再绘制背景
      canvas.translate(scrollX, scrollY);
      background.draw(canvas);
      canvas.translate(-scrollX, -scrollY);
     }
}
    3.绘制子View,view中这个方法为空,viewgroup实现了这个方法
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
            
        for (int i = 0; i < childrenCount; i++) { 
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime); //这句是重点
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);//这句是重点
            }
        }
            //省略...
    }

    drawChild(canvas, transientChild, drawingTime)的实现,调用了子view的绘制方法
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
         return child.draw(canvas, this, drawingTime);
    }
  • view 的draw(Canvas canvas, ViewGroup parent, long drawingTime)
  • 我们主要来看核心部分,首先判断是否已经有缓存,即之前是否已经绘制过一次了,如果没有,则会调用draw(canvas)方法,开始正常的绘制,即上面所说的六个步骤,否则利用缓存来显示。
  • 这一步也可以归纳为ViewGroup绘制过程,它对子View进行了绘制,而子View又会调用自身的draw方法来绘制自身,这样不断遍历子View及子View的不断对自身的绘制,从而使得View树完成绘制
  •   Skip 6 绘制装饰,指View除了背景、内容、子View的其余部分,例如滚动条等,我们看View#onDrawForeground:
      public void onDrawForeground(Canvas canvas) {
       onDrawScrollIndicators(canvas);
       onDrawScrollBars(canvas);
       
       final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
       if (foreground != null) {
        if (mForegroundInfo.mBoundsChanged) {
         mForegroundInfo.mBoundsChanged = false;
         final Rect selfBounds = mForegroundInfo.mSelfBounds;
         final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
       
         if (mForegroundInfo.mInsidePadding) {
          selfBounds.set(0, 0, getWidth(), getHeight());
         } else {
          selfBounds.set(getPaddingLeft(), getPaddingTop(),
            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
         }
       
         final int ld = getLayoutDirection();
         Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
           foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
         foreground.setBounds(overlayBounds);
        }
       
        foreground.draw(canvas);
       }
      }
    

自定义view,宽高写为wrap_content时,如果不处理,和写成 march_parent是一样的,处理规则如下,mWidth,mHeigh为自定义宽高

11.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容