阅读他人的,自己进行总结的。原文章:
Android LayoutInflater原理分析,带你一步步深入了解View(一)_郭霖的专栏-CSDN博客_layoutinflater
Android视图绘制流程完全解析,带你一步步深入了解View(二)_郭霖的专栏-CSDN博客_android 视图绘制
该作者的文章,非常只好,我是想要方便记忆,所以自己总结一下。
以上4篇,原作者分为LayoutInflater的原理分析、视图的绘制流程、视图的状态及重绘和自定义view。
我们先介绍前两篇
目录
一:LayoutInflater原理分析
1、如何使用LayoutInflater
LayoutInflater.from(context).inflate(R.layout.activityMain, null);
2、例子
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</LinearLayout>
button_Layout.xml:
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" >
</Button>
MainActivity:
public class MainActivity extends Activity {
private LinearLayout mainLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainLayout = (LinearLayout) findViewById(R.id.main_layout);
LayoutInflater layoutInflater = LayoutInflater.from(this);
View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);
mainLayout.addView(buttonLayout);
}
}
效果图:
3、源码分析
我们调用的方法是LayoutInflater的inflate方法,无论调用哪个inflate最后调用的都是下面这个inflate方法。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
mConstructorArgs[0] = mContext;
View result = root;
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
}
......
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
......
rInflate(parser, root, attrs);
} else {
View temp = createViewFromTag(name, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
......
}
return result;
}
}
首先LayoutInflater使用pull解析方式来解析布局文件的,然后在方法中第22行,调用了createViewFromTag()方法。然后在其后续会调用rInflateChildren()
createViewFromTag:用于根据节点名来创建View对象,在其内部有调用createView方法(createView方法中通过反射的方式创建出View的实例并返回)
rInflateChildren:会调用rInflate方法。
rInflate:查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中。
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
......
}
同样在第25行是createViewFromTag()创建实例,然后在第28行递归调用rInflateChildren()。
inflate方法中第三个参数,我这边不介绍了,大概是会给加载的文件制定一个父布局,即root
4、问题引出
我要改变button的大小,我改成如下所示。
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="80dp"
android:text="Button" >
</Button>
300宽和80高,无任何变化。什么原因导致的,由于我们button没有父容器,他的宽高属性就会失去作用,我们改成如下所示,效果就可以更改了:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:layout_width="300dp"
android:layout_height="80dp"
android:text="Button" >
</Button>
</RelativeLayout>
那activity中setContentView我们的宽高怎么会有效呢,其实在setContentView方法中,Android会自动在最外层再嵌套一个FrameLayout。证实:
public class MainActivity extends Activity {
private LinearLayout mainLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainLayout = (LinearLayout) findViewById(R.id.main_layout);
ViewParent viewParent = mainLayout.getParent();
Log.d("TAG", "the parent of mainLayout is " + viewParent);
}
}
二、视图绘制流程
1、onMeasure()
measure:测量。
原文中说的onMeasure从ViewRoot开始,我的sdk是android-29,我这边就置换过来,是从ViewRootImpl.performTraversals()开始的;
根据原文MeasureSpec总结得出,其MeasureSpec的specMode有三种类型:
我们来看看performTraversals()方法:
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
我们只看关键代码,其中getRootMeasureSpec()方法可以证实我们上图所述。lp.width和lp.height创建ViewGroup实例时就被赋值了,接着执行了performMeasure();
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
我们可以看到执行了View.onMeasure()了;那我们继续后面的追踪。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
if (forceLayout || needsLayout) {
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
......
}
我们看到其中在第8行执行了onMeasure方法。measure方法参数接受了ViewRootImpl确认之后的高度、宽度的规格和大小.onMeasure才是我们真正去测量并且设置View的地方。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
onMeasure调用了setMeasuredDimension、getDefaultSize、getSuggestedMinimumWidth、getSuggestedMinimumHeight
getSuggestedMinimumWidth():返回视图应使用的建议最小宽度。
getSuggestedMinimumHeight():返回视图应使用的建议最小高度。
getDefaultSize():获取视图的大小
setMeasuredDimension():设定测量出的大小
这样measure的一次过程就结束了。一个界面会设计到多次measure,因为一个布局中包含多个子视图,每个视图都要经历一次measure过程。ViewGroup中定义了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);
}
}
}
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);
}
我们可以看到在measureChildren中第6行的判断,如果我们没有设置这个控件的Visibility为GONE那么我们就会走到measureChild()中,在measureChild中我们也会去获取控件的MeasureSpec,之后就跟前面的measure后的流程一样了。
来一个小例子吧。onMeasure可以重写,不用系统默认的测量方式,按照我们自己的意愿定制,如:
public class MyView extends View {
......
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(200, 200);
}
}
通过上述流程中,我们知道measure最后是到setMeasuredDimension方法中去的,这样我们定义这个视图的无论多大,都将显示200*200。需要注意的是在setMeasuredDimension调用之前,我们getMeasuredWidth、getMeasuredHeight得到的返回值会是0,调用之后才能获取视图测量出的宽高。
测量这一步我们就差不多分析完了。父视图、布局文件和视图本身共同完成,父视图提供子视图参考大小,开发人员在布局文件中指定视图大小,视图本身对最终大小进行拍板。
总结流程:
ViewRootImpl.performTraversals()
--ViewRootImpl.performMeasure()
(子视图的话 ViewGroup.measureChildren->ViewGroup.measureChild)
--View.onMeasure()
--View.Measure()
2、onLayout()
layout:布局、安排、设计
measure之后就应该layout了,我们通过观察下面ViewRootImpl中的performTraversals()方法看看:
......
if (!mStopped || mReportNextDraw) {...}
}else {....}
if (surfaceSizeChanged) {
updateBoundsSurface();
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
......
其中,我们刚才说的onMeasure就发生在第二行的if判断体中。和之前一样我的版本是android-29跟原文不太一样。这边会调用的方法是performLayout方法();就像performMeasure调用View.measure()一样,performLayout调用了View.Layout();
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
......
final View host = mView;
if (host == null) {
return;
}
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
false);
if (validLayoutRequesters != null) {
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
}
layout()方法接受四个参数,代表左上右下的坐标。我们可以看到上面传的宽高给了我们刚才measure测量出的宽高。坐标是相对于当前视图而言。
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);
......
}
}
layout方法中,把坐标信息赋值给几个变量。change这个布尔值代表是否发生过变化,来确认它是否需要重新绘制。在13行时走进了onLayout中。我们来看看onLayout:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
我们看到该方法没有实现。onLayout为了确定视图在布局中所在位置,所以应该由布局来完成。因此我们看看ViewGroup中的onLayout:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到这是一个抽象方法,那么所有子类必须重写次方法。LinearLayout、RelativeLayout等布局都实现了次方法,所以完成了那些强大布局的功能。
我们来一个例子:
SimpleLayout继承自ViewGroup:
public class SimpleLayout extends ViewGroup {
public SimpleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
以上的方法都做过介绍了,就不再赘述。
SimpleLayout的布局文件:
<com.example.viewtest.SimpleLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
/>
</com.example.viewtest.SimpleLayout>
我java代码中只想要第一个子视图,所以只拿第一个,多余的就会被舍弃掉。这边SimpleLayout包含一个ImageView。运行效果如图:
如果想要改变ImageView显示的位置,改变childView,Layout()的四个参数就好了。onLayout结束后就可以拿到getWidth和getHeight的值了。getWidth和getMeasureWidth的值好像一直相同。getWidth和getMeasureWidth有什么区别:
1.getMeasure在measure过程结束后可以拿到,getWidth要到layout过程结束拿到
2.getMeasureWidth通过setMeasureDimension()设置,getWdith通过右边坐标减左边坐标计算出的
他们的值相同的原因相信大家知道了,当然我们可以让它不相同。还是上面的代码我们改变onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, 200, 200);
}
}
这样getwidth得到的值就是200-0 = 200。不会和getMeasureWidth相同。通常情况不推荐这么写,getHeight()与getMeasureHeight()方法之间关系同上,不再赘述。
总结流程:
ViewRootImpl.performTraversals()
--ViewRootImpl.performLayout()
--View.Layout()
--(? extends ViewGroup).onLayout()
3、onDraw
测量完(measure)和布局结束(layout)就该将视图描绘(draw)出来了。这边才是真正开始对视图进行绘制。根据上面的经验我们可以很快得出在ViewRootImpl中开始。在2625行走出了onLayout的if判断体。2747行的if判断体进入performDraw()我们直接只贴相应代码:
if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
继续跟踪performDraw:
private void performDraw() {
......
try {
boolean canUseAsync = draw(fullRedrawNeeded);
....
.....
}
private boolean draw(boolean fullRedrawNeeded) {
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
......
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
.......
try{
....
mView.draw(canvas);
......
}
.......
}
原文没有这部分没有详细出来,我这边把流向给大家贴出来。主要意思原文讲清楚了:“执行并创建一个Canvas对象,然后调用View.draw()方法来执行具体的绘制”。上面的方法就不详细叙述了,都在ViewRootImpl中,有兴趣可以自己翻阅源码看看。我们来看看View.draw(),源码比较多,我把相应步骤代码贴出:
@CallSuper
public void draw(Canvas canvas) {
......
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 1. 画背景
* 2. If necessary, save the canvas' layers to prepare for fading
* 2. 如有必要,保存画布的层以准备褪色
* 3. Draw view's content
* 3. 绘制视图的内容
* 4. Draw children
* 4. 画孩子
* 5. If necessary, draw the fading edges and restore layers
* 5. 如有必要,绘制淡入淡出的边并恢复图层
* 6. Draw decorations (scrollbars for instance)
* 6. 绘制装饰(例如滚动条)
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// Step 3, draw the content
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);
.......
}
第一步:对视图进行绘制,这里会先得到一个mDrawable对象,然后根据layout过程确定的视图位置来进行背景的绘制,之后调用mDrawable的draw()方法。那这个drawable对象哪里来的呢,通过设置android:background属性设置的图片或颜色setBackgroundColor,setBackgroundResource等方法。
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mThreadedRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.hasDisplayList()) {
setBackgroundRenderNodeProperties(renderNode);
((RecordingCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
第三步:对视图的内容进行绘制,可以看到调用了onDraw()。onDraw方法是空实现,因为每个视图的内容各不相同,交给子类实现。
第四步:对当前视图的所有子视图进行绘制。调用了dispatchDraw(),它也是空实现,而ViewGroup的dispatchDraw()方法中有具体的绘制代码,代码过多,有兴趣可以翻看源码,这边不贴了。
第六步:对视图的滚动条进行绘制,大家之前以为只有ListView、ScrollView这类的有滚动条吧,其实button、textView也好,任何一个视图都有滚动条。只是我们没让他显示出来。
我们来一个例子:
MyView.java:
public class MyView extends View {
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String text = "Hello View";
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
}
布局文件xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.viewtest.MyView
android:layout_width="200dp"
android:layout_height="100dp"
/>
</LinearLayout>
效果如下图所示:
总结流程:
ViewRootImpl.performTraversals()
--ViewRootImpl.performDraw()
--ViewRootImpl.draw(boolean)
--ViewRootImpl.drawSoftware()
--View.draw(Canvas)
--(第3步中)onDraw()