浅析Android的绘制流程以及自定义流式布局实战

在这里插入图片描述

Android View的绘制流程以及自定义流式布局实战

这篇文章开头分析view的绘制流程,笔者分为4步进行展开

  • 起点
  • measure
  • layout
  • draw

起点

解析之前补充一些相关的小知识

Window

View都是依附于Window的,Window是虚拟的概念,他并不是真实存在的,只是说在Android中,视图处理都需要经过WindowManagerServiceWindowManagerService管理的则是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的创建

起点是ActivitysetContentView()方法,具体的调用栈如下图:

最终调用到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的属性mDecormDecor会根据主题的值来确定布局,最终会返回idcontent 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

此处的childWidthMeasureSpecchildHeightMeasureSpecWMS计算的,上述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);
    }
}

在测量大小时,并不是使用具体的大小,而是借助了MeasureSpecView的静态内部类)

试想View的大小的影响因素

1.自身的设置(wrapmatch,具体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,调用CanvasAPI进行绘制

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);
        }
    }
    ...

}

新的循环继续进入Viewdraw方法,走上述一样的流程

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

实战

通过自定义View实现流式布局(其实现思路来源于享学课堂的Alvin老师)

布局则是ViewGroup,重点实现onMeasureonLayout

自定义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}{评论,你的意见是我进步的财富!}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值