前言
本章主要介绍 LayoutParams 原理解析、MeasureSpec 原理解析、以及手撸一个京东流式布局;
自定义View
自定义 View 包含什么?
- 布局;onLayout、onMeasure 对应的是 ViewGroup
- 显示;onDraw 对应的是 View Canvas、Paint、Martix、Clip、Rect、Animation、Path、Line
- 事件分发;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解析,手写插件化换肤框架核心实现
欢迎三连
来都来了,点个关注点个赞吧,你的支持是我最大的动力~~