文章目录
Android View的绘制流程以及自定义流式布局实战
这篇文章开头分析view的绘制流程,笔者分为4步进行展开
- 起点
- measure
- layout
- draw
起点
解析之前补充一些相关的小知识
Window
View
都是依附于Window
的,Window
是虚拟的概念,他并不是真实存在的,只是说在Android
中,视图处理都需要经过WindowManagerService
,WindowManagerService
管理的则是Window
。
可以这么理解一个Window
等价于一个View
树。
综上所述:
可以这么理解程序员所写的布局最终也是依附于Window
的。,下面分析Window
和根布局的创建流程
Activity
的启动最终会执行performLaunchActivity
PhoneWindow的创建
ActivityThread#performLaunchActivity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
appContext.setOuterContext(activity);
//绑定window
activity.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,
r.assistToken);
...
//执行oncreat
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
return activity;
}
Activity#attach
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
...
//创建window,PhoneWindow时window的唯一实现类
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
DecorView的创建
起点是Activity
的setContentView()
方法,具体的调用栈如下图:
最终调用到PhoneWindow#installDecor
方法
PhoneWindow#installDecor
private void installDecor() {
...
//创建DecorView
mDecor = generateDecor(-1);
...
//拿到程序员可操作的真正的根布局
mContentParent = generateLayout(mDecor);
//对mContentParent进行初始化操作
...
}
PhoneWindow#generateLayout
protected ViewGroup generateLayout(DecorView decor) {
//提取主题中的值
...
int layoutResource;
//根据主题中的值拿到布局,这里只是举例(此布局比较简单),layoutResource可能还有很多种情况,后续分析R.layout.screen_simple
layoutResource = R.layout.screen_simple;
mDecor.startChanging();
//设置布局下边分析 DecorView#onResourcesLoaded
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//获取xml中content id的view,后续xml中是一个FrameLayout
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}
DecorView#onResourcesLoaded
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
//获取布局
final View root = inflater.inflate(layoutResource, null);
...
//添加root
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) root;
}
分析一个比较简单的布局
R.layout.screen_simple
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
回过头看DecorView
创建的开始方法
AppCompatDelegateImpl#setContentView
@Override
public void setContentView(int resId) {
//创建
ensureSubDecor();
//找到上述分析的id为content的FrameLayout
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
//将布局解析并添加到contentParent
LayoutInflater.from(mContext).inflate(resId, contentParent);
}
inflate的添加过程在笔者的另一篇文章中已经解析
总结:在Activity
启动时创建Window
,在调用setContentView
方法时初始化Window
的属性mDecor
,mDecor
会根据主题的值来确定布局,最终会返回id
为content
的FrameLayout
作为程序员布局的根布局。
2022.5.13更新 上述对DecorView
的创建流程的解析并不准确,详情请看DecorView创建时的坑
绘制的开始
ActivityThread#handleResumeActivity
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//creat创建的decor
View decor = r.window.getDecorView();
//获取windowManager
ViewManager wm = a.getWindowManager();
//添加
wm.addView(decor, l);
}
WindowManagerImpl#addView
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
WindowanagerGlobal#addView
WindowManagerGlobal
是全局单例
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
//创建ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
//维护整个app的view,root
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
root.setView(view, wparams, panelParentView);
}
ViewRootImpl#setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//绘制的开始
requestLayout();
//binder跨进程与WMS通信
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
//将计算的大小结果保存,后续measure时会用到
setFrame(mTmpFrame);
}
ViewRootImpl#requestLayout
public void requestLayout() {
...
//发送消息执行绘制流程
scheduleTraversals();
}
ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//handler同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//执行绘制,Choreographer是刷新协调者这里不进行解析,只需要知道在这要执行绘制
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
TraversalRunnable
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
ViewRootImpl#doTraversal
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
//执行绘制
performTraversals();
}
}
ViewRootImpl#performTraversals
private void performTraversals() {
...
//measur过程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//layout过程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//draw过程
performDraw();
}
View
的绘制需要先测量,再布局,再绘制。
对于ViewGroup
来讲重点是测量和布局,因为其职责就是摆放子View
。
而对于View
来讲重点是测量和绘制,因为其职责就是某种具体的效果
measure
ViewRootImpl#performMeasure
此处的childWidthMeasureSpec
和childHeightMeasureSpec
是WMS
计算的,上述ViewRootImpl#setView
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
...
//测量
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View#measure
opublic final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//绘制
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
View#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置大小
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
分析LinearLayout
的测量,根据方向不同执行不同的方法,其内部会去遍历子View
,来判断自己有多大
LinearLayout#onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
在测量大小时,并不是使用具体的大小,而是借助了MeasureSpec
(View
的静态内部类)
试想View
的大小的影响因素
1.自身的设置(wrap
,match
,具体dp
)
2.以及父亲的大小
MeasureSpec
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//三种模式
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;
上述分析一个View
大小的影响因素很多,仅仅是简单的数字表示是不够的,MeasureSpec
使用一个32位的int
来表示
安卓钟爱位运算,前两位是mode
,后30
位是size
一个View
的大小情况无非三种,确切多大,最大多大,无法确定
无论何种影响因素都属于这三种情况的某一种,要注意的是MeasureSpec
只是参考测量值
MeasureSpec#makeMeasureSpec
创建MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
在onMeasure
肯定需要遍历所有的子View
,想要获取子的参考值,就需调用下面方法,spec
是父亲给的,子需要参照自己的属性值和父亲来确定自己的参考值
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
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;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
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;
// Parent asked to see how big we want to be
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
以上代等价于下图
layout
ViewRootImpl#performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
View#layout
public void layout(int l, int t, int r, int b) {
...
//执行布局
onLayout(changed, l, t, r, b);
...
}
View#onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
空实现,分析一下ViewGroup
中的实现
ViewGroup#onLayout
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
抽象方法,因为不同的布局有自己不同的实现
draw
ViewRootImpl#performDraw
private void performDraw() {
...
//绘制成功为true,失败为false
boolean canUseAsync = draw(fullRedrawNeeded);
...
}
ViewRootImpl#draw
private void draw(boolean fullRedrawNeeded) {
...
//开始绘制
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
ViewRootImpl#drawSoftware
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
// Draw with software renderer.
final Canvas canvas;
...
//Surface底层的绘制者,canvas是从native拿到的
canvas = mSurface.lockCanvas(dirty);
...
//执行绘制
mView.draw(canvas);
return true;
}
View#draw
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. 绘制背景
* 2. 如有必要,保存画布的图层以准备褪色
* 3. 绘制视图的内容
* 4. 绘制子
* 5. 如有必要,绘制褪色边缘并恢复图层
* 6. 绘制装饰(例如滚动条)
*/
// Step 1, 绘制背景
drawBackground(canvas);
// Step 2, 裁剪图层
...
// Step 3, 绘制自己
onDraw(canvas);
// Step 4, 绘制子view
dispatchDraw(canvas);
// Step 5, 绘制淡入淡出效果并恢复图层
...
// Step 6, 绘制装饰(前景、滚动条)
onDrawForeground(canvas);
}
2和5不是这篇文章关注的重点,主要解析主线流程,步骤二主要在裁剪画布时使用,可以查看笔者的另一篇文章,讲解了如何使用裁剪,裁剪则是在此处生效
Step 1, 绘制背景
View#drawBackground
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
//Drawable是接口提供了draw方法,任何背景最后都会解析成Drawable,只是Drawable的类型不同
background.draw(canvas);
}
Step 3, 绘制自己
View#onDraw
protected void onDraw(Canvas canvas) {
}
空实现,可以根据自身需要重写onDraw
,调用Canvas
的API
进行绘制
Step 4, 绘制子view
View#dispatchDraw
protected void dispatchDraw(Canvas canvas) {
}
空实现,可以根据自身子View的需要重写dispatchDraw
去调用子的Draw
方法,分析一下ViewGroup
中的实现
View#dispatchDraw
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
...
//获取子View
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//可见则绘制
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
新的循环继续进入View
的draw
方法,走上述一样的流程
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
实战
通过自定义View
实现流式布局(其实现思路来源于享学课堂的Alvin老师)
布局则是ViewGroup
,重点实现onMeasure
和onLayout
自定义View的构造
在实现之前对构造进行浅析
public class MyFlowLayout extends ViewGroup {
//一个参数的构造函数:View或者ViewGroup可以利用代码直接new对象,这时就会调用一个参数的构造函数,生成对象之后利用内部提供的属性设置方法就行属性设置。
public MyFlowLayout(Context context) {
super(context);
}
//xml编写的代码,在初始化反射调用此方法,笔者在下面文章讲解过view的创建
//https://blog.csdn.net/zjm807778317/article/details/123871148 attrs是xml中的值
public MyFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
//三个参数的构造函数:一般系统不会主动调用,需要手动调用,可以手动传入defStyleAttr并调用,即时在view中定义了them,style,也不会调用三参构造函数。defStyleAttr主题中的值
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//四个参数的构造函数,如果第三个参数为0或者没有定义defStyleAttr时,第四个参数才起作用,它是style的引用,高版本才支持,所以一般不会用到。defStyleRes view的默认样式
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
//必须实现抽象方法
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
}
MyFlowLayout
中的属性
private List<List<View>> allLines = new ArrayList<>(); // 记录所有的行,一行一行的存储,用于layout
List<Integer> lineHeights = new ArrayList<>(); // 记录每一行的行高,用于layout
private int mHorizontalSpacing = dp2px(16); //每个item横向间距
private int mVerticalSpacing = dp2px(8); //每个item横向间距
onMeasure
先解析思路,xml
中添加了很多的TextView
,在onMeasure
拿到总子数,对每个View
进行遍历,当前遍历的View
的宽加起来大于父总宽意味着换行,此行的高度由当前行最高的View
决定,换行则累加高度,最终的ViewGroup
的宽度为所有行中最宽的一行,高度为所有行的累计高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
allLines.clear();
lineHeights.clear();
//父给的参考宽高
int referWidth = MeasureSpec.getSize(widthMeasureSpec);
int referHeight = MeasureSpec.getSize(heightMeasureSpec);
//拿到所有的padding
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getBottom();
//获取所有的子总数
int childCount = getChildCount();
//当前行使用的宽度
int usedWidth = 0;
//当前行的宽度
int lineHeight = 0;
//经过对子View的测量后,自身需要的高度
int needWidth = 0;
int needHeight= 0;
//每行的view
List<View> lineViews = new ArrayList<>();
for (int i = 0; i < childCount; i++) {
//获取当前View
View child = getChildAt(i);
//如果不可见直接越过
if (child.getVisibility() == View.GONE) continue;‘
//子View的参数
LayoutParams childParams = child.getLayoutParams();
//获取子View的MeasureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingBottom + paddingTop, childParams.height);
//测量子View
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//拿到子View的参考宽高
int childMesauredWidth = child.getMeasuredWidth();
int childMeasuredHeight = child.getMeasuredHeight();
//换行逻辑
if (childMesauredWidth + mHorizontalSpacing + usedWidth > referWidth) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
//清空当前行
lineViews = new ArrayList<>();
//对宽高进行操作
needWidth = Math.max(needWidth, usedWidth + mHorizontalSpacing);
needHeight = needHeight + lineHeight + mVerticalSpacing;
//重置当前行的宽高,为下一行做准备
lineHeight = 0;
usedWidth = 0;
}
//添加view
lineViews.add(child);
//对宽高进行操作
lineHeight = Math.max(lineHeight, childMeasuredHeight);
usedWidth = usedWidth + mHorizontalSpacing + childMesauredWidth;
//对最后一行进行添加,否则会缺少一行
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
needHeight = needHeight + lineHeight + mVerticalSpacing;
needWidth = Math.max(needWidth, usedWidth + mHorizontalSpacing);
}
}
//拿到MeasureSpec的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//是否是确切大小,确切则使用确切的参考高度
int realWidth = (widthMode == MeasureSpec.EXACTLY) ? referWidth: needWidth;
int realHeight = (heightMode == MeasureSpec.EXACTLY) ? referHeight: needHeight;
//设置宽高
setMeasuredDimension(realWidth, realHeight);
}
onLayout
思路比较简单,读者自行理解
protected void onLayout(boolean change, int l, int t, int r, int b) {
int lineCount = allLines.size();
int curL = getPaddingLeft();
int curT = getPaddingTop();
for (int i = 0; i < lineCount; i++){
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for (int j = 0; j < lineViews.size(); j++){
View view = lineViews.get(j);
int left = curL;
int top = curT;
、
//getWidth和getMeasuredWidth的区别在下文解析
// int right = left + view.getWidth();
// int bottom = top + view.getHeight();
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left,top,right,bottom);
curL = right + mHorizontalSpacing;
}
curT = curT + lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
getWidth,getMeasuredWidth的区别
getWidth
获取的是layout
之后的值,getMeasuredWidth
获取的是measure
之后的值,在onLayout
使用getMeasuredWidth
是有效的,调用getWidth
是无效的,在draw
中调用两者都可,但是原则上讲getWidth
是准确的,getMeasuredWidth
可能不准确。
view#getWidth()
//mRight和mLeft在setFrame中赋值,setFrame被layout调用,因此可以这两个属性是由layout决定的,layout未执行则取不到
public final int getWidth() {
return mRight - mLeft;
}
view#getMeasuredWidth()
//mMeasuredWidth在setMeasuredDimensionRaw赋值,setMeasuredDimensionRaw被setMeasuredDimension调用,setMeasuredDimension又一定会在onMeasure调用,则可说mMeasuredWidth由onMeasure决定,不执行onMeasure则拿不到测量宽高
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
最终效果
demo可私信笔者
✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下
👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!} 点赞,你的认可是我创作的动力!
⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!} 收藏,你的青睐是我努力的方向!
✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!