View的工作原理

View的工作原理

ViewRoot和DecorView

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

View的绘制流程是从ViewRoot的performTranversals开始的,经过measure -> layout -> draw,才能将一个View绘制出来

如何得到DecorView的content

findViewById(android.R.id.content);

如何得到setContentView的view呢

content.getChildAt(0);

View工作流程

measure

  • 原始View
    • 完成自己的测量
  • ViewGroup
    • 完成自己的测量
    • 遍历去调用所有子元素的measure方法

View的measure过程

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //Z-setMeasuredDimension会设置View宽高的测量值
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getDefaultSize

getDefaultSize在AT_MOST和EXACTLY时,返回specSize (View测量后的大小,[但不是最终大小,最终大小是在layout阶段确定的])
UNSPECIFIED一般用于系统内部的测量过程,这种情况下,View的大小为getDefaultSize的第一个参数size(即宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这连个方法的返回值)

结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则放在布局中使用wrap_content就相当于match_parent

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;
}
getSuggestedMinimumWidth和getSuggestedMinimumHeight
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
  • 如果没有设置背景,那么View的宽度为mMinWidth(android:minWidth,默认为0)
  • 如果有指定背景,那么为mMinWidth和背景.getMinimumWidth中的最大值
Drawable的MinimumWidth

返回背景的最小宽度

 public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
  • 如果Drawable没有原始大小,返回0
  • 如果Drawable有原始大小,则返回原始大小
    • 原始大小:比如ShapeDrawable无原始大小,而BitmapDrawable有原始大小

ViewGroup的measure过程

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对每一个子元素进行measure: 取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着讲MeasureSpec直接传递给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);
}

LinearLayout为例的measure过程

其他的ViewGroup的实现方式是不同的,因为要实现不同的布局。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

measureVertial -> 遍历子元素 -> measureChildBeforeLayout -> measure -> mTotalLength += mPaddingTop + mPaddingBottom + child.height -> 测量自己 -> setMeasuredDimension

measureVertial
  • 如果高度是match_parent或者具体数据,那么测量过程与View一致(即高度为specSize)
  • 如果高度为wrap_content,那么高度为所有子元素所占用的高度总和+竖直方向padding的总和(不超过父容器)
measureHorizontal
  • 水平方向的测量过程遵循View的测量过程。

Activity获取View的宽高

#### onWindowFocusChanged ####
View已初始化韩币,宽高已准备好
但onWindowFocusChanged会被调用好多次
当Activity继续执行和暂停时,onWindowFocusChanged均会调用

view.post(runnable)

将一个runnable投递到消息队列的尾部,然后等Looper调用此runnable的时候,View也已经初始化好了

ViewTreeObserver

伴随着View树的状态改变等,onGlobalLayout会被调用多次(所以,一般被调用后就remove)

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        //view.getViewTreeObserver().removeOnGlobalLayoutListener(this); API16及以上
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
});
view.measure(int widthMeasureSpec,int heightMeasureSpec)

比较复杂,需分情况根据View的LayoutParams来分

Layout过程

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); //Z-setFrame 设置View的四个顶点的位置,(确定该view在父容器的位置)

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b); //Z-调用onLayout方法(确定所有子元素的位置)
        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;
}

View和ViewGroup都没有实现onLayout方法,来看看LinearLayout的OnLayout

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight;

    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight;

    final int count = getVirtualChildCount();

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
       case Gravity.CENTER_VERTICAL:
           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }

    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);
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            //Z-setChildFrame 为子元素指定对应的位置(调用子元素的layout) childTop会之间增大,后面的子元素会被放置在靠下的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

这样就形成了传递
ViewGroupA.layout -> onLayout -> 循环遍历子元素 -> ViewGroupB.layout -> onLayout -> 循环遍历子元素 -> ViewA.layout

setChildFrame设置子元素的layout

private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height); //width/height 子元素的宽高 left,top是子元素的四个顶点的位置
}
View的getMeasuredWidth和getWidth这两个方法有什么区别

先来看看getwidth
public final int getWidth() {
return mRight - mLeft;
}

getMeasuredWidth
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}

在默认的实现中,View的测量宽/高和最终的宽/高是相等的,只不过

  • 测量宽高形成与View的measure过程 (稍早)
  • 最终宽高形成与View的layout过程

在日常的开发中,我们可以认为View的测量宽高就等于最终宽高,但是的确在某些特殊的情况下会导致两者不一致。

  • 如果重写了View的layout方法,修改了其super.layout传入的参数,那么测量宽高就会与最终宽高不相等了
  • 如果View需要多次measure才能确定自己测量的宽高,在前几次的测量过程中,其得出的测量宽高可能和最终宽高不一致,但最终来讲,测量宽高还是和最终宽高相同。
应用场景

getMeasuredWidth:在自定义view重写onLayout时、在我们用layoutinflater动态加载view后想获得view的原始宽度时。
getWidth:一般在view已经布局后呈现出来了,想获取宽度时

Draw过程

view的绘制步骤

  • 绘制背景 background.draw(canvas)
  • 绘制自己(onDraw)
  • 绘制children (dispatchDraw)
    • View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所用子元素的draw方法,如此draw事件就传递下去了。
  • 绘制装饰(onDrawForeground(onDrawScrollBar))
setWillNoteDraw

默认View不开启,ViewGroup开启
如果设为true,该View将不具备绘制功能,然后系统会进行后续的(不用绘制的)优化

自定义View

须知
  • 让View支持wrap_content
  • 如果有必要,让你的View支持padding
  • 尽量不要在View中使用Handler
  • View中如果有线程或者动画,需要及时停止(退出或remove或变成不可见时)
    • View#onDetachedFromWindow: 包含此View的Activity退出或者当前View被remove时
    • View#onAttachedToWindow: 当包含此View的Activity启动时
  • View带有滑动嵌套时,需要处理好滑动冲突
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

氦客

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值