灵魂画师,Android绘制流程—,2024Android面试

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);
// 调用了 onCreate
onCreate(icicle);
mActivityTransitionState.readState(icicle);
performCreateCommon();
}

// Activity 类
final void performStart() {
// 省略不相关代码

// 进行调用 Activity 的 onStart
mInstrumentation.callActivityOnStart(this);

// 省略不相关代码
}

// Instrumentation 类
public void callActivityOnStart(Activity activity) {
// 调用了 Activity 的 onStart
activity.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 的 Window
r.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) {

// 省略不相关代码

// 初始化 ViewRootImpl
root = 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,运行其run
mChoreographer.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 {
@Override
public 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) {

// 省略不相关代码

// FLAG2
int 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 返回的是 PhoneWindow
getWindow().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 {
// 加载我们设置的布局文件 到 mContentParent
mLayoutInflater.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) {
// 会进行实例化 一个mDecor
mDecor = 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) {
// 初始化 mContentParent
mContentParent = 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 layout
if (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 layout
if (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.content
ViewGroup 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 就为 false
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

@MeasureSpecMode
public 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 it
    resultSize = 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 it
    resultSize = childDimension;
    resultMode = MeasureSpec.EXACTLY;
    } else if (childDimension == LayoutParams.MATCH_PARENT) {
    // Child wants to be our size… find out how big it should
    // be
    resultSize = 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 be
    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
    resultMode = MeasureSpec.UNSPECIFIED;
    }
    break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

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

子LayoutParams(纵向) \ 父类的SpecMode(横向)EXACTLYAT_MOSTUNSPECIFIED
dp/px (确定的值)EXACTLY
ChildSizeEXACTLY
ChildSizeEXACTLY
ChildSize
MATCH_PARENTEXACTLY
ParentSizeAT_MOST
ParentSizeUNSPECIFIED
0
WRAP_CONTENTAT_MOST
ParentSizeAT_MOST

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

推荐学习资料

  • Android进阶学习全套手册

  • Android对标阿里P7学习视频

  • BAT TMD大厂Android高频面试题

-1712030001918)]
[外链图片转存中…(img-pKyOHqJd-1712030001918)]
[外链图片转存中…(img-gPqHqxN6-1712030001919)]
[外链图片转存中…(img-56qrqdnh-1712030001919)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-tRMFAxEq-1712030001919)]

推荐学习资料

  • Android进阶学习全套手册

    [外链图片转存中…(img-2OEaQQjY-1712030001919)]

  • Android对标阿里P7学习视频

    [外链图片转存中…(img-S9okqCGp-1712030001920)]

  • BAT TMD大厂Android高频面试题

[外链图片转存中…(img-oWt5MonV-1712030001920)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值