目录

  • 一、前言
  • 二、我们的目标是啥
  • 三、绘制流程从何而起
  • 四、Activity 的界面结构在哪里开始形成
  • 五、绘制流程如何运转起来的
    • 1、onMeasure
      • MeasureSpec是什么
      • (1)测量模式
      • (2)makeMeasureSpec
      • (3)getMode
      • (4)getSize
      • 这两个参数值从哪来
      • 这两个参数值怎么使用
    • 2、onLayout
    • 3、onDraw
  • 六、实战
    • 1、效果图
    • 2、编码思路
      • (1) onMeasure
      • (2) onLayout
      • (3) onDraw
    • 3、小结
  • 七、写在最后

一、前言

绘制流程可以说是Android进阶中必不可少的一个内容,也是面试中被问得最多的问题之一。这方面优秀的文章也已经是非常之多,但是小盆友今天还是要以自己的姿态来炒一炒这冷饭,或许就是蛋炒饭了?。话不多说,老规矩先上实战图,然后开始分享。

标签布局

二、我们的目标是啥

其实这篇文章,小盆友纠结了挺久,因为绘制流程涉及的东西非常之多,并非一篇文章可以写完,所以这篇文章我先要确定一些目标,防止因为追查源码过深,而迷失于源码中,最后导致一无所获。我们的目标是:

  1. 绘制流程从何而起
  2. Activity 的界面结构在哪里开始形成
  3. 绘制流程如何运转起来

接下来我们就一个个目标来 conquer。

三、绘制流程从何而起

我们一说到绘制流程,就会想到或是听过onMeasureonLayoutonDraw这三个方法,但是有没想过为什么我们开启一个App或是点开一个Activity,就会触发这一系列流程呢?想知道绘制流程从何而起,我们就有必要先解释 App启动流程Activity的启动流程

我们都知道 ActivityThread 的 main 是一个App的入口。我们来到 main 方法看看他做了什么启动操作。

ActivityThread 的 main方法是由 ZygoteInit 类中最终通过 RuntimeInit类的invokeStaticMain 方法进行反射调用。有兴趣的童鞋可以自行查阅下,限于篇幅,就不再展开分享。

// ActivityThread 类
public static void main(String[] args) {// ...省略不相关代码// 准备主线程的 LooperLooper.prepareMainLooper();// 实例化 ActivityThread,用于管理应用程序进程中主线程的执行ActivityThread thread = new ActivityThread();// 进入 attach 方法thread.attach(false);// ...省略不相关代码// 开启 LooperLooper.loop();// ...省略不相关代码}

进入 main 方法,我们便看到很熟悉的 Handler机制。在安卓中都是以消息进行驱动,在这里也不例外,我们可以看到先进行 Looper 的准备,在最后开启 Looper 进行循环获取消息,用于处理传到主线程的消息。

这也是为什么我们在主线程不需要先进行 Looper 的准备和开启,emmm,有些扯远了。

回过头,可以看到夹杂在中间的 ActivityThread 类的实例化并且调用了 attach 方法。具体代码如下,我们接着往下走。

// ActivityThread 类
private void attach(boolean system) {// ...省略不相关代码// system 此时为false,进入此分支if (!system) {// ...省略不相关代码// 获取系统的 AMS 服务的 Proxy,用于向 AMS 进程发送数据final IActivityManager mgr = ActivityManager.getService();try {// 将我们的 mAppThread 传递给 AMS,AMS 便可控制我们 App 的 Activitymgr.attachApplication(mAppThread);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}// ...省略不相关代码} else {// ...省略不相关代码}// ...省略不相关代码}// ActivityManager 类
public static IActivityManager getService() {return IActivityManagerSingleton.get();
}// ActivityManager 类
private static final Singleton<IActivityManager> IActivityManagerSingleton =new Singleton<IActivityManager>() {@Overrideprotected IActivityManager create() {// 在这里获取 AMS 的binderfinal IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);// 这里获取 AMS 的 proxy,可以进行发送数据final IActivityManager am = IActivityManager.Stub.asInterface(b);return am;}};

我们进入attach 方法,方法内主要是通过 ActivityManager 的 getService 方法获取到了 ActivityManagerService(也就是我们所说的AMS) 的 Proxy,达到与AMS 进行跨进程通信的目的。

文中所说的 Proxy 和 Stub,是以系统为我们自动生成AIDL时的类名进行类比使用,方便讲解。Proxy 代表着发送信息,Stub 代表着接收信息。

mgr.attachApplication(mAppThread); 代码中向 AMS 进程发送信息,携带了一个类型为 ApplicationThread 的 mAppThread 参数。这句代码的作用,其实就是把 我们应用的 “控制器” 上交给了 AMS,这样使得 AMS 能够来控制我们应用中的Activity的生命周期。为什么这么说呢?我们这就有必要来了解下 ApplicationThread 类的结构,其部分代码如下:

// ActivityThread$ApplicationThread 类
private class ApplicationThread extends IApplicationThread.Stub {// 省略大量代码@Overridepublic final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,ActivityInfo info, Configuration curConfig, Configuration overrideConfig,CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,int procState, Bundle state, PersistableBundle persistentState,List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {updateProcessState(procState, false);// 会将 AMS 发来的信息封装在 ActivityClientRecord 中,然后发送给 HandlerActivityClientRecord r = new ActivityClientRecord();r.token = token;r.ident = ident;r.intent = intent;r.referrer = referrer;r.voiceInteractor = voiceInteractor;r.activityInfo = info;r.compatInfo = compatInfo;r.state = state;r.persistentState = persistentState;r.pendingResults = pendingResults;r.pendingIntents = pendingNewIntents;r.startsNotResumed = notResumed;r.isForward = isForward;r.profilerInfo = profilerInfo;r.overrideConfig = overrideConfig;updatePendingConfiguration(curConfig);sendMessage(H.LAUNCH_ACTIVITY, r);}// 省略大量代码}

从 ApplicationThread 的方法名,我们会惊奇的发现大多方法名以 scheduleXxxYyyy 的形式命名,而且和我们熟悉的生命周期都挺接近。上面代码留下了我们需要的方法 scheduleLaunchActivity ,它们包含了我们 Activity 的 onCreateonStartonResume

scheduleLaunchActivity 方法会对 AMS 发来的信息封装在 ActivityClientRecord 类中,最后通过 sendMessage(H.LAUNCH_ACTIVITY, r); 这行代码将信息以 H.LAUNCH_ACTIVITY 的信息标记发送至我们主线程中的 Handler。我们进入主线程的 Handler 实现类 H。具体代码如下:

// ActivityThread$H 类
private class H extends Handler {public static final int LAUNCH_ACTIVITY = 100;// 省略大量代码public void handleMessage(Message msg) {if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));switch (msg.what) {case LAUNCH_ACTIVITY: {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");final ActivityClientRecord r = (ActivityClientRecord) msg.obj;r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo);handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);}// 省略大量代码}}// 省略大量代码
}

我们从上面的代码可以知道消息类型为 LAUNCH_ACTIVITY,则会进入 handleLaunchActivity 方法,我们顺着往里走,来到下面这段代码

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {// If we are getting ready to gc after going to the background, well// we are back active so skip it.unscheduleGcIdler();mSomeActivitiesChanged = true;if (r.profilerInfo != null) {mProfiler.setProfiler(r.profilerInfo);mProfiler.startProfiling();}// Make sure we are running with the most recent config.handleConfigurationChanged(null, null);if (localLOGV) Slog.v(TAG, "Handling launch of " + r);// Initialize before creating the activityWindowManagerGlobal.initialize();// 获得一个Activity对象,会进行调用 Activity 的 onCreate 和 onStart 的生命周期Activity a = performLaunchActivity(r, customIntent);// Activity 不为空进入if (a != null) {r.createdConfig = new Configuration(mConfiguration);reportSizeConfigurations(r);Bundle oldState = r.state;// 该方法最终回调用到 Activity 的 onResumehandleResumeActivity(r.token, false, r.isForward,!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);if (!r.activity.mFinished && r.startsNotResumed) {performPauseActivityIfNeeded(r, reason);if (r.isPreHoneycomb()) {r.state = oldState;}}} else {// If there was an error, for any reason, tell the activity manager to stop us.try {ActivityManager.getService().finishActivity(r.token, Activity.RESULT_CANCELED, null,Activity.DONT_FINISH_TASK_WITH_ACTIVITY);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}
}

我们先看这行代码 performLaunchActivity(r, customIntent); 最终会调用 onCreateonStart 方法。眼见为实,耳听为虚,我们继续进入深入。来到下面这段代码

// ActivityThread 类
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {// 省略不相关代码// 创建 Activity 的 ContextContextImpl appContext = createBaseContextForActivity(r);Activity activity = null;try {java.lang.ClassLoader cl = appContext.getClassLoader();// ClassLoader 加载 Activity类,并创建 Activityactivity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);// 省略不相关代码} catch (Exception e) {// 省略不相关代码}try {// 创建 ApplicationApplication app = r.packageInfo.makeApplication(false, mInstrumentation);// 省略不相关代码if (activity != null) {// 省略不相关代码// 调用了 Activity 的 attachactivity.attach(appContext, this, getInstrumentation(), r.token,r.ident, app, r.intent, r.activityInfo, title, r.parent,r.embeddedID, r.lastNonConfigurationInstances, config,r.referrer, r.voiceInteractor, window, r.configCallback);// 这个 intent 就是我们 getIntent 获取到的if (customIntent != null) {activity.mIntent = customIntent;}// 省略不相关代码// 调用 Activity 的 onCreateif (r.isPersistable()) {mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);} else {mInstrumentation.callActivityOnCreate(activity, r.state);}// 省略不相关代码if (!r.activity.mFinished) {// zincPower 调用 Activity 的 onStartactivity.performStart();r.stopped = false;}if (!r.activity.mFinished) {// zincPower 调用 Activity 的 onRestoreInstanceState 方法,数据恢复if (r.isPersistable()) {if (r.state != null || r.persistentState != null) {mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,r.persistentState);}} else if (r.state != null) {mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);}}// 省略不相关代码}// 省略不相关代码} // 省略不相关代码return activity;
}// Instrumentation 类
public void callActivityOnCreate(Activity activity, Bundle icicle,PersistableBundle persistentState) {prePerformCreate(activity);activity.performCreate(icicle, persistentState);postPerformCreate(activity);
}// Activity 类
final void performCreate(Bundle icicle) {restoreHasCurrentPermissionRequest(icicle);// 调用了 onCreateonCreate(icicle);mActivityTransitionState.readState(icicle);performCreateCommon();
}// Activity 类
final void performStart() {// 省略不相关代码// 进行调用 Activity 的 onStartmInstrumentation.callActivityOnStart(this);// 省略不相关代码
}// Instrumentation 类
public void callActivityOnStart(Activity activity) {// 调用了 Activity 的 onStartactivity.onStart();
}

进入 performLaunchActivity 方法后,我们会发现很多我们熟悉的东西,小盆友已经给关键点打上注释,因为不是文章的重点就不再细说,否则篇幅过长。

我们直接定位到 mInstrumentation.callActivityOnCreate 这行代码。进入该方法,方法内会调用 activityperformCreate 方法,而 performCreate 方法里会调用到我们经常重写的 Activity 生命周期的 onCreate 方法。?至此,找到了 onCreate 的调用地方,这里需要立个 FLAG1,因为目标二需要的开启便是这里,我下一小节分享,勿急。

回过头来继续 performLaunchActivity 方法的执行,会调用到 activityperformStart 方法,而该方法又会调用到 mInstrumentation.callActivityOnStart 方法,最后在该方法内便调用了我们经常重写的 Activity 生命周期的 onStart 方法。?至此,找到了 onStart 的调用地方。

找到了两个生命周期的调用地方,我们需要折回到 handleLaunchActivity 方法中,继续往下运行,便会来到 handleResumeActivity 方法,具体代码如下:

// ActivityThread 类
final void handleResumeActivity(IBinder token,boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {// 省略部分代码r = performResumeActivity(token, clearHide, reason);// 省略部分代码if (r.window == null && !a.mFinished && willBeVisible) {// 将 Activity 中的 Window 赋值给 ActivityClientRecord 的 Windowr.window = r.activity.getWindow();// 获取 DecorView,这个 DecorView 在 Activity 的 setContentView 时就初始化了View decor = r.window.getDecorView();// 此时为不可见decor.setVisibility(View.INVISIBLE);// WindowManagerImpl 为 ViewManager 的实现类ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();a.mDecor = decor;l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;l.softInputMode |= forwardBit;if (r.mPreserveWindow) {a.mWindowAdded = true;r.mPreserveWindow = false;ViewRootImpl impl = decor.getViewRootImpl();if (impl != null) {impl.notifyChildRebuilt();}}if (a.mVisibleFromClient) {if (!a.mWindowAdded) {a.mWindowAdded = true;// 往 WindowManager 添加 DecorView,并且带上 WindowManager.LayoutParams// 这里面便触发真正的绘制流程wm.addView(decor, l);} else {a.onWindowAttributesChanged(l);}}}// 省略不相关代码
}

performResumeActivity方法最终会调用到 Activity 的 onResume 方法,因为不是我们该小节的目标,就不深入了,童鞋们可以自行深入,代码也比较简单。至此我们就找齐了我们一直重写的三个 Acitivity 的生命周期函数 onCreateonStartonResume 。按照这一套路,童鞋们可以看看 ApplicationThread 的其他方法,会发现 Activity 的生命周期均在其中可以找到影子,也就证实了我们最开始所说的 我们将应用 “遥控器” 交给了AMS。而值得一提的是,这一操作是处于一个跨进程的场景。

继续往下运行来到 wm.addView(decor, l); 这行代码,wm 的具体实现类为 WindowManagerImpl,继续跟踪深入,来到下面这一连串的调用

// WindowManagerImpl 类
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);// tag:进入这一行mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}// WindowManagerGlobal 类
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {// 省略不相关代码ViewRootImpl root;View panelParentView = null;synchronized (mLock) {// 省略不相关代码// 初始化 ViewRootImplroot = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);try {// 将 view 和 param 交于 root// ViewRootImpl 开始绘制 view// tag:进入这一行root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {if (index >= 0) {removeViewLocked(index, true);}throw e;}}
}// ViewRootImpl 类
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {synchronized (this) {if (mView == null) {// 省略不相关代码// 进入绘制流程// tag:进入这一行requestLayout();// 省略不相关代码}}
}// ViewRootImpl 类
@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;// tag:进入这一行scheduleTraversals();}
}// ViewRootImpl 类
void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();// 提交给 编舞者,会在下一帧绘制时调用 mTraversalRunnable,运行其runmChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);if (!mUnbufferedInputDispatch) {scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}
}

中间跳转的方法比较多,小盆友都打上了 // tag:进入这一行 注释,童鞋们可以自行跟踪,会发现最后会调用到编舞者,即 Choreographer 类的 postCallback方法。Choreographer 是一个会接收到垂直同步信号的类,所以当下一帧到达时,他会调用我们刚才提交的任务,即此处的 mTraversalRunnable,并执行其 run 方法。

值得一提的是通过 Choreographer 的 postCallback 方法提交的任务并不是每一帧都会调用,而是只在下一帧到来时调用,调用完之后就会将该任务移除。简而言之,就是提交一次就会在下一帧调用一次。

我们继续来看 mTraversalRunnable 的具体内容,看看每一帧都做了写什么操作。

// ViewRootImpl 类
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();// ViewRootImpl 类
final class TraversalRunnable implements Runnable {@Overridepublic void run() {doTraversal();}
}// ViewRootImpl 类
void doTraversal() {if (mTraversalScheduled) {mTraversalScheduled = false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);if (mProfile) {Debug.startMethodTracing("ViewAncestor");}// 进入此处performTraversals();if (mProfile) {Debug.stopMethodTracing();mProfile = false;}}
}// ViewRootImpl 类
private void performTraversals() {// 省略不相关代码if (!mStopped || mReportNextDraw) {// 省略不相关代码// FLAG2int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// 省略不相关代码// 进行测量performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);// 省略不相关代码// 进行摆放performLayout(lp, mWidth, mHeight);// 省略不相关代码// 布局完回调if (triggerGlobalLayoutListener) {mAttachInfo.mRecomputeGlobalAttributes = false;mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();}// 省略不相关代码// 进行绘制performDraw();}

调用了 mTraversalRunnablerun 方法之后,会发现也是一连串的方法调用,后来到 performTraversals,这里面就有我们一直提到三个绘制流程方法的起源地。这三个起源地就是我们在上面看到的三个方法 performMeasureperformLayoutperformDraw

而这三个方法会进行如下图的一个调用链(?还是手绘,勿喷),从代码我们也知道,会按照 performMeasureperformLayoutperformDraw 的顺序依次调用。

performMeasure 会触发我们的测量流程,如图中所示,进入第一层的 ViewGroup,会调用 measureonMeasure,在 onMeasure 中调用下一层级,然后下一层级的 View或ViewGroup 会重复这样的动作,进行所有 View 的测量。(这一过程可以理解为书的深度遍历)

performLayoutperformMeasure 的流程大同小异,只是方法名不同,就不再赘述。

performDraw 稍微些许不同,当前控件为ViewGroup时,只有需要绘制背景或是我们通过 setWillNotDraw(false) 设置我们的ViewGroup需要进行绘制时,会进入 onDraw 方法,然后通过 dispatchDraw 进行绘制子View,如此循环。而如果为View,自然也就不需要绘制子View,只需绘制自身的内容即可。

至此,绘制流程的源头我们便了解清楚了, onMeasureonLayoutonDraw 三个方法我们会在后面进行详述并融入在实战中。

四、Activity 的界面结构在哪里开始形成


上图是 Activity 的结构。我们先进行大致的描述,然后在进入源码体会这一过程。

我们可以清晰的知道一个 Activity 会对应着有一个 Window,而 Window 的唯一实现类为 PhoneWindowPhoneWindow 的初始化是在 Activity 的 attach 方法中,我们前面也有提到 attach 方法,感兴趣的童鞋可以自行深入。

在往下一层是一个 DecorView,被 PhoneWindow 持有着,DecorView 的初始化在 setContentView 中,这个我们待会会进行详细分析。DecorView 是我们的顶级View,我们设置的布局只是其子View。

DecorView 是一个 FrameLayout。但在 setContentView 中,会给他加入一个线性的布局(LinearLayout)。该线性布局的子View 则一般由 TitleBar 和 ContentView 进行组成。TitleBar 我们可以通过 requestWindowFeature(Window.FEATURE_NO_TITLE); 进行去除,而 ContentView 则是来装载我们设置的布局文件的 ViewGroup 了

现在我们已经有一个大概的印象,接下来进行详细分析。在上一节中(FLAG1处),我们最先会进入的生命周期为onCreate,在该方法中我们都会写上这样一句代码setContentView(R.layout.xxxx) 进行设置布局。经过上一节我们也知道,真正的绘制流程是在 onResume 之后(忘记的童鞋请倒回去看一下),那么 setContentView 起到一个什么作用呢?我进入源码一探究竟吧。

进入 Activity 的 setContentView 方法,可以看到下面这段代码。getWindow 返回的是一个 Window 类型的对象,而通过Window的官方注释可以知道其唯一的实现类为PhoneWindow, 所以我们进入 PhoneWindow 类查看其 setContentView 方法,这里值得我们注意有两行代码。我们一一进入,我们先进入 installDecor 方法。

// Activity 类
public void setContentView(@LayoutRes int layoutResID) {// getWindow 返回的是 PhoneWindowgetWindow().setContentView(layoutResID);initWindowDecorActionBar();
}// Activity 类
public Window getWindow() {return mWindow;
}// PhoneWindow 类
@Override
public void setContentView(int layoutResID) {// 此时 mContentParent 为空,mContentParent 是装载我们布局的容器if (mContentParent == null) {// 进行初始化 顶级View——DecorView 和 我们设置的布局的装载容器——ViewGroup(mContentParent)installDecor();} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());transitionTo(newScene);} else {// 加载我们设置的布局文件 到 mContentParentmLayoutInflater.inflate(layoutResID, mContentParent);}mContentParent.requestApplyInsets();final Callback cb = getCallback();if (cb != null && !isDestroyed()) {cb.onContentChanged();}mContentParentExplicitlySet = true;
}

installDecor 方法的作用为初始化了我们的顶级View(即DecorView)和初始化装载我们布局的容器(即 mContentParent 属性)。具体代码如下

private void installDecor() {mForceDecorInstall = false;if (mDecor == null) {// 会进行实例化 一个mDecormDecor = generateDecor(-1);mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);mDecor.setIsRootNamespace(true);if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);}} else {mDecor.setWindow(this);}if (mContentParent == null) {// 初始化 mContentParentmContentParent = generateLayout(mDecor);// 省略不相关代码
}

generateDecor 中会进行 DecorView 的创建,具体代码如下,较为简单

protected DecorView generateDecor(int featureId) {Context context;if (mUseDecorContext) {Context applicationContext = getContext().getApplicationContext();if (applicationContext == null) {context = getContext();} else {context = new DecorContext(applicationContext, getContext().getResources());if (mTheme != -1) {context.setTheme(mTheme);}}} else {context = getContext();}return new DecorView(context, featureId, this, getAttributes());
}

紧接着是generateLayout 方法,核心代码如下,如果我们在 onCreate 方法前通过requestFeature 进行设置一些特征,此时的 getLocalFeatures 就会获取到,并根据其值选择合适的布局赋值给 layoutResource 属性。最后将该布局资源解析,赋值给 DecorView,紧接着将 DecorView 中 id 为 content 的控件赋值给 contentParent,而这个控件将来就是装载我们设置的布局资源。

protected ViewGroup generateLayout(DecorView decor) {// 省略不相关代码int layoutResource;int features = getLocalFeatures();// System.out.println("Features: 0x" + Integer.toHexString(features));if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {layoutResource = R.layout.screen_swipe_dismiss;setCloseOnSwipeEnabled(true);} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {if (mIsFloating) {TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogTitleIconsDecorLayout, res, true);layoutResource = res.resourceId;} else {layoutResource = R.layout.screen_title_icons;}// XXX Remove this once action bar supports these features.removeFeature(FEATURE_ACTION_BAR);// System.out.println("Title Icons!");} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {// Special case for a window with only a progress bar (and title).// XXX Need to have a no-title version of embedded windows.layoutResource = R.layout.screen_progress;// System.out.println("Progress!");} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {// Special case for a window with a custom title.// If the window is floating, we need a dialog layoutif (mIsFloating) {TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogCustomTitleDecorLayout, res, true);layoutResource = res.resourceId;} else {layoutResource = R.layout.screen_custom_title;}// XXX Remove this once action bar supports these features.removeFeature(FEATURE_ACTION_BAR);} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {// If no other features and not embedded, only need a title.// If the window is floating, we need a dialog layoutif (mIsFloating) {TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogTitleDecorLayout, res, true);layoutResource = res.resourceId;} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {layoutResource = a.getResourceId(R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar);} else {layoutResource = R.layout.screen_title;}// System.out.println("Title!");} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {layoutResource = R.layout.screen_simple_overlay_action_mode;} else {// Embedded, so no decoration is needed.layoutResource = R.layout.screen_simple;// System.out.println("Simple!");}   mDecor.startChanging();// 进行加载 DecorView 的布局mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);// 这里就获取了装载我们设置的内容容器 id 为 R.id.contentViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);// 省略不相关代码return contentParent;
}

我们折回到 setContentView 方法,来到 mLayoutInflater.inflate(...); 这行代码,layoutResID 为我们设置的布局文件,而 mContentParent 就是我们刚刚获取的id 为 content 的控件, 这里便是把他从 xml 文件解析成一棵控件的对象树,并且放入在 mContentParent 容器内。

至此我们知道,Activity 的 setContentView 是让我们布局文件从xml “翻译” 成对应的控件对象,形成一棵以 DecorView 为根结点的控件树,方便我们后面绘制流程进行遍历。

五、绘制流程如何运转起来的

终于来到核心节,我们来继续分析第三节最后说到的三个方法onMeasureonLayoutonDraw,这便是绘制流程运转起来的最后一道门阀,是我们自定义控件中可操作的部分。我们接下来一个个分析

1、onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

要解释清楚这个方法,我们需要先说明两个参数的含义和构成。两个参数都是 MeasureSpec 的类型

MeasureSpec是什么

MeasureSpec 是一个 32位的二进制数。高2位为测量模式,即SpecMode;低30位为测量数值,即SpecSize。我们先看下源码,从源码中找到这两个值的含义。

以下是 MeasureSpec 类的代码(删除了一些不相关的代码)

public static class MeasureSpec {private static final int MODE_SHIFT = 30;// 最终结果为:11 ...(30位)private static final int MODE_MASK = 0x3 << MODE_SHIFT;// 父View 不对 子View 施加任何约束。 子View可以是它想要的任何尺寸。// 二进制:00 ...(30位)public static final int UNSPECIFIED = 0 << MODE_SHIFT;// 父View 已确定 子View 的确切大小。子View 的大小便是父View测量所得的值// 二进制:01 ...(30位)public static final int EXACTLY = 1 << MODE_SHIFT;// 父View 指定一个 子View 可用的最大尺寸值,子View大小 不能超过该值。// 二进制:10 ...(30位)public static final int AT_MOST = 2 << MODE_SHIFT;public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {// API 17 之后,sUseBrokenMakeMeasureSpec 就为 falseif (sUseBrokenMakeMeasureSpec) {return size + mode;} else {return (size & ~MODE_MASK) | (mode & MODE_MASK);}}@MeasureSpecModepublic static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}}

(1)测量模式

类中有三个常量: UNSPECIFIEDEXACTLYAT_MOST,他们对应着三种测量模式,具体含义我们在注释中已经写了,小盆友整理出以下表格方便我们查阅。

名称 含义 数值(二进制) 具体表现
UNSPECIFIED 父View不对子View 施加任何约束,子View可以是它想要的任何尺寸 00 …(30个0) 系统内部使用
EXACTLY 父View已确定子View 的确切大小,子View的大小为父View测量所得的值 01 …(30个0) 具体数值、match_parent
AT_MOST 父View 指定一个子View可用的最大尺寸值,View大小 不能超过该值。 10 …(30个0) wrap_content

(2)makeMeasureSpec

makeMeasureSpec 方法,该方法用于合并测量模式和测量尺寸,将这两个值合为一个32位的数,高2位为测量模式,低30位为尺寸。

该方法很简短,主要得益于 (size & ~MODE_MASK) | (mode & MODE_MASK) 的位操作符,但也带来了一定的理解难度。我们拆解下

  • size & ~MODE_MASK 剔除 size 中的测量模式的值,即将高2位置为00
  • mode & MODE_MASK 保留传入的模式参数的值,同时将低30位置为 0…(30位0)
  • (size & ~MODE_MASK) | (mode & MODE_MASK) 就是 size的低30位 + mode的高2位(总共32位)

至于 &~|这三个位操作为何能做到如此的骚操作,请移步小盆友的另一博文——Android位运算简单讲解。(内容很简短,不熟悉这块内容的童鞋,强烈推荐浏览一下)

(3)getMode

getMode 方法用于获取我们传入的 measureSpec 值的高2位,即测量模式。

(4)getSize

getSize 方法用于获取我们传入的measureSpec 值的低30位,即测量的值。

解释完 MeasureSpec 的是什么,我们还有两个问题需要搞清楚:

  1. 这两个参数值从哪来
  2. 这两个参数值怎么使用

这两个参数值从哪来

借助下面这张简图,设定当前运行的 onMeasure 方法处于B控件,则其两个MeasureSpec值是由其父视图(即A控件)计算得出,计算的规则ViewGroup 有对应的方法,即 getChildMeasureSpec

getChildMeasureSpec 的具体代码如下。我们继续使用上面的情景, B中所获得的值,是 A使用自身的MeasureSpec 和 B 的 LayoutParams.width 或 LayoutParams.height 进行计算得出B的MeasureSpec。

// ViewGroup 类
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {int specMode = MeasureSpec.getMode(spec);int specSize = MeasureSpec.getSize(spec);int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;switch (specMode) {// 父视图为确定的大小的模式case MeasureSpec.EXACTLY:/*** 根据子视图的大小,进行不同模式的组合:* 1、childDimension 大于 0,说明子视图设置了具体的大小* 2、childDimension 为 {@link LayoutParams.MATCH_PARENT},说明大小和其父视图一样大* 3、childDimension 为 {@link LayoutParams.WRAP_CONTENT},说明子视图想为其自己的大小,但* 不能超过其父视图的大小。*/if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size. So be it.resultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// 父视图已经有一个最大尺寸限制case MeasureSpec.AT_MOST:/*** 根据子视图的大小,进行不同模式的组合:* 1、childDimension 大于 0,说明子视图设置了具体的大小* 2、childDimension 为 {@link LayoutParams.MATCH_PARENT},* -----说明大小和其父视图一样大,但是此时的父视图还不能确定其大小,所以只能让子视图不超过自己* 3、childDimension 为 {@link LayoutParams.WRAP_CONTENT},* -----说明子视图想为其自己的大小,但不能超过其父视图的大小。*/if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size, but our size is not fixed.// Constrain child to not be bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size.... find out how// big it should beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

我们将这段代码整理成表格

子LayoutParams(纵向) \ 父类的SpecMode(横向) EXACTLY AT_MOST UNSPECIFIED
dp/px (确定的值) EXACTLY
ChildSize
EXACTLY
ChildSize
EXACTLY
ChildSize
MATCH_PARENT EXACTLY
ParentSize
AT_MOST
ParentSize
UNSPECIFIED
0
WRAP_CONTENT AT_MOST
ParentSize
AT_MOST
ParentSize
UNSPECIFIED
0

所以最终,B的 onMeasure 方法获得的两个值,便是 父视图A 对 B 所做的约束建议值。

你可能会有一个疑惑, 顶级DecorView 的约束哪里来,我们切回 FLAG2 处,在进入 performMeasure 方法时,携带的两个MeasureSpec 是由 WindowManager 传递过来的 Window 的 Rect 的宽高 和 Window 的 WindowManager.LayoutParam 共同决定。简而言之,DecorView的约束从 Window的参数得来

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// 进行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

这两个参数值怎么使用

我们上面一直提到的一个词叫做 “建议”,是因为到达B的两个维度(横纵)的 MeasureSpec,不是就已经决定了控件B的宽高。

这里我们可以类比为 父母总是语重心长的跟自己的孩子说,你要怎么做怎么做(即算出了子View 的 MeasureSpec),懂事的孩子会知道听从父母的建议可以让自己少走弯路(即遵循传递下来的MeasureSpec约束),而调皮一点的孩子,觉得打破常规更加好玩(即不管 MeasureSpec 的规则约束)。

按照约定,我们是要遵循父View给出的约束。而B控件再进行计算其自己子View的MeasureSpec(如果有子View),子View 会再进行测量 孙View,这样一层层的测量(这里能感受到树结构的魅力了吧?)。

B控件完成子View的测量,调用setMeasuredDimension 将自身最终的 测量宽高 进行设置,这样就完成B控件的测量流程就完毕了。

2、onLayout

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

onLayout 则是进行摆放,这一过程比较简单,因为我们从 onMeasure 中已经得到各个子View 的宽高。父View 只要按照自己的逻辑负责给定各个子View 的 左上坐标右下坐标 即可。

3、onDraw

protected void onDraw(Canvas canvas)

绘制流程中,onDraw 应该说是童鞋们最为熟悉的,只要在 canvas 绘制自身需要绘制的内容便可以。

六、实战

上一节总结起来,就是我们在面试时总会说的那句话,onMeasure负责测量、onLayout负责摆放、onDraw负责绘制,但理论总是过于空洞,我们现在将理论融入到操作中来。我们用标签的流式布局来说明进一步解释这一切。

1、效果图


Github入口:传送门

2、编码思路

在这种标签流式布局的情景中,我们会往控件TagFlowLayout中放入标签TextView(当然也可以是更复杂的布局,这里为了方便讲清楚思路)。 我们放入四个标签,分别为 “大Android”、“猛猛的小盆友”、“JAVA”、“ PHP是最好的语言”。

我们借助这张小盆友手绘的流程图,来讲清楚这绘制流程。

(1) onMeasure

最开始,控件是空的,也就是第一幅小图。

接着将第一个标签 “大Android” 放入,此时不超出 TagFlowLayout 的宽,如第二幅小图所示。

然后将第二个标签 “猛猛的小盆友” 放入,此时如第三幅小图所示,超出了 TagFlowLayout 的宽, 所以我们进行换行,将 “猛猛的小盆友” 放入第二行。

在接着将第三个标签 “JAVA” 放入,此时不超出 TagFlowLayout 的宽,如第四幅小图所示。

最后把剩下的 “PHP是最好的语言” 也放入,当此时有个问题,即使一行放一个也容不下(第五幅小图),因为 “ PHP是最好的语言” 的宽已经超出 TagFlowLayout 的宽,所以我们在给 “PHP是最好的语言” 测量的MeasureSpec时,需要进行“纠正”,使其宽度为 TagFlowLayout 的宽,最终形成了第六幅小图的样子。

最后还需要将我们测量的结果通过 setMeasuredDimension 设置我们自身的 TagFlowLayout 控件的宽高。

(2) onLayout

经过 onMeasure ,TagFlowLayout 心中已经知道自己的 每个孩子的宽高每个孩子要“站”在哪一行,但具体的坐标还是需要进行计算。

“大Android” 的标签比较坐标比较容易(我们这里讨论思路的时候不考虑padding和margin),(l1,t1) 就是 (0,0),而 (r1,b1) 则是 (0+ width, 0+height)。

“猛猛的小盆友” 的坐标需要依赖 “大Android”,(l2,t2) 则为 (0, 第一行的高度) ,(r2,b2) 为 (自身的Width,第一行的高度+自身的Height)。

“JAVA” 的坐标则需要依赖“猛猛的小盆友” 和 “大Android”, (l3,t3) 为 (“猛猛的小盆友”的Width, 第一行的高度) ,(r3,b3) 为 (“猛猛的小盆友”的Width + 自身的Width, 第一行的高度+自身的Height)。

“PHP是最好的语言” 需要依赖前两行的总高度,具体看坐标的计算。 (l4,t4) 为 (0,第一行高+第二行高), (r4,b4) 为 (自身的Width,第一行高+第二行高+自身的Height)。

(3) onDraw

这个方法在我们这个控件中不需要,因为绘制的任务是由各个子View负责。确切的说 onDraw 在我们的 TagFlowLayout 并不会被调用,具体原因我们在前面已经说了,这里就不赘述了。

3、小结

虽然铺垫了很多,但是 TagFlowLayout 的代码量并不多,这里也不再粘贴出来,需要的进入传送门。我们只需要在onMeasure 中进行测量,然后将测量的值进行存储,最后在 onLayout 依赖测量的结果进行摆放即可。

七、写在最后

距离上篇博文的发布也有接近三个星期了,这次耗时比较久原因挺多,绘制流程涉及的知识点很多,这里讲述的只是比较接近于我们开发者的部分,所以导致小盆友在写这篇文章的时候有些纠结。还有另一个原因是小盆友的一些私人事情,需要些时间来平复,但最终也坚持着写完。如果童鞋们发现有那些欠妥的地方,请留言区与我讨论,我们共同进步。如果觉得这碗“蛋炒饭”别有一番滋味,给我一个赞吧。

灵魂画师,Android绘制流程——Android高级UI相关推荐

  1. Android绘制流程

    一.前言 1.1.C++界面库 MFC.WTL.DuiLib.QT.Skia.OpenGL. Android里面的画图分为2D和3D两种: 2D是由Skia 来实现的,3D部分是由OpenGL实现的. ...

  2. android 绘图流程,Android View绘制流程

    前言 不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure().onLayout().onDraw ...

  3. Android(基本、高级UI组件)

    目录 一:前言 二:文本框组件 三:编辑框组件 四:按钮组件 4.1 匿名内部类监听器 4.2 onClick属性实现 4.3 图像按钮(Imagebutton) 4.4 单选按钮(radioButt ...

  4. 【字节码插桩】Android 打包流程 | Android 中的字节码操作方式 | AOP 面向切面编程 | APT 编译时技术

    文章目录 一.Android 中的 Java 源码打包流程 1.Java 源码打包流程 2.字符串常量池 二.Android 中的字节码操作方式 一.Android 中的 Java 源码打包流程 Ja ...

  5. Android绘制自定义控件,Android自定义控件绘制基本图形基础入门

    本文讲述绘制android自定义各种图形效果,为自定义控件的入门篇 相关视频链接: android自定义控件系列 android视频全系列 绘制点–这个控件只需要在布局中引用或者代码中new 即可,下 ...

  6. android 绘制分割线,Android EditText在其drawable和它的文本之间绘制一个分隔线

    >在res / drawable / shape.xml中创建一个矩形圆角形状 >现在创建一个布局 android:layout_width="match_parent" ...

  7. android 绘制按钮,Android:使用xml定义创建一个三角形的按钮(可绘制)

    如果有人仍然有这个问题: > xml: android:fromDegrees="45" android:toDegrees="0" android:pi ...

  8. flutter对比Android绘制流程,Flutter与android的对比---View

    本文是在GitHub上一个flutter项目的资料中看到的,由于原文过于太长,因此对其进行了章节拆分方便阅读,此篇为原文的部分内容,如果想查看该项目请跳转GitHub查看.

  9. android绘制直角坐标系,Android自定义View之扇形统计图

    Android自定义View之扇形统计图 点击标题下「蓝色微信名」可快速关注 作者| Android_gen 地址 | http://www.jianshu.com/p/cc93c5dd43ad 源码 ...

最新文章

  1. 牛逼!二维码会被人类扫完吗?疫情期间用掉了1400亿个!
  2. MySQL 故障集锦
  3. Python 解决写入csv中间隔一行空行问题
  4. mySql中使用命令行建表基本操作
  5. postgresql 安装_CentOS7安装使用PostgreSQL数据库
  6. 九十八、轻松搞定Python中的Markdown系列
  7. WPF--ContextMenu绑定命令的一个问题
  8. InnoDB存储引擎学习笔记(更新ing)
  9. Error-Input tensor has type kTfLiteFloat32: it requires specifying NormalizationOptions metadata to
  10. Type-C笔记本电脑全功能TCPC接口方案
  11. educoder:实验二 数字类型及其操作(新)
  12. jadx卡死解决方案
  13. Vue Mixin 与小程序 Mixins 应用
  14. 幼儿使用计算机亮度,选儿童护眼灯小心被广告忽悠,亮度值并非越高越好!
  15. OSChina 周三乱弹 ——程序员下班后总是不关电脑原因竟然是这样
  16. 异动K线--庄家破绽
  17. SpringBoot实战系列之发送短信验证码
  18. php 数据库万能引擎类,ADODB PHP 数据库万能引擎类
  19. BREW平台主要技术的分析与总结
  20. 以前管Facebook叫“脸书” 现在管Meta叫什么呢

热门文章

  1. 11408的备考建议
  2. d=[张三,李四,王五] 输出d[0] 结果 '\xe5\xbc\xa0\xe4\xb8\x89' Python2.6列表中文输出问题怎么解决?
  3. 到底什么是分布式系统,该如何学习
  4. #后疫情时代的新思考#风险之中,我们更应该看到责任与机遇丨数据猿公益策划...
  5. FPGA开发基础知识
  6. 惠普linux进入bios设置u盘启动,如何进入bios设置,详细教您惠普如何进入bios设置u盘启动...
  7. 用java演示斐波那契数列
  8. shell的自定义变量
  9. 项目管理基础:什么是项目管理?
  10. 【矩阵论】Hermite二次型(3)