View的绘制流程

作者: 老衲法号能吃 | 来源:发表于2016-10-09 22:54 被阅读400次

    View的绘制流程主要分为散步,measure,layout以及draw,接下来,我们就从源码角度分析这个步骤。

    测量(measure)

    测量是绘制的第一步,用来决定View或者ViewGroup的测量宽高,这里讲View和ViewGroup分别讨论下

    1.View的测量过程

    概述:View的测量过程相对简单,直接可以通过onMeasure方法完成测量工作。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
      setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    首先通过getSuggestedMinimumHeight获取View的最小高度(宽度),如果没设置背景则返回minHeight,否则返回背景的原始高度和minHeight的较大值

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

    其次,计算出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;
     }
    

    2.ViewGroup的测量过程

    概述:ViewGroup的测量工作分为两步,完成自己的测量工作,遍历子View并完成他们的测量工作,因此对应的方法有两个onMeasuremeasureChildren

    measureChildren

    遍历测量每一个子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];        
        //View的状态不能是GONE
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {            
          measureChild(child, widthMeasureSpec, heightMeasureSpec);        
        }
      }
    }
    

    每一个子View调用自己的measure方法

    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);
    

    }

    onMeasure

    测量自己的过程需要根据不同的ViewGroup自己实现,相对简单的ViewGroup有FrameLayout和LinearLayout,但是FrameLayout的重叠属性导致分析起来不太清晰,这里拿LinearLayout举个例子。

    区分LinearLayout的布局类型

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   
      if (mOrientation == VERTICAL) { //垂直布局       
        measureVertical(widthMeasureSpec, heightMeasureSpec);    
      } else { //水平布局
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);    
      }
    }
    

    拿垂直方向距离,计算高度和宽度

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {    
      final int count = getVirtualChildCount();
      final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
      final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
      for (int i = 0; i < count; ++i) {    
        final View child = getVirtualChildAt(i);    
        //View为null则直接跳过    
        if (child == null) {    
          mTotalLength += measureNullChild(i);        
          continue;    
        }
         //View为GONE也直接跳过 
        if (child.getVisibility() == View.GONE) { 
          i += getChildrenSkipCount(child, i);   
          continue;
        }
        //将divider的高度算到最终高度里
        if (hasDividerBeforeChildAt(i)) {    
          mTotalLength += mDividerHeight;
        }
        //如果通过权重来分配空间,则根据权重计算mTotalLength
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        totalWeight += lp.weight;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {   
          ...
        } else {
          measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
          final int childHeight = child.getMeasuredHeight();        
          final int totalLength = mTotalLength;
          mTotalLength = Math.max(totalLength, totalLength + childHeight + 
            lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
        }
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
      //计算LinearLayout的宽度
      ...
      setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), 
        heightSizeAndState);
    }
    

    measure过程完成后,我们就可以获取到View的测量宽高,但是,这还不是View的最终宽高,最终宽高是在下面layout中完成的。

    布局(layout)

    布局用来确定View或者ViewGroup的位置,如果是ViewGroup的话,与measure一样会遍历所有的子元素并调用layout方法。

    1.View的layout过程

    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) {    
        //调用View的onLayout方法将View放置到指定位置。
        onLayout(changed, l, t, r, b);    
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;    
        ListenerInfo li = mListenerInfo;    
        //执行onLayoutChangeListener
        if (li != null && li.mOnLayoutChangeListeners != null) {        
          ...   
          for (int i = 0; i < numListeners; ++i) {            
            listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);        
          }    
        }
      }
    }
    

    获取四个顶点的位置

    protected boolean setFrame(int left, int top, int right, int bottom) {      
     ...
     if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {             
        ...    
        //计算新的宽高
        int oldWidth = mRight - mLeft;        
        int oldHeight = mBottom - mTop;        
        int newWidth = right - left;        
        int newHeight = bottom - top;    
        //View的大小是否变化    
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        //刷新
        invalidate(sizeChanged);
        //设置新的四个顶点的位置
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        ...
    }
    

    2.ViewGroup的layout过程

    这里依然以LinearLayout为例,首先区分线性布局类型

    protected void onLayout(boolean changed, int l, int t, int r, int b) {    
      if (mOrientation == VERTICAL) {        
        layoutVertical(l, t, r, b);    
      } else {        
        layoutHorizontal(l, t, r, b);    
      }
    }
    

    计算自身的位置以及遍历子View并计算他们的位置

    void layoutVertical(int left, int top, int right, int bottom) {    
      final int paddingLeft = mPaddingLeft;    
      int childTop;    
      int childLeft;        
      // 计算View右侧的结束位置
      final int width = right - left;    
      int childRight = width - mPaddingRight;        
      // 计算留给子View的剩余空间
      int childSpace = width - paddingLeft - mPaddingRight;
      //计算子View个数
      final int count = getVirtualChildCount();
      //根据Gravity计算childTop
      ...
      for (int i = 0; i < count; i++) {    
        final View child = getVirtualChildAt(i);    
        if (child == null) {        
          childTop += measureNullChild(i);    
        } else if (child.getVisibility() != GONE) {
          //计算子View的测量宽高
          final int childWidth = child.getMeasuredWidth();
          final int childHeight = child.getMeasuredHeight();
          final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
          //计算childLeft
          ...
          //如果有divider,那么childTop要加上它。
          if (hasDividerBeforeChildAt(i)) {    
            childTop += mDividerHeight;
          }
    
          childTop += lp.topMargin;
          //调用子View的layout方法。
          setChildFrame(child, childLeft, childTop + getLocationOffset(child),        childWidth, childHeight);    
          childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
        }
    }
    

    绘制(draw)

    绘制过程比较简单,主要6步

    public void draw(Canvas canvas) {
      ...
      //画背景
      if (!dirtyOpaque) {    
        drawBackground(canvas);
      }
      //绘制自己的内容
      if (!dirtyOpaque) onDraw(canvas);
      //绘制子View(如果有的话。)
      dispatchDraw(canvas);
      ...
      //绘制前景和滚动条
      onDrawForeground(canvas);
    
    }
    

    获取View宽高的方法

    1.onWindowFocusChanged

    该方法的注释:该方法在得到或者失去焦点时调用,这是确定activity是否对用户可见的指示器。不过该方法随着focus的得到和失去会多次调用。

    2.ViewTreeObserver

    可以通过ViewTreeObserver的接口回调完成View宽高的获取

    3.View.post

    通过post将一个runnable扔到消息队列的末尾,然后调用

    测量宽高和真实宽高的区别

    测量宽高和真实宽高基本一样,只不过测量宽高是在measure时期确定的,而真实宽高是在layout时期决定的,两者的确定时期不同。日常开发过程中,我们可以认为是测量宽高就是真实宽高。但不绝对,我们可以在View的Layout方法中手动修改left,top,right,bottom的值导致他们不一样。

    相关文章

      网友评论

      本文标题:View的绘制流程

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