android view源码解析,Android开发——View绘制过程源码解析(二)

0.前言

View的绘制流程从ViewRoot的performTraversals开始,经过measure,layout,draw三个流程,之后就可以在屏幕上看到View了。上一篇已经介绍了View和ViewGroup的measure的源码解析过程,本篇介绍measure后如何获得View的宽和高,以及layout和draw的过程。

1.  获取Measure后的宽高

Meaure完成以后,可以通过getMeasuredWidth/Height()来获得View的测量宽高。但是在onMeasure()中拿到的宽高不像在onLayout()中拿到的那么可靠。因为有时View可能需要多次measure,虽然最终测量和最终结果相同,但是前几次measure可能会得到不可靠的宽高。

因为measure的过程和Activity的生命周期没有任何关系,无法确定在哪个生命周期执行完毕以后View的measure过程一定完成。因此在Activity的生命周期里可能无法获得测量宽高。可以尝试如下三种方法获取view的测量宽高。

1.1  焦点相关回调//重写Activity的onWindowFocusChanged方法

//该方法当Activity得到/失去焦点便会回调,表示View已经初始化完毕,可以保证Measure完成

public void onWindowFocusChanged(boolean hasFocus) {

super.onWindowFocusChanged(hasFocus);

if (hasFocus) {

int width = tv.getMeasuredWidth();

int height = tv.getMeasuredHeight();

}

}

1.2 消息队列

//将Runnable置于消息队列尾部,延迟获取测量宽高的时间,保证View初始化完毕

@Override

protected void onStart() {

super.onStart();

view.post(new Runnable() {

@Override

public void run() {

int width = view.getMeasuredWidth();

int height = view.getMeasuredHeight();

}

});

}

1.3  监听view树

//当View树状态发生改变时回调onGlobalLayout

@Override

protected void onStart() {

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

@Override

public void onGlobalLayout() {

//因为会调用多次,所以拿到测量结果后要remove掉监听

view.getViewTreeObserver().removeOnGlobalLayoutListener(this);

int width = view.getMeasuredWidth();

int height = view.getMeasuredHeight();

}

});

}

2. Layout过程

ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法,layout方法是确定本身View的位置,而onLayout方法是确定所有子元素的位置。layout方法如下所示://四个参数分别代表相对于父视图左上右下四个坐标值

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

public void layout(int l, int t, int r, int b) {

int oldL = mLeft;

int oldT = mTop;

int oldB = mBottom;

int oldR = mRight;

boolean changed = setFrame(l, t, r, b);

if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {

if (ViewDebug.TRACE_HIERARCHY) {

ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);

}

onLayout(changed, l, t, r, b);

mPrivateFlags &= ~LAYOUT_REQUIRED;

if (mOnLayoutChangeListeners != null) {

ArrayList listenersCopy =

(ArrayList) 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 &= ~FORCE_LAYOUT;

}

在layout()方法中,第8行首先会调用setFrame()方法来判断视图的大小是否发生过变化,避免未发生变化时进行重绘操作。

13行会调用onLayout()方法,用于父容器确定子元素的位置,这是一个空实现,View和ViewGrope均没有实现它。

我们尝试自定义一个布局,借此来理解onLayout()的过程。代码如下所示:public class MyLayout extends ViewGroup {

public MyLayout (Context context, AttributeSet attrs) {

super(context, attrs);

}

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//调用measureChild()来测量出子元素的大小

measureChild(getChildAt(0), widthMeasureSpec, heightMeasureSpec);

}

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

//子元素可以在xml中任意添加一个控件

//根据测量结果布置子元素在MyLayout中的位置

childView.layout(0, 0, getChildAt(0).getMeasuredWidth(),getChildAt(0).getMeasuredHeight());

}

}

onLayout()过程结束后,我们就可以调用getWidth/Height来获取View的最终宽高了。之前也提到过,getMeasureWidth方法在measure过程结束后取到的是测量宽高,两者几乎在任何情况下都是相同的。但是也有非一般的情况,原因就是我们可以重写onLayout,并且对measure测量的结果"置之不理"。

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

//这样getWidth得到的值就是100减0,虽然这么做毫无意义

getChildAt(0).layout(0, 0, 100, 100);

}

3. Draw过程

measure和layout的过程都结束后,就进入到对View进行绘制的draw过程了。

ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。代码如下所示:public void draw(Canvas canvas) {

//…

final int privateFlags = mPrivateFlags;

final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&

(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;

// 第一步,对背景进行绘制

int saveCount;

if (!dirtyOpaque) {

//得到android:background属性设置的图片或颜色

final Drawable background = mBGDrawable;

if (background != null) {

final int scrollX = mScrollX;

final int scrollY = mScrollY;

if (mBackgroundSizeChanged) {

//根据layout的View位置结果确定背景绘制区域

background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);

mBackgroundSizeChanged = false;

}

if ((scrollX | scrollY) == 0) {

//调用Drawable的draw()方法来完成背景的绘制工作

background.draw(canvas);

} else {

canvas.translate(scrollX, scrollY);

background.draw(canvas);

canvas.translate(-scrollX, -scrollY);

}

}

}

final int viewFlags = mViewFlags;

boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;

boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

if (!verticalEdges && !horizontalEdges) {

//第三步,对内容进行绘制,onDraw为空实现,需要子类实现

if (!dirtyOpaque) onDraw(canvas);

//第四步,ViewGroup的dispatchDraw()方法遍历调用所有子元素的draw()

dispatchDraw(canvas);

//第六步,显示控件的滚动条

onDrawScrollBars(canvas);

// we're done...

return;

}

}

从源码中可以看出,onDraw()是需要我们去重写实现的。

绘制主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,基本可以把它当成一块画布,在上面绘制任意的东西。

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 onDraw(Canvas),you should clear this flag.

*/

public void setWillNotDraw(boolean willNotDraw) {

setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);

}

如果你的自定义View不需要绘制出来的话,就可以设置这个方法为true,这样可以优化执行速度。

ViewGrope默认设置为true,因为ViewGrope多数都是只负责布局。如果我们继承自ViewGrope的自定义控件需要通过onDraw绘制内容时,需要手动关闭这个标记位。public class MyLayout extends LinearLayout {

public MyLayout (Context context, intposition) {

super(context);

setWillNotDraw(false);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

}

}

至此关于View绘制的过程便总结完毕。

935f1790281d88a098ff963fffc9ca22.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值