View的工作原理(三):layout与draw

一. 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) {
            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);
                }
            }
        }
    
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

    setFrame(l, t, r, b):设定View的4个顶点位置.

    onLayout(changed, l, t, r, b):ViewGroup确定其子元素的位置。View和ViewGroup都未实现该方法,普通的View无需实现,而ViewGroup的onLayout实现,则与布局类型有关。

  2. 示例:线性布局的onLayout方法实现:

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

    我们选择VERTICAL类型继续查看源码:

    void layoutVertical(int left, int top, int right, int bottom) {
        ...
        ...
        final int count = getVirtualChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
    
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
    
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                ...
                ...
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
    
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                i += getChildrenSkipCount(child, i);
            }
        }
    }
    • 可以看到,遍历子元素并调用setChildFrame()方法来指定子元素的位置。
    • setChildFrame() 将会调用子元素的layout方法:

      private void setChildFrame(View child, int left, 
              int top, int width, int height) {
          child.layout(left, top, left + width, top + height);
      }

      而子元素的layout又会调用其onLayout,从而达到递归遍历整个View树的过程。

    • setChildFrame()的参数:

      final int childWidth = child.getMeasuredWidth();
      final int childHeight = child.getMeasuredHeight();

      显然就是子View的测量宽高。

    • childTop会随着子元素的遍历而主键增大,显然,后添加子View将会出现在更靠下的位置,这也是竖直方向’LinearLayout’的特性。

  3. 再论View的measureWidth/measureHeightwidth/height: 基本无差

    1. 首先看看getWidthgetHieght:

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

      显然,getWidthgetHieght必须等到layout完成之后,才能获取到正确的值。

    2. 前文总结过,测量宽高形成于View的measure过程,而最终宽高形成于layout过程。所以日常开发中,可以认为它们没有差别。
    3. 凡是总有例外,比如重写View的layout方法(皮一下很开心):

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

      于是最终宽高就会比测量宽高多了10个像素。

二. draw过程

  1. 源码太长,这里贴下关键部分:

    //View的draw方法
    /*
     * 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)
     */
     ...
     ...
    
    // 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;
    }
    ...
    ...
  2. 上面源码中的注释已经讲的很清楚了,基本的绘制流程是这样的:

    1. 绘制背景
    2. 如有必要,保存canvaslayers准备渐变。
    3. 绘制View的内容
    4. 绘制children
    5. 如有必要,绘制渐变边缘并恢复layers
    6. 绘制装饰,比如scrollbars.
  3. 绘制过程的传递,通过dispatchDraw来实现的:

    // Step 4, draw the children
    dispatchDraw(canvas);

    View中这个方法是个空的实现(普通的View也没有children),而ViewGroup则提供了实现。在ViewGroup的这个方法中,会遍历调用所有子View的draw方法。

  4. 特殊的方法:setWillNotDraw

    /**
     * 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);
    }
    • 如果View不打算绘制自己,立此flag以通知系统来做更进一步的优化工作。
    • 普通的View默认关闭此flag,但ViewGroup则默认启用这个flag。
    • 当我们自定义一个ViewGroup类型的容器View时,如果需要通过onDraw来绘制自己时,需要手动关闭此flag:setWillNotDraw(false)

猜你喜欢

转载自blog.csdn.net/cangely/article/details/80211304
今日推荐