如何应对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解析,手写插件化换肤框架核心实现

欢迎三连

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

相关推荐
达文汐2 小时前
【困难】力扣算法题解析LeetCode332:重新安排行程
java·数据结构·经验分享·算法·leetcode·力扣
培风图南以星河揽胜2 小时前
Java版LeetCode热题100之零钱兑换:动态规划经典问题深度解析
java·leetcode·动态规划
Dxy12393102163 小时前
MySQL如何加唯一索引
android·数据库·mysql
启山智软3 小时前
【中大企业选择源码部署商城系统】
java·spring·商城开发
我真的是大笨蛋3 小时前
深度解析InnoDB如何保障Buffer与磁盘数据一致性
java·数据库·sql·mysql·性能优化
怪兽源码3 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
恒悦sunsite3 小时前
Redis之配置只读账号
java·redis·bootstrap
梦里小白龙3 小时前
java 通过Minio上传文件
java·开发语言
人道领域3 小时前
javaWeb从入门到进阶(SpringBoot事务管理及AOP)
java·数据库·mysql
sheji52614 小时前
JSP基于信息安全的读书网站79f9s--程序+源码+数据库+调试部署+开发环境
java·开发语言·数据库·算法