如何应对Android面试官->手撸一个京东流式布局,MeasureSpec&LayoutParams 大揭秘

前言

本章主要介绍 LayoutParams 原理解析、MeasureSpec 原理解析、以及手撸一个京东流式布局;

自定义View

自定义 View 包含什么?

  1. 布局;onLayout、onMeasure 对应的是 ViewGroup
  2. 显示;onDraw 对应的是 View Canvas、Paint、Martix、Clip、Rect、Animation、Path、Line
  3. 事件分发;onTouchEvent 对应的是组合的 ViewGroup

自定义View的绘制流程?

自定义View

在没有现成的 View,需要自己实现的时候,就是用自定义 View,一般继承自 View,SurfaceView 或者其他 View;

自定义ViewGroup

自定义 ViewGroup 一般是利用现有的组件根据特定的布局方式组成新的组件,大多继承自 ViewGroup 或者各种 Layout;

所以,自定义 View 主要实现的是 onMeasure + onDraw;

自定义 ViewGroup 主要实现的是 onMeasure + onLayout;

开胃小菜

当我们自定义 layout 继承 ViewGroup 的时候 会要求我们实现几个不同参数的构造方法,那么这几个分别表示什么?

java 复制代码
public class FlowLayout extends ViewGroup {
    // Java 代码直接 new    
    public FlowLayout(Context context) {        
        super(context);    
    }
    // xml 声明的时候调用    
    public FlowLayout(Context context, AttributeSet attrs) {        
        super(context, attrs);    
    }
    // 自定义 style 的时候调用    
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {        
        super(context, attrs, defStyleAttr);    
    }
    // 自定义属性的时候调用    
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        
        super(context, attrs, defStyleAttr, defStyleRes);    
    }
    // 布局    
    @Override    
    protected void onLayout(boolean changed, int l, int t, int r, int b) {    
    }
    // 测量    
    @Override    
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
    }
}

FlowLayout

自定义 ViewGroup 主要就是实现 onMeasure 和 onLayout 方法;

onMeasure

在测量的时候,应该怎么测量?以及测量哪些内容?

测量子 View 和 自身,测量的时候,可以先测量自己在测量子 View,也可以先测量子 View,再测量自己;

先测量自己在测量子 View 的实例:ViewPager;

其他的大部分 ViewGroup 都是先测量子 View,再测量自己;

那么,具体子View怎么测量呢,自己怎么测量呢?

子 View 测量

ini 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
    // 先测量子View    
    int childCount = getChildCount();    
    for (int i = 0; i < childCount; i++) {        
        View child = getChildAt(i);        
        child.measure(widthMeasureSpec, heightMeasureSpec);    
    }
}

通过调用 measure 方法进行子 View 的测量;

测量自身

java 复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    
    // 再测量自己的高度    
    setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);}

通过调用 setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); 来测量自己并保存;

那么 widthMeasureSpec 和 heightMeasureSpec 具体怎么计算呢?

我们在布局的时候需要解析子 View 的 width 和 height 转换成具体的 dp 或者 dip

ini 复制代码
<Button    
    android:id="@+id/crateSAF"    
    android:layout_width="match_parent"    
    android:layout_height="wrap_content"    
    android:text="使用getFilesDir创建文件"    
    android:onClick="createFilesDir" />

也就是说我们需要将 match_parent、wrap_content 变成具体的值或者拿到子 View 设置的具体值;那么 match_parent 这些是什么呢? 它就是我们的 LayoutParams,我们可以进入 LayoutParams 的源码看下

arduino 复制代码
public static class LayoutParams {
    // 
    ...
    // 省略其他代码
    public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;
}

LayoutParams 是 ViewGroup 的一个静态内部类,我们发现 match_parent 的值是 -1, wrap_content 的值是 -2;这些对应的就是 xml 中我们设置的值;

因为 View 是以树形结构存在的,ViewGroup 的父类是 View,但是 ViewGroup 包含的子 View 是 View,那么当我们要测量子 View 的时候,需要递归遍历,因为 ViewGroup 始终受制与子 View 的宽高,如果 ViewGroup 的子 View 还是 ViewGroup 那么就需要继续测量这个 ViewGroup 的子 View;

我们需要将子 View 的宽高转换成具体的值,那么具体怎么转换呢?

ini 复制代码
LayoutParams layoutParams = child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, layoutParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, layoutParams.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

FlowLayout 的 onMeasure 方法中有两个入参数 int widthMeasureSpec, int heightMeasureSpec;

那么这两个值怎么来的呢?是它的父 View 传递进来的,假如 FlowLayout 被一个 LinearLayout 包裹,我们可以看下 LinearLayout 的 onMeasure 方法

scss 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    if (mOrientation == VERTICAL) {        
        measureVertical(widthMeasureSpec, heightMeasureSpec);    
    } else {        
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);    
    }
}

我们随便看一个方法,进入 measureVertical(widthMeasureSpec, heightMeasureSpec) 方法看一下

arduino 复制代码
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 
    ...
    // 省略部分代码
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,        
        heightMeasureSpec, usedHeight);
}

我们进入这个方法看一下:

arduino 复制代码
void measureChildBeforeLayout(View child, int childIndex,        
        int widthMeasureSpec, int totalWidth, int heightMeasureSpec,        
        int totalHeight) {    
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,            
        heightMeasureSpec, totalHeight);
}

我们进入这个方法看一下:

arduino 复制代码
protected void measureChildWithMargins(View child,        
        int parentWidthMeasureSpec, int widthUsed,        
        int parentHeightMeasureSpec, int heightUsed) {    
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();    
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,            
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin                    
            + widthUsed, lp.width);    
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,            
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin                    
            + heightUsed, lp.height);    
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

看到这里,和我们前面在 FlowLayout 中测量子 View 的宽高的实现其实是一样的,也就是说 FlowLayout 的 widthMeasureSpec 和 heightMeasureSpec 是由它的父 View 传递过来的,那么 FlowLayout 的子 View 需要的值,就由 FlowLayout 传递过去;

所以 ViewGroup 就是通过 getChildMeasureSpec 来获取子 View 的宽高具体值;那么这个方法具体做了什么?能让我们可以拿到具体的什么值?

这里面有五个比较重要的知识点;MeasureSpec类、getChildMeasureSpec方法、measure方法、getMode方法、getSize方法

MeasureSpec

MeasureSpec 是 View 对象的内部类,封装了父布局传递给子布局的布局要求,MeasureSpec 可以生成一个 32 位二进制组成的 int 值得测量规格,测量规格中装载了一种测量模式和一个 size;

int 类型的值 32 位,int 4个字节,每个字节是8位,所以是32位;

在 MeasureSpec 中,用一个 int 的值的高 2 位表示 mode(测量模式)低 30 位表示 size;

arduino 复制代码
private static final int MODE_SHIFT = 30;

MeasureSpec 中的这个 MODE_SHIFT 用这个值来表示位移;

arduino 复制代码
public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 0 << 30 (0向左位移30位)

public static final int EXACTLY     = 1 << MODE_SHIFT; // 1 << 30 (1向左位移30位)

public static final int AT_MOST     = 2 << MODE_SHIFT; // 2 << 30 (2向左位移30位)

UNSPECIFIED

未指定,父元素不对自身元素施加任何束缚,子元素可以得到任意想要的大小;

EXACTLY

确切的数值,如果当前控件的宽高是确切的值那么就给它定这个值,否则由父元素决定;对应的 xml 中的 match_parent,或者 具体的数值例如 100dp;

AT_MOST

至多不超过某个值,子元素最多达到指定大小的值(父控件的大小) ;对应的 xml 中的 wrap_content;

getChildMeasureSpec

这个方法用来获取子 View 的测量模式和 size;

ini 复制代码
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {    
    // 获取父容器的测量模式以及测量值
    int specMode = MeasureSpec.getMode(spec);
    // 获取父容器大小    
    int specSize = MeasureSpec.getSize(spec);    
    // 父容器 size 减去父容器的 padding 值之后是否大于0(因为我们的子控件在测量的时候要考虑到父控件的 padding 值)
    int size = Math.max(0, specSize - padding);    
    // 用来装子 View 的测量模式及大小
    int resultSize = 0;    
    int resultMode = 0;
    // 判断父控件的测量模式    
    switch (specMode) {    
        // 如果父空间的测量模式是 match_parent 的时候,则进入 exactly   
        case MeasureSpec.EXACTLY: // 父布局是 exactly 的时候       
            // 当前子控件的宽高设置的到底是不是具体的值
            if (childDimension >= 0) {
                // 如果子 View 设置的是具体的值,那么就把子 View 自己设置的值给它            
                resultSize = childDimension;            
                resultMode = MeasureSpec.EXACTLY;        
            } else if (childDimension == LayoutParams.MATCH_PARENT) {// 当前的子 View 设置的宽高是不是设置的 MATCH_PARENT            
                // 如果子 View 设置的是 MATCH_PARENT,那么就把父容器的size赋值给子 View         
                resultSize = size;            
                resultMode = MeasureSpec.EXACTLY;        
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {            
                // 如果子 View 设置的是 WRAP_CONTENT,那么就把父容器的size赋值给子 View            
                resultSize = size;            
                resultMode = MeasureSpec.AT_MOST;        
            }        
            break;    
        // Parent has imposed a maximum size on us    
        case MeasureSpec.AT_MOST: // 父布局是 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);
}

由此可以得出子 View 的测量生成规则是:

子 View 的规则获取之后,父 View 就需要调用 MeasureSpec.makeMeasureSpec() 方法去生成自己的测量规则;

measure

scss 复制代码
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

这个方法用来生成子 View 的宽高测量值

ini 复制代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {    
    boolean optical = isLayoutModeOptical(this);    
    if (optical != isLayoutModeOptical(mParent)) {        
        Insets insets = getOpticalInsets();        
        int oWidth  = insets.left + insets.right;        
        int oHeight = insets.top  + insets.bottom;        
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);        
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);    
    }
    //
    ...
    // 省略部分代码
}

进入 MeasureSpec.adjust() 看下:

arduino 复制代码
static int adjust(int measureSpec, int delta) {    
    final int mode = getMode(measureSpec);    
    int size = getSize(measureSpec);    
    if (mode == UNSPECIFIED) {        
        // No need to adjust size for UNSPECIFIED mode.        
        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);
}

进入 makeMeasureSpec() 看下:

less 复制代码
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, 
            @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
       return size + mode;
    } else {
       return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

主要是 else 逻辑:size & ~MODE_MASK) | (mode & MODE_MASK;

~ 非运算符,一元操作符,生成与输入位相反的值,若出入0,则生成1,若出入1,则生成0;

& 与运算符,二元操作符,操作两个二进制数据;两个二进制数最低位对齐;只有当两个对位数都是1时才为1,否则为0;

| 或运算符,二元操作符,操作两个二进制数,两个二进制数最低位对齐,当两个对位数只要有一个是1则为1,否则为0;

最终的二进制结果为:01 0000000000000000001111101000;

getMode

arduino 复制代码
public static int getMode(int measureSpec) {    
    //noinspection ResourceType    
    return (measureSpec & MODE_MASK);
}

measureSpec 的二进制: 01 0000000000000000001111101000;

MODE_MASK二进制: 11 00000000000000000000000000;

&运算之后 01 000000000000000000000000000000 mode 就是 EXACTLY;

getSize

arduino 复制代码
public static int getSize(int measureSpec) {    
    return (measureSpec & ~MODE_MASK);
}

measureSpec 的二进制: 01 0000000000000000001111101000;

~MODE_MASK二进制: 00 111111111111111111111111111111111111111111;

& 运算之后 00 0000000000000000001111101000 size 就是1000dp;

假设布局的宽度设置成 1000dp,经过上面五个知识点之后,那么这个方法的参数中 size 就是1000,mode 就是 EXACTLY;

FlowLayout 中获取测量的子 View 宽高

ini 复制代码
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();

FlowLayout 中测量判断是否需要换行

ini 复制代码
// 判断是否需要换行
if (measuredWidth + lineWidthUsed + mHorizonalSpacing > selfWidth ) {    
    // 换行
    lineViews.clear();
    lineWidthUsed = 0;
    lineHeightUsed = 0;
}

FlowLayout 中记录每行需要的宽度和高度

ini 复制代码
lineViews.add(child);
lineWidthUsed = lineWidthUsed + measuredWidth + mHorizontalSpacing;
lineHeightUsed = Math.max(lineHeightUsed, measuredHeight);

获取所有子 View 的宽和高,FlowLayout 设置给自己并保存

ini 复制代码
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth : parentNeedWidth;
int realHeight = (heightMode == MeasureSpec.EXACTLY) ? selfHeight : parentNeedHeight;
// 再测量自己的高度
setMeasuredDimension(realWidth, realHeight);

onLayout

View 的摆放,我们需要调用

scss 复制代码
view.layout(getLeft(),getTop(), getRight(),getBottom());

那么摆放的时候,需要知道在屏幕上的坐标,Android 提供了两种坐标系:屏幕坐标系、视图坐标系

屏幕坐标系

视图坐标系

ini 复制代码
protected void onLayout(boolean changed, int l, int t, int r, int b) {   
    int lineCount = allViews.size();   
    int paddingLeft =  getPaddingLeft();   
    int paddingTop = getPaddingTop();    
    for (int i = 0; i < lineCount; i++) {        
        List<View> lineViews = allViews.get(i);        
        int lineHeight = lineHeights.get(i);        
        for (int j = 0; j < lineViews.size(); j++) {            
            View view = lineViews.get(j);            
            int left = paddingLeft;            
            int top = paddingTop;            
            int right = left + view.getMeasuredWidth();            
            int bottom = top + view.getMeasuredHeight();            
            view.layout(left, top, right, bottom);            
            paddingLeft = right + mHorizontalSpacing;        
        }        
        paddingTop = paddingTop + lineHeight + mVerticalSpacing;        
        paddingLeft = getPaddingLeft();    
    }
}

getMeasureHeight 和 getHeight 的区别

getMeasureWidth 在 measure 过程结束后就可以获取到对应的值,通过 setMeasureDimension 方法进行设置;

getWidth 在 layout 过程结束后才能获取到,通过视图右边的坐标减去左边的坐标计算出来的;

最终实现效果图

完整实现

ini 复制代码
public class FlowLayout extends ViewGroup {    
    private final int mHorizontalSpacing = dp2px(16); //每个item横向间距    
    private final int mVerticalSpacing = dp2px(8); //每个item横向间距    
    private List<List<View>> allViews = new ArrayList<>(); // 记录所有行,用来layout    
    private List<Integer> lineHeights = new ArrayList<>(); // 记录每行的高度,用来layout    
    public FlowLayout(Context context) {        
        super(context);    
    }    
    public FlowLayout(Context context, AttributeSet attrs) {        
        super(context, attrs);    
    }    
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {        
        super(context, attrs, defStyleAttr);    
    }    
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        
        super(context, attrs, defStyleAttr, defStyleRes);    
    }    
    private void clearMeasureParams() {        
        allViews.clear();        
        lineHeights.clear();    
    }    
    
    @Override    
    protected void onLayout(boolean changed, int l, int t, int r, int b) {       
        int lineCount = allViews.size();       
        int paddingLeft =  getPaddingLeft();       
        int paddingTop = getPaddingTop();       
        for (int i = 0; i < lineCount; i++) {            
            List<View> lineViews = allViews.get(i);            
            int lineHeight = lineHeights.get(i);            
            for (int j = 0; j < lineViews.size(); j++) {                
                View view = lineViews.get(j);                
                int left = paddingLeft;                
                int top = paddingTop;                
                int right = left + view.getMeasuredWidth();                
                int bottom = top + view.getMeasuredHeight();                
                view.layout(left, top, right, bottom);                
                paddingLeft = right + mHorizontalSpacing;            
            }            
            paddingTop = paddingTop + lineHeight + mVerticalSpacing;           
            paddingLeft = getPaddingLeft();        
        }    
    }    
    @Override    
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        
        clearMeasureParams();        
        // 先测量子View        
        int childCount = getChildCount();        
        // 获取子View的左 padding        
        int paddingLeft = getPaddingLeft();        
        // 获取子View的右 padding        
        int paddingRight = getPaddingRight();        
        // 获取子View的上 padding        
        int paddingTop = getPaddingTop();        
        // 获取子View的下 padding        
        int paddingBottom = getPaddingBottom();        
        int selfWidth = MeasureSpec.getSize(widthMeasureSpec); // ViewGroup解析父容器传递过来的宽度        
        int selfHeight = MeasureSpec.getSize(heightMeasureSpec); // ViewGroup解析父容器传递过来的高度        
        List<View> lineViews = new ArrayList<>(); // 记录每行显示的所有View        
        int lineWidthUsed = 0; // 记录当前行已经使用的宽度        
        int lineHeightUsed = 0; // 记录当前行已经使用的高度        
        int parentNeedWidth = 0; // measure过程中,子View要求的父ViewGroup的宽        
        int parentNeedHeight = 0; // measure过程中,子View要求的父ViewGroup的高        
        for (int i = 0; i < childCount; i++) {            
            View child = getChildAt(i);            
            LayoutParams layoutParams = child.getLayoutParams();            
            if(child.getVisibility() != View.GONE) {                
                int childWidthMeasureSpec =                        
                    getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, layoutParams.width);                
                int childHeightMeasureSpec =                        
                    getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, layoutParams.height);                
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);                
                // 获取测量的子View的宽度                
                int measuredWidth = child.getMeasuredWidth();                
                // 获取测量的子View的高度                
                int measuredHeight = child.getMeasuredHeight();                
                // 判断是否需要换行                
                if (measuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth ) {                    
                    allViews.add(lineViews);                    
                    lineHeights.add(lineHeightUsed);                    
                    // 换行,数据清除                    
                    parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed + mHorizontalSpacing);                    
                    parentNeedHeight = parentNeedHeight + lineHeightUsed + mVerticalSpacing;                    
                    lineViews = new ArrayList<>();                    
                    lineWidthUsed = 0;                    
                    lineHeightUsed = 0;                
                }                
                lineViews.add(child);                
                lineWidthUsed = lineWidthUsed + measuredWidth + mHorizontalSpacing;                
                lineHeightUsed = Math.max(lineHeightUsed, measuredHeight);                
                // 判断是否是最后一行                
                if (i == childCount - 1) {                    
                    allViews.add(lineViews);                    
                    lineHeights.add(lineHeightUsed);                    
                    parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed + mHorizontalSpacing);                    
                    parentNeedHeight = parentNeedHeight + lineHeightUsed + mVerticalSpacing;                
                }            
            }        
        }        
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        
        int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth : parentNeedWidth;        
        int realHeight = (heightMode == MeasureSpec.EXACTLY) ? selfHeight : parentNeedHeight;        
        // 再测量自己的高度        
        setMeasuredDimension(realWidth, realHeight);    
    }    
    public static int dp2px(int dp) {        
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());    
    }
}

简历润色

简历上可写:深度理解MeasureSpec&LayoutParams,可基于此实现复杂ViewGroup

下一章预告

布局原理和xml解析,手写插件化换肤框架核心实现

欢迎三连

来都来了,点个关注点个赞吧,你的支持是我最大的动力~~

相关推荐
公贵买其鹿26 分钟前
List深拷贝后,数据还是被串改
java
xlsw_4 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭5 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫5 小时前
泛型(2)
java
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
南宫生5 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
拭心5 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
李小白665 小时前
Spring MVC(上)
java·spring·mvc