【Android】View的工作流程——measure

1.View的工作流程入口

1.1DecorView被加载到Window中

看到这里你对Activity的构成有一定的了解,每个 Activity 都有一个与之关联的 Window 对象,而 DecorView 是这个 Window 的根视图。当DecorView被创建以及加载资源的时候,此时它的内容还无法进行显示,因为还没有加载到Window当中。接下来就看看是如何加载的。

  1. DecorView被加载到Window当中

当我们调用ActivitystartActivity方法的时候,最终调用的是ActivityThreadhandleLaunchActivity方法来创建Activity

public Activity handleLaunchActivity(ActivityClientRecord r,PendingTransactionActions pendingActions, Intent customIntent) {
.......//通过调用 performLaunchActivity(r, customIntent) 方法尝试启动一个 Activity。这个方法会创建 Activity 实例,并调用其 onCreate 方法,完成 DecorView 的创建。这个方法返回一个 Activity 对象,或者在失败时返回 nullfinal Activity a = performLaunchActivity(r, customIntent);if (a != null) {//为 ActivityClientRecord 对象 r 设置一个新的 Configuration 对象,该对象包含当前设备的配置信息r.createdConfig = new Configuration(mConfigurationController.getConfiguration());//用于报告 Activity 的大小配置,可能用于调试或日志记录reportSizeConfigurations(r);//如果 r.activity.mFinished 为 false(即 Activity 没有完成)且 pendingActions 不为 null,则设置 pendingActions 的一些状态if (!r.activity.mFinished && pendingActions != null) {//设置旧的状态;标记需要恢复实例状态;标记需要调用 onPostCreate 方法pendingActions.setOldState(r.state);pendingActions.setRestoreInstanceState(true);pendingActions.setCallOnPostCreate(true);}} else {// 调用 ActivityClient.getInstance().finishActivity 方法,通知活动管理器停止当前的 Activity。r.token用于标识当前的Activity;Activity.RESULT_CANCELED结果码,标识操作被取消或失败ActivityClient.getInstance().finishActivity(r.token, Activity.RESULT_CANCELED,null /* resultData */, Activity.DONT_FINISH_TASK_WITH_ACTIVITY);}return a;
}

上面的performLaunchActivity方法是用来创建Activity,里面会调用ActivityonCreate方法,从而完成DecorView的创建,来看看源码:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {......//将Activity实例设置到ActivityClientRecord中r.activity = activity;//检查Activity是否是持久化的。持久化的Activity可以在系统配置更改(如屏幕旋转)时保存和恢复状态if (r.isPersistable()) {//如果Activity是持久化的,调用Activity的onCreate方法,并传递临时状态和持久状态mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);} else {//否则不传入持久状态mInstrumentation.callActivityOnCreate(activity, r.state);}if (!activity.mCalled) {throw new SuperNotCalledException("Activity " + r.intent.getComponent().toShortString() +" did not call through to super.onCreate()");}r.mLastReportedWindowingMode = config.windowConfiguration.getWindowingMode();}r.setState(ON_CREATE);} catch (SuperNotCalledException e) {throw e;} catch (Exception e) {if (!mInstrumentation.onException(activity, e)) {throw new RuntimeException("Unable to start activity " + component+ ": " + e.toString(), e);}}return activity;
}

我们看到会调用mInstrumentation.callActivityOnCreate方法,其实无论调用的是哪个方法,里面都是一样的,只是多了一个持久状态参数:

public void callActivityOnCreate(Activity activity, Bundle icicle,PersistableBundle persistentState) {//在调用Activity的onCreate方法之前执行的准备工作。这包括设置Activity的一些内部状态,或者进行其他必要的初始化操作prePerformCreate(activity);//调用Activity的performCreate方法,传递临时状态icicle和持久化状态persistentState。这个方法实际上会调用Activity的onCreate方法,并处理状态恢复activity.performCreate(icicle, persistentState);//在Activity的onCreate方法执行之后执行的后续工作。这包括进一步的初始化操作或者状态设置postPerformCreate(activity);
}

performCreate方法最终调用到了ActivityonCreate回调方法,onCreate方法中的setContentView方法就创建得到了DecorView,所以onCreate方法完成了DecorView的创建。

同理,onResume方法也是在ActivityThread中的handleResumeActivity方法中被调用的,我们之前提到的DecorView就是在这个方法里被添加进Window中的,我们接下来看这个handleResumeActivity的源码

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {......//检查Activity的窗口是否已经创建,Activity是否未完成,以及Activity是否将可见if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();获取到了DecorView对象View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);//获得了WindowManager对象,是用来将DecorView添加到Window中的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();}}//如果Activity从客户端可见if (a.mVisibleFromClient) {//如果窗口尚未添加,将其添加到窗口管理器if (!a.mWindowAdded) {a.mWindowAdded = true;wm.addView(decor, l);//1} else {//如果窗口已添加,通知Activity窗口属性已更改a.onWindowAttributesChanged(l);}}......
}

在上面我们获取到了WindowManagerWindowManager是一个接口并继承于ViewManager,之后调用了WindowManageraddView方法,WindowManager的实现类是WindowManagerImpl,所以实际调用的是WindowManagerImpladdView方法

public final class WindowManagerImpl implements WindowManager {表示该字段(mGlobal)不应该被应用程序代码直接使用,它是供系统内部使用的@UnsupportedAppUsage//声明了一个WindowManagerGlobal类型的私有成员变量mGlobal,并使用WindowManagerGlobal.getInstance()方法来初始化它。WindowManagerGlobal是一个全局的单例类,提供全局的窗口管理功能private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();......@Overridepublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyTokens(params);mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,mContext.getUserId());}......
}

调用了WindowManagerGlobaladdView方法

public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow, int userId) {......//ViewRootImpl root;View panelParentView = null;//确保以下代码块在同一时间只能被一个线程执行,防止多线程并发导致的数据不一致问题synchronized (mLock) {//说明这不是一个无窗口的会话,创建一个普通的ViewRootImpl实例if (windowlessSession == null) {root = new ViewRootImpl(view.getContext(), display);//如果windowlessSession不为null,说明这是一个无窗口的会话,创建一个ViewRootImpl实例,并传入windowlessSession和WindowlessWindowLayout} else {root = new ViewRootImpl(view.getContext(), display,windowlessSession, new WindowlessWindowLayout());}//为传入的view设置布局参数view.setLayoutParams(wparams);//将view添加到mViews列表中,这个列表管理所有的视图mViews.add(view);//将root添加到mRoots列表中,这个列表管理所有的ViewRootImpl实例mRoots.add(root);//将wparams添加到mParams列表中,这个列表管理所有的布局参数mParams.add(wparams);//调用root的setView方法,将view设置到root中,并传递布局参数、父视图和用户ID。这一步会启动视图的绘制流程try {root.setView(view, wparams, panelParentView, userId);} catch (RuntimeException e) {final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);if (viewIndex >= 0) {removeViewLocked(viewIndex, true);}throw e;}}
}

上面调用了setView方法将DecorView传进去,就这样将DecorView加载到了Window当中。

1.2ViewRootImpl的performTraversals方法

上面我们提到此时将DecorView加载到了Window当中,但此时界面并不会完全显示出来,因为View的流程并没有执行完,还需经过measurelayout以及draw才会将View绘制出来。如何进行绘制的呢?

ViewRootImpl还有一个方法performTraversals,这个方法使ViewTree开启了View的工作流程:

private void performTraversals() {....childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,lp.privateFlags);performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//1int width = host.getMeasuredWidth();int height = host.getMeasuredHeight();boolean measureAgain = false;if (lp.horizontalWeight > 0.0f) {width += (int) ((mWidth - width) * lp.horizontalWeight);childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);measureAgain = true;}if (lp.verticalWeight > 0.0f) {height += (int) ((mHeight - height) * lp.verticalWeight);childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);measureAgain = true;}if (measureAgain) {if (DEBUG_LAYOUT) Log.v(mTag,"And hey let's measure once more: width=" + width+ " height=" + height);performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}layoutRequested = true;}}} else {maybeHandleWindowMove(frame);}if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || windowAttributesChanged) {prepareSurfaces();}final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);boolean triggerGlobalLayoutListener = didLayout|| mAttachInfo.mRecomputeGlobalAttributes;if (didLayout) {performLayout(lp, mWidth, mHeight);//2if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {host.getLocationInWindow(mTmpLocation);mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],mTmpLocation[0] + host.mRight - host.mLeft,mTmpLocation[1] + host.mBottom - host.mTop);host.gatherTransparentRegion(mTransparentRegion);if (mTranslator != null) {mTranslator.translateRegionInWindowToScreen(mTransparentRegion);}if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {mPreviousTransparentRegion.set(mTransparentRegion);mFullRedrawNeeded = true;SurfaceControl sc = getSurfaceControl();if (sc.isValid()) {mTransaction.setTransparentRegionHint(sc, mTransparentRegion).apply();}}}if (DBG) {System.out.println("======================================");System.out.println("performTraversals -- after setFrame");host.debug();}}..........	.....if (!isViewVisible) {if (mPendingTransitions != null && mPendingTransitions.size() > 0) {for (int i = 0; i < mPendingTransitions.size(); ++i) {mPendingTransitions.get(i).endChangingAnimations();}mPendingTransitions.clear();}if (mSyncBufferCallback != null) {mSyncBufferCallback.onBufferReady(null);}} else if (cancelAndRedraw) {scheduleTraversals();} else {if (mPendingTransitions != null && mPendingTransitions.size() > 0) {for (int i = 0; i < mPendingTransitions.size(); ++i) {mPendingTransitions.get(i).startChangingAnimations();}mPendingTransitions.clear();}if (!performDraw() && mSyncBufferCallback != null) {//3mSyncBufferCallback.onBufferReady(null);}}....}

这个方法比较长,我们仍然截取我们需要的部分,可以看到在注释1,2,3处分别调用了performMeasureperformLayoutperformDraw三个方法,这三个方法会会调用对应的measurelayoutdraw三个方法,而这三个方法又会调用对应的onMeasureonLayoutonDraw三个方法。

在这里插入图片描述

对于ViewGroup来说,performTraversals方法会依次调用performMeasureperformLayoutperformDraw三个方法,这三个方法会依次执行对应的工作方法,最后会调用onMeasure等对应方法,在对应的onMeasure等方法中又会依次调用子Viewmeasure等流程,总之就是会递归执行。

2.理解MeasureSpec

在了解View的测量过程之前先来看看MeasureSpecMeasureSpecView的一个内部类,其封装了一个View的规格尺寸,包括View的宽高信息。MeasureSpec很大程度上决定了View的规格尺寸,在Measure流程中,系统会将ViewLayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。这里的宽高是测量的宽高,不一定等于View最终的宽高。

public static class MeasureSpec {//是一个位移量,用于确定测量模式在MeasureSpec整数值中的位置,即模式需要左移30位private static final int MODE_SHIFT = 30;//是一个掩码,用于从MeasureSpec整数值中提取测量模式,0x3二进制为11,左移30为正好占据32整数的高位private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//这是一个自定义的注解(@IntDef),用于定义测量模式的合法值:UNSPECIFIED、EXACTLY和AT_MOST@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})@Retention(RetentionPolicy.SOURCE)public @interface MeasureSpecMode {}//三种测量模式public static final int UNSPECIFIED = 0 << MODE_SHIFT;public static final int EXACTLY     = 1 << MODE_SHIFT;public static final int AT_MOST     = 2 << MODE_SHIFT;//创建一个MeasureSpec整数值,它结合了给定的尺寸(size)和模式(mode)public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << View.MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {//如果sUseBrokenMakeMeasureSpec为true,则简单地将尺寸和模式相加(这是一种不推荐的做法,因为它可能导致不正确的行为)if (sUseBrokenMakeMeasureSpec) {return size + mode;} else {//使用位运算将尺寸和模式组合成一个整数值return (size & ~MODE_MASK) | (mode & MODE_MASK);}}//创建一个安全的MeasureSpec值。如果sUseZeroUnspecifiedMeasureSpec为true且模式为UNSPECIFIED,则返回0;否则,调用makeMeasureSpec(size, mode)@UnsupportedAppUsagepublic static int makeSafeMeasureSpec(int size, int mode) {if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {return 0;}return makeMeasureSpec(size, mode);}//从给定的MeasureSpec值中提取测量模式@MeasureSpecModepublic static int getMode(int measureSpec) {return (measureSpec & MODE_MASK);}//从给定的MeasureSpec值中提取尺寸public static int getSize(int measureSpec) {return (measureSpec & ~MODE_MASK);}//调整给定的MeasureSpec值,根据delta值修改尺寸。如果模式是UNSPECIFIED,则不进行调整。如果调整后的尺寸为负数,则记录错误并将尺寸设置为static int adjust(int measureSpec, int delta) {final int mode = getMode(measureSpec);int size = getSize(measureSpec);if (mode == UNSPECIFIED) {return makeMeasureSpec(size, UNSPECIFIED);}size += delta;if (size < 0) {Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +") spec: " + toString(measureSpec) + " delta: " + delta);size = 0;}return makeMeasureSpec(size, mode);}public static String toString(int measureSpec) {int mode = getMode(measureSpec);int size = getSize(measureSpec);StringBuilder sb = new StringBuilder("MeasureSpec: ");if (mode == UNSPECIFIED)sb.append("UNSPECIFIED ");else if (mode == EXACTLY)sb.append("EXACTLY ");else if (mode == AT_MOST)sb.append("AT_MOST ");elsesb.append(mode).append(" ");sb.append(size);return sb.toString();}
}

根据常量可以看出它代表了32位的int值,通过将specModespecSize打包为一个int值来避免过多的对象内存分配,为了方便操作,提供了打包和解包方法,其中高两位代表specMode(测量模式),低30位则代表specSize(测量大小)。

specMode有三种模式:

  • UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量,表示一种测量的状态
  • EXACTLY:精准模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是specSize的值
  • AT_MOST:最大模式,对应于LayoutParams中的wrap_content属性,子View的最终大小是父View指定的specSize值,并且子View的大小不能大于这个值

View的测量流程当中,通过makeMeasureSpec来保存宽和高的信息。通过getMode或者getSize得到模式的宽和高。MeasureSpec是受自身的LayoutParams和父容器的MeasureSpec共同影响的,作为顶层ViewDecorView来说,其并没有父容器,它的MeasureSpec是如何得到的,返回到ViewRootImplperformTraversals方法:

//调用getRootMeasureSpec方法来创建子视图的宽度测量规格。这个方法会根据父容器的尺寸、子视图的布局参数和私有标志来确定子视图的宽度测量规格
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,lp.privateFlags);
//调用performMeasure方法来执行子视图的测量操作。这个方法会使用前面创建的宽度和高度测量规格来确定子视图的尺寸
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

在注释1处调用了getRootMeasureSpec方法,看看他都做了什么:

//windowSize指的是窗口的尺寸
private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {int measureSpec;//这行代码检查 privateFlags 是否包含 PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT 标志。如果包含,rootDimension 被设置为 MATCH_PARENT,否则使用 measurement 的值final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0? MATCH_PARENT : measurement;switch (rootDimension) {//如果 rootDimension 是 MATCH_PARENT,表示根视图想要填满父容器的大小。因此,measureSpec 被设置为 windowSize 和 MeasureSpec.EXACTLY 模式,强制根视图的大小为窗口的大小case ViewGroup.LayoutParams.MATCH_PARENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;//如果 rootDimension 是 WRAP_CONTENT,表示根视图想要包裹其内容,但窗口可以调整大小。因此,measureSpec 被设置为 windowSize 和 MeasureSpec.AT_MOST 模式,为根视图设置最大尺寸case ViewGroup.LayoutParams.WRAP_CONTENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;//如果 rootDimension 是具体的尺寸值,这意味着根视图想要一个确切的大小。因此,measureSpec 被设置为 rootDimension 和 MeasureSpec.EXACTLY 模式,强制根视图的大小为 rootDimension 指定的大小default:measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec;
}

windowSize指的是窗口的尺寸,所以对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定,这一点和普通的View不同。

3.View的measure流程

measure用来测量View的宽和高。他的流程分别为Viewmeasure流程和ViewGroupmeasure流程。

3.1View的measure流程

先来看看View的onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension方法用于设置 View 的测量尺寸。具体而言,它用于设置 View 在进行测量过程中计算得出的宽度和高度。当一个 View 被测量时,系统会调用其 onMeasure() 方法来计算它的宽度和高度。在 onMeasure() 方法内部,会调用 setMeasureDimension() 方法来设置测量尺寸,将计算得出的宽度和高度保存起来。再来看看getDefaultSize方法:

public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {//如果模式是UNSPECIFIED,表示父视图对子视图的尺寸没有具体要求,因此结果保持为建议的尺寸sizecase MeasureSpec.UNSPECIFIED:result = size;break;//如果模式是AT_MOST或EXACTLY,表示父视图对子视图的尺寸有具体的要求,因此结果设置为specSize,即测量规格中指定的尺寸case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;}

根据上面我们知道不同模式下的返回值,也就是说,对于一个直接继承于View的自定义View来说,它的wrap_contentmatch_parent属性的效果是一样的。因此如果要实现自定义Viewwrap_content,则需要重写onMeasure方法,并对自定义Viewwrap_content属性进行处理。我们看到在UNSPECIFIED模式下返回的是第一个参数size的值,size的值由onMeasure方法看来是通过getSuggestedMinimumWidth方法或者getSuggestedMinimumHeight方法获取的,接下来就来看看这个方法做了什么:

protected int getSuggestedMinimumWidth() {return (mBackground == null) ? mMinWidth : max(mMinWidth,   mBackground.getMinimumWidth());
}

如果View没有背景,则取值为mMinWidth(这是视图的内部最小宽度,可能是在视图构造时设置的或者由视图自身逻辑决定的)。mMinWidth是可以设置的,它对应的属性Android:minWidth或者ViewsetMinimumWidth的值,如果不指定的话,默认值为0。如果View设置了背景,则比较 mMinWidth(视图的内部最小宽度)和 mBackground.getMinimumWidth()(背景图的最小宽度【当背景图设置了大小则为固有的大小,当没有设置则为0】)的大小,并返回两者中的最大值。

3.2ViewGroup的measure流程

前面提到了View的measure流程,接下来看看ViewGroup的measure流程,对于ViewGroup他不止要测量自身,还要遍历的调用子元素的measure方法,ViewGroup中没有定义onMeasure方法,但却定义了measureChildren方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {final int size = mChildrenCount;final View[] children = mChildren;for (int i = 0; i < size; ++i) {final View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {measureChild(child, widthMeasureSpec, heightMeasureSpec);}}}
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {//获得子元素的LayoutParams属性final LayoutParams lp = child.getLayoutParams();final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

一个一个遍历子元素,具体获得子View的MeasureSpec参数是在measureChild方法中,通过getChildMeasureSpec得到了子View的参数,传入的第一个参数是父容器的MeasureSpec,之前提到过子ViewMeasureSpec的转化会受到父容器的影响就是在这里体现,第二个参数是已经使用的空间,具体来说就是Padding参数(还有个方法是也考虑到Margin参数的),第三个参数是子View的布局参数LayoutParams。,接下来就看看是如何进行转换的:

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: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;case MeasureSpec.AT_MOST:if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
  • MeasureSpec.EXACTLY:如果父视图的模式是精确模式,子视图的尺寸由其布局参数决定。如果子视图的布局参数是具体尺寸,则子视图的测量模式也是精确模式;如果子视图的布局参数是MATCH_PARENT,则子视图的尺寸等于父视图的可用尺寸,模式也是精确模式;如果子视图的布局参数是WRAP_CONTENT,则子视图的尺寸等于父视图的可用尺寸,但模式是AT_MOST
  • MeasureSpec.AT_MOST:如果父视图的模式是最大模式,子视图的尺寸和模式与EXACTLY模式类似,但当子视图的布局参数是MATCH_PARENT时,子视图的模式是AT_MOST
  • MeasureSpec.UNSPECIFIED:如果父视图的模式是未指定模式,子视图的尺寸和模式取决于子视图的布局参数。如果子视图的布局参数是具体尺寸,则子视图的测量模式是精确模式;如果子视图的布局参数是MATCH_PARENTWRAP_CONTENT,则子视图的尺寸可能是0(如果View.sUseZeroUnspecifiedMeasureSpectrue)或父视图的可用尺寸,模式是未指定模式。

在这里插入图片描述

文章到这里就结束了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/486561.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4.opengl中变换

变换 1.向量 向量有一个方向(Direction)和大小(Magnitude&#xff0c;也叫做强度或长度)。 数学家喜欢在字母上面加一横表示向量&#xff0c;比如说vv。当用在公式中时它们通常是这样的&#xff1a; 1.1.向量相乘 1.1.1.点乘 我们该如何计算点乘呢&#xff1f;点乘是通过将…

聊聊开发一个接口用到哪些Swagger 注解

文章目录 常用swagger注解类注解方法注解字段注解 Swagger配置引入依赖编写配置类静态资源映射访问swagger ui 为什么要聊Swagger呢&#xff0c;原因是我发现实际开发中前端同事每次都需要问我枚举是什么&#xff0c;经过反思&#xff0c;我觉得是接口文档写的不够好。所以整理…

【Nginx系列】多个路径指向一个地址

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

fastadmin修改后台登录背景

背景 fastadmin 用习惯了&#xff0c;但是登录界面真的不好看&#xff0c;今天就修改一下。先看界面&#xff1a; 解决方案 1.安装插件&#xff1a;后台登录背景。 2.上传固定图片修改登录页面为如下&#xff1a; <!DOCTYPE html> <html> <head>{include…

Unreal Engine Groom打包没有物理模拟

编辑器中运行头发有物理模拟效果&#xff0c;打包后没有 Project Setting 加/HairStrands

MCU-USB转UART的底层逻辑

USB/UART Bridge: Understand Everything in Animations - Parlez-vous Tech 没有 USB 端口的 PC 如何与单片机通信&#xff1f; 答案就在 USB/UART 桥接器中。 无论是用于调试、连接 IoT 传感器还是进行工业监督&#xff0c;此桥接器都简化了硬件集成并使通信更加可靠。以…

仿真键盘输入遇到Edge环境不识别 回车符如何处理

这个问题我也是最近才遇到&#xff0c;可能现在大家都喜欢用新架构&#xff0c;基于网页来写应用管理软件。 当遇到Edge环境下&#xff0c;文本框不识别回车符如何处理&#xff0c;根据笔者经验可通过配置Edge 基于键盘管理设置来解决这个事情。如图 即在Edge浏览器环境下&…

在做题中学习(79):最小K个数

解法&#xff1a;快速选择算法 说明&#xff1a;堆排序也是经典解决问题的算法&#xff0c;但时间复杂度为&#xff1a;O(NlogK)&#xff0c;K为k个元素 而将要介绍的快速选择算法的时间复杂度为: O(N) 先看我的前两篇文章&#xff0c;分别学习&#xff1a;数组分三块&#…

【html网页页面009】html+css制作学校官网主题网页制作含登录(5页面附效果及源码)

校园网站主题网页制作 &#x1f964;1、写在前面&#x1f367;2、涉及知识&#x1f333;3、网页效果&#x1f308;4、网页源码4.1 html4.2 CSS4.3 源码获取w034学校网页源码及介绍链接 &#x1f40b;5、作者寄语 &#x1f964;1、写在前面 学校网站主题的网页 一共5个页面 网…

2024-12-08 数字人最新论文更新(MEMO, INFP, IF-MDM, SINGER, One Shot, One Talk, FLOAT等)

2024-12-08 数字人最新论文更新(MEMO, INFP, IF-MDM, SINGER, One Shot, One Talk, FLOAT等) 汇总一下最近一个星期的一些数字人论文的更新&#xff0c;我觉得比较有意思的一些文章比如SINGER&#xff0c;用Diffusion来做sing的talking head&#xff0c;确实是一个不错的文章&…

亚马逊云科技用生成式AI,向开发的复杂性动手了

生成式 AI、分布式扩展功能全面进化&#xff0c;还降价了。 同一天的发布&#xff0c;完全不同的方向。 今天凌晨&#xff0c;云计算巨头亚马逊云科技的 re:Invent 与大号创业公司 OpenAI 的发布「撞了车」。后者公布了一系列生成式 AI 应用&#xff0c;价格更贵、性能更强大&a…

HTML+CSS+JS实现简单的打字机

HTMLCSSJS实现简单的打字机 js /*** 动态打字效果函数* (select和element只能选择一个)* param {Object} options - 配置选项* param {string} options.select - 选择器&#xff0c;用于定位要显示文本的DOM元素("#id"或".class")* param {Object} optio…

[Collection与数据结构] 位图与布隆过滤器

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

探秘AES加密算法:多种Transformation全解析

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…

【Liunx篇】基础开发工具 - vim

文章目录 一.vim的基本概念1.正常/命令模式2.插入模式3.底行模式/末行模式4.视图模式5.替换模式 二.vim的基本操作1.进入vim&#xff1a;2.退出vim: 三.vim正常模式命令集1.光标定位&#xff1a;2.复制/粘贴3.撤销4.剪切/删除5. 更改 四.vim底行模式命令集1.保存/退出2.调出行号…

基于 Python、OpenCV 和 PyQt5 的人脸识别上课打卡系统

大家好&#xff0c;我是Java徐师兄&#xff0c;今天为大家带来的是基于 Python、OpenCV 和 PyQt5 的人脸识别上课签到系统。该系统采用 Python 语言开发&#xff0c;开发过程中采用了OpenCV框架&#xff0c;Sqlite db 作为数据库&#xff0c;系统功能完善 &#xff0c;实用性强…

在Linux(ubuntu22.04)搭建rust开发环境

1.安装rust 1.安装curl: sudo apt install curl 2.安装rust最新版 curl --proto ‘https’ --tlsv1.2 https://sh.rustup.rs -sSf | sh 安装完成后出现&#xff1a;Rust is installed now. Great! 重启当前shell即可 3.检验是否安装成功 rustc --version 结果出现&…

手机租赁系统全面解析与开发指南

内容概要 手机租赁系统已经成为现代商业中不可或缺的一部分&#xff0c;尤其是在智能手机普及的时代。随着消费者对新机型兴趣的不断增加&#xff0c;大家纷纷走上了“试一试再买”的道路&#xff0c;手机租赁这条路因此越走越宽。这部分的市场需求让创业者们看到了机会。不仅…

vue vxe-table 实现财务记账凭证

使用 vxe-table 实现财务记账凭证非常简单&#xff0c;实现在线实时编辑的记账凭证、自动合计金额等 官网&#xff1a;https://vxetable.cn/ <template><div><vxe-grid ref"gridRef" v-bind"gridOptions" v-on"gridEvents">&…

OpenNebula 开源虚拟平台,对标 VMware

Beeks Group 主要为金融服务提供商提供虚拟专用服务器和裸机服务器。该公司表示&#xff0c;转向 OpenNebula 不仅大幅降低了成本&#xff0c;还使其虚拟机效率提升了 200%&#xff0c;并将更多裸机服务器资源用于客户端负载&#xff0c;而非像以往使用 VMware 时那样用于虚拟机…