Android UI的绘制流程
Posted 花花young
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android UI的绘制流程相关的知识,希望对你有一定的参考价值。
前言
对于android开发者来说,要学好自定义View就需要了解绘制流程,包含measure、layout、draw,Android的View绘制是一个自上而下的过程,本文便通过对UI的绘制流程研究来增强自身能力提高,内容不好不要见怪。
Part 1、初步了解Activity UI的形成过程
首先我们在Activity里面写上setContentView一运行就显示了视图,对于初学者都不会去考虑它是怎么来的,但面临进阶就有必要去了解一番了,ok,我们进入setContentView源码
public void setContentView(@LayoutRes int layoutResID)
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
getWindow()指代的是什么呢,通过查看attach方法可知getWindow是继承Window类PhoneWindow对象
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window)
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
......
由此,进入PhoneWindow#setContentView
@Override
public void setContentView(int layoutResID)
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null)
installDecor();
……
mLayoutInflater.inflate(layoutResID, mContentParent);
installDecor()方法生成一个DecorView,mLayoutInflater.inflate()方法可知mContentParent肯定是Activity的布局,接下来我们进入installDecor方法
private void installDecor()
if (mDecor == null)
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0)
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
if (mContentParent == null)
mContentParent = generateLayout(mDecor);
在这个方法里,通过调用generateDecor()方法生成一个DecorView对象,而generateLayout(mDecor)将生成内容布局View进入generateDecor()方法中可以看到直接new了一个DecorView,而DecorView是一个自定义FrameLayout
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
有时候可能会问为什么要使用FrameLayout而不使用线性布局等等,个人见解,因为其它的一些界面如Dialog优先级比Activity要高,只有定义了FrameLayout才将Dialog显示在Activity的上面并且位于中心
进入generateLayout方法
protected ViewGroup generateLayout(DecorView decor)
// Apply data from current theme.
TypedArray a = getWindowStyle();
......//requestFeature和setFlag
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
......
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0)
frame = getContext().getDrawable(mFrameResource);
else
frame = null;
mDecor.setWindowFrame(frame);
mDecor.finishChanging();
return contentParent;
将由布局生成的View添加到DecorView中并生成内容View对象contentParent并返回,我们在查看installDecor方法可知将返回的View赋值给了全局变量mcontentParent,在查看PhoneWindow的setContentView方法可知将Activity中的布局又加入到mContentParent中这里展示一下screen_simple.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
从布局中可以看出ActionBar使用了懒加载ViewStub,内容布局则是FrameLayout经过分析得出:
1、Window是一个抽象类,提供了绘制窗口的一组通用的API
2、PhoneWindow是Window的具体继承实现类,并且该类内部包含了一个DecorView对象,该DecorView对象是所有应用窗口的根View
3、DecorView是PhoneWindow的内部类,是FrameLayout的子类,是对FrameLayout进行功能的修饰,是所有应用窗口的根View
Part 2、measure、layout、draw方法的执行流程
measure:测量自己有多大,如果是ViewGroup的话会同时测量里面的子控件的大小
layout:摆放里面子控件(left、top、right、bottom)
draw:绘制
我们来看一下Activity的启动流程
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume)
......
if (r.window == null && !a.mFinished && willBeVisible)
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient)
a.mWindowAdded = true;
wm.addView(decor, l);
......
此方法得到了Window、DecorView、WindowManager(ViewManager的子类)的对象,然后调用了WindowManager.addView方法将DecorView传了进去
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params)
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
再addView方法调用了WindowManagerGlobal.addView方法
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow)
......
ViewRootImpl root;
View panelParentView = null;
......
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try
root.setView(view, wparams, panelParentView);
catch (RuntimeException e)
可以清晰的看到最后执行的是ViewRootImpl的addView方法将DecorView传了进去
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
synchronized (this)
if (mView == null)
......
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0)
mInputChannel = new InputChannel();
......
此方法执行了ViewRootImpl类的requestLayout方法,我们一步步的进行查看最后可以看到最后执行了ViewRootImpl的performTraversals();
private void performTraversals()
......
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged)
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout)
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
if (!cancelDraw && !newSurface)
if (!skipDraw || mReportNextDraw)
if (mPendingTransitions != null && mPendingTransitions.size() > 0)
for (int i = 0; i < mPendingTransitions.size(); ++i)
mPendingTransitions.get(i).startChangingAnimations();
mPendingTransitions.clear();
performDraw();
else
if (viewVisibility == View.VISIBLE)
// 当View可见的时候再次执行一次performTraversals
scheduleTraversals();
else if (mPendingTransitions != null && mPendingTransitions.size() > 0)
for (int i = 0; i < mPendingTransitions.size(); ++i)
mPendingTransitions.get(i).endChangingAnimations();
mPendingTransitions.clear();
可以看到此方法顺序执行了performMeasure、performLayout、performDraw。
再查看performMeasure之前,先查看getRootMeasureSpec方法
private static int getRootMeasureSpec(int windowSize, int rootDimension)
int measureSpec;
switch (rootDimension)
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
return measureSpec;
tips:
1、MeasureSpec:测量规格,单位int 32位,前两位当做mode,后30位当做值。
mode:
(1)、 EXACTLY: 精确或Match_parent
(2)、AT_MOST: 根据父容器当前的大小,结合你指定的尺寸参考值来考虑你应该是多大尺寸,需要计算
(3)、UNSPECIFIED: 最多的意思。根据当前的情况,结合你制定的尺寸参考值来考虑,在不超过父容器给你限定的尺寸的前提下,来测量你的一个恰好的内容尺寸。
用的比较少,一般见于ScrollView,ListView(大小不确定,同时大小还是变的。会通过多次测量才能真正决定好宽高。)
value:宽高的值。
2、调用setMeasuredDimension(w,h)来确定自己最终的宽高
3、通过调用getRootMeasureSpec方法获得MeasureSpec并传给performMeasure,其实实际上是传给了DecorView
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec)
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
finally
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
再performMeasure中执行的是mView.measure(),根据上面分析Activity的绘制流程可知mView代表的是DecorView,由此DecorView便开始绘制了。
然而DecorView没有measure方法,而继承的FrameLayout也没有,所以这里调用的是View类的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
......
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache)
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
else
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
这里调用了onMeasure方法,由于DecorView类中有onMeasure方法,所以调用的是DecorView中的onMeasure方法,因为每个ViewGroup实现onMeasure方法不同,这里分析一下FrameLayout的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int count = getChildCount();//得到FrameLayout的孩子个数
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//对每个Child进行遍历
for (int i = 0; i < count; i++)
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE)
//测量Child和Margin值,其实是调用了Child的measure方法,当调用测量的方法之后才可以是调用child的宽高
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//寻找子View的最大尺寸,因为如果FrameLayout为wrap_Content的时候则它的尺寸便为子View最大尺寸
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
//这里将Child高或者宽为match_parent的View添加倒measureMatchParentChild中
//宽高为Match_Parent的子View大小会收到FrameLayout的影响
if (measureMatchParentChildren)
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT)
mMatchParentChildren.add(child);
//将padding计算再内
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
//将计算得到值和建议值进行比较
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//保存得到的最终值
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1)
//对Child设有march_parent的情况进行处理
for (int i = 0; i < count; i++)
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
//当宽度和高度为march_parent时,将FrameLayout的宽高传给Child并将子View Mode设置为EXACTLY
if (lp.width == LayoutParams.MATCH_PARENT)
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
else //通过对FrameLayout的mode进行判断来确定Child的尺寸大小
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
//高度一样
......
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
接下来我们来看getChildMeasureSpec方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension)
//得到Mode和size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
switch (specMode)
/*
对于子View,FrameLayout有一个确切的尺寸
如果子View的尺寸也为一个确定的值则将其置为该值,并将Mode置为EXACTLY
如果为Match_parent则将子View的尺寸置为FrameLayout的尺寸,并将Mode置为EXACTLY
如果为Wrap_content则将子View尺寸设置为FrameLayout尺寸,并将Mode置为AT_Most
(也就是子View的尺寸不得大于FrameLayout的尺寸)*/
case MeasureSpec.EXACTLY:
if (childDimension >= 0)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
else if (childDimension == LayoutParams.MATCH_PARENT)
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
else if (childDimension == LayoutParams.WRAP_CONTENT)
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
break;
//下面是判断FrameLayout的Mode为AT_Most和UNSPECIFIED
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
总结:
1、经过大量的测量之后,最终确定了自己的宽高,需要调用setMeasureDimension()方法
2、再写自定义控件的时候,我们咬对自己的宽高进行计算,必须要经过measure才能获得宽高,获得宽高的方法不是getWidth而是getMeasureWidth
3、从规格中获取mode和value
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
将mode和size结合成一个规格
MeasureSpec.makeMeasureSpec(resultSize, resultMode);
4、自定义View时候我们只需要测量自己的宽高就可以了
int mode = MeasureSpec.getMode(widthMeasureSpec);
int Size = MeasureSpec.getSize(widthMeasureSpec);
int viewSize = 0;
switch(mode)
case MeasureSpec.EXACTLY:
viewSize = size;//当前view的尺寸就为父容器的尺寸
break;
case MeasureSpec.AT_MOST:
viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。
break;
case MeasureSpec.UNSPECIFIED:
viewSize = getContentSize();//内容有多大,就设置多大尺寸。
break;
default:
break;
//setMeasuredDimension(width, height);
setMeasuredDimension(size);
5、自定义ViewGroup,我们不但需要onMeasure测量自己还需要测量子控件的大小
//1.测量自己的尺寸
ViewGroup.onMeasure();
//为每一个child计算测量规格信息(MeasureSpec)
ViewGroup.getChildMeasureSpec();
//将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
child.measure();
//子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
child.getChildMeasuredSize();//child.getMeasuredWidth()和child.getMeasuredHeight()
//ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
ViewGroup.calculateSelfSize();
//2.保存自己的尺寸
ViewGroup.setMeasuredDimension(size);
以上是关于Android UI的绘制流程的主要内容,如果未能解决你的问题,请参考以下文章