灵魂画师,Android绘制流程——Android高级UI
目录
- 一、前言
- 二、我们的目标是啥
- 三、绘制流程从何而起
- 四、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进阶中必不可少的一个内容,也是面试中被问得最多的问题之一。这方面优秀的文章也已经是非常之多,但是小盆友今天还是要以自己的姿态来炒一炒这冷饭,或许就是蛋炒饭了?。话不多说,老规矩先上实战图,然后开始分享。
标签布局
二、我们的目标是啥
其实这篇文章,小盆友纠结了挺久,因为绘制流程涉及的东西非常之多,并非一篇文章可以写完,所以这篇文章我先要确定一些目标,防止因为追查源码过深,而迷失于源码中,最后导致一无所获。我们的目标是:
- 绘制流程从何而起
- Activity 的界面结构在哪里开始形成
- 绘制流程如何运转起来
接下来我们就一个个目标来 conquer。
三、绘制流程从何而起
我们一说到绘制流程,就会想到或是听过onMeasure
、onLayout
、onDraw
这三个方法,但是有没想过为什么我们开启一个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 的 onCreate
、onStart
和 onResume
。
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);
最终会调用 onCreate
和 onStart
方法。眼见为实,耳听为虚,我们继续进入深入。来到下面这段代码
// 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
这行代码。进入该方法,方法内会调用 activity
的 performCreate
方法,而 performCreate
方法里会调用到我们经常重写的 Activity 生命周期的 onCreate
方法。?至此,找到了 onCreate
的调用地方,这里需要立个 FLAG1,因为目标二需要的开启便是这里,我下一小节分享,勿急。
回过头来继续 performLaunchActivity
方法的执行,会调用到 activity
的 performStart
方法,而该方法又会调用到 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 的生命周期函数 onCreate
、onStart
和 onResume
。按照这一套路,童鞋们可以看看 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();}
调用了 mTraversalRunnable
的 run
方法之后,会发现也是一连串的方法调用,后来到 performTraversals
,这里面就有我们一直提到三个绘制流程方法的起源地。这三个起源地就是我们在上面看到的三个方法 performMeasure
、performLayout
、performDraw
。
而这三个方法会进行如下图的一个调用链(?还是手绘,勿喷),从代码我们也知道,会按照 performMeasure
、performLayout
、performDraw
的顺序依次调用。
performMeasure
会触发我们的测量流程,如图中所示,进入第一层的 ViewGroup,会调用 measure
和 onMeasure
,在 onMeasure
中调用下一层级,然后下一层级的 View或ViewGroup 会重复这样的动作,进行所有 View 的测量。(这一过程可以理解为书的深度遍历)
performLayout
和 performMeasure
的流程大同小异,只是方法名不同,就不再赘述。
performDraw
稍微些许不同,当前控件为ViewGroup时,只有需要绘制背景或是我们通过 setWillNotDraw(false)
设置我们的ViewGroup需要进行绘制时,会进入 onDraw
方法,然后通过 dispatchDraw
进行绘制子View,如此循环。而如果为View,自然也就不需要绘制子View,只需绘制自身的内容即可。
至此,绘制流程的源头我们便了解清楚了, onMeasure
、 onLayout
、onDraw
三个方法我们会在后面进行详述并融入在实战中。
四、Activity 的界面结构在哪里开始形成
上图是 Activity 的结构。我们先进行大致的描述,然后在进入源码体会这一过程。
我们可以清晰的知道一个 Activity 会对应着有一个 Window,而 Window 的唯一实现类为 PhoneWindow,PhoneWindow 的初始化是在 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 为根结点的控件树,方便我们后面绘制流程进行遍历。
五、绘制流程如何运转起来的
终于来到核心节,我们来继续分析第三节最后说到的三个方法onMeasure
、onLayout
、onDraw
,这便是绘制流程运转起来的最后一道门阀,是我们自定义控件中可操作的部分。我们接下来一个个分析
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)测量模式
类中有三个常量: UNSPECIFIED
、EXACTLY
、AT_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位置为00mode & 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
的是什么,我们还有两个问题需要搞清楚:
- 这两个参数值从哪来
- 这两个参数值怎么使用
这两个参数值从哪来
借助下面这张简图,设定当前运行的 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相关推荐
- Android绘制流程
一.前言 1.1.C++界面库 MFC.WTL.DuiLib.QT.Skia.OpenGL. Android里面的画图分为2D和3D两种: 2D是由Skia 来实现的,3D部分是由OpenGL实现的. ...
- android 绘图流程,Android View绘制流程
前言 不知道大家有没有想过一个问题,当启动一个Activity的时候,相应的XML布局文件中的View是如何显示到屏幕上的?有些同学会说是通过onMeasure().onLayout().onDraw ...
- Android(基本、高级UI组件)
目录 一:前言 二:文本框组件 三:编辑框组件 四:按钮组件 4.1 匿名内部类监听器 4.2 onClick属性实现 4.3 图像按钮(Imagebutton) 4.4 单选按钮(radioButt ...
- 【字节码插桩】Android 打包流程 | Android 中的字节码操作方式 | AOP 面向切面编程 | APT 编译时技术
文章目录 一.Android 中的 Java 源码打包流程 1.Java 源码打包流程 2.字符串常量池 二.Android 中的字节码操作方式 一.Android 中的 Java 源码打包流程 Ja ...
- Android绘制自定义控件,Android自定义控件绘制基本图形基础入门
本文讲述绘制android自定义各种图形效果,为自定义控件的入门篇 相关视频链接: android自定义控件系列 android视频全系列 绘制点–这个控件只需要在布局中引用或者代码中new 即可,下 ...
- android 绘制分割线,Android EditText在其drawable和它的文本之间绘制一个分隔线
>在res / drawable / shape.xml中创建一个矩形圆角形状 >现在创建一个布局 android:layout_width="match_parent" ...
- android 绘制按钮,Android:使用xml定义创建一个三角形的按钮(可绘制)
如果有人仍然有这个问题: > xml: android:fromDegrees="45" android:toDegrees="0" android:pi ...
- flutter对比Android绘制流程,Flutter与android的对比---View
本文是在GitHub上一个flutter项目的资料中看到的,由于原文过于太长,因此对其进行了章节拆分方便阅读,此篇为原文的部分内容,如果想查看该项目请跳转GitHub查看.
- android绘制直角坐标系,Android自定义View之扇形统计图
Android自定义View之扇形统计图 点击标题下「蓝色微信名」可快速关注 作者| Android_gen 地址 | http://www.jianshu.com/p/cc93c5dd43ad 源码 ...
最新文章
- 牛逼!二维码会被人类扫完吗?疫情期间用掉了1400亿个!
- MySQL 故障集锦
- Python 解决写入csv中间隔一行空行问题
- mySql中使用命令行建表基本操作
- postgresql 安装_CentOS7安装使用PostgreSQL数据库
- 九十八、轻松搞定Python中的Markdown系列
- WPF--ContextMenu绑定命令的一个问题
- InnoDB存储引擎学习笔记(更新ing)
- Error-Input tensor has type kTfLiteFloat32: it requires specifying NormalizationOptions metadata to
- Type-C笔记本电脑全功能TCPC接口方案
- educoder:实验二 数字类型及其操作(新)
- jadx卡死解决方案
- Vue Mixin 与小程序 Mixins 应用
- 幼儿使用计算机亮度,选儿童护眼灯小心被广告忽悠,亮度值并非越高越好!
- OSChina 周三乱弹 ——程序员下班后总是不关电脑原因竟然是这样
- 异动K线--庄家破绽
- SpringBoot实战系列之发送短信验证码
- php 数据库万能引擎类,ADODB PHP 数据库万能引擎类
- BREW平台主要技术的分析与总结
- 以前管Facebook叫“脸书” 现在管Meta叫什么呢
热门文章
- 11408的备考建议
- d=[张三,李四,王五] 输出d[0] 结果 '\xe5\xbc\xa0\xe4\xb8\x89' Python2.6列表中文输出问题怎么解决?
- 到底什么是分布式系统,该如何学习
- #后疫情时代的新思考#风险之中,我们更应该看到责任与机遇丨数据猿公益策划...
- FPGA开发基础知识
- 惠普linux进入bios设置u盘启动,如何进入bios设置,详细教您惠普如何进入bios设置u盘启动...
- 用java演示斐波那契数列
- shell的自定义变量
- 项目管理基础:什么是项目管理?
- 【矩阵论】Hermite二次型(3)