1 背景前言
一直说自定义View那么到底什么是自定义View?自定义View其实就是实现UI效果,也可以这样认为:一个UI效果只要它能够在手机上面实现作为Android开发者就应该具备实现这个UI效果的能力。
能够自定义View是搞android开发的基础,那么我们平时所说的自定义View是如何分类的呢?
1.1 自定义View的分类
1.1.1 自定义View
在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View。
1.1.2 自定义ViewGroup
自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout。
1.1.3 自定义View 包含什么
其实自定义View主要就是如下的3个方面:
- 布局: onLayout onMeausure / Layout:viewGroup
- 显示: onDraw :view: canvas paint matrix clip rect animation path(贝塞尔) line text绘制
- 交互: onTouchEvent :组合的viewGroup
我们来理解一下哈,假如说你现在有一个大房子(ViewGroup),那你要装修,那么你是不是就要先测量好房子的尺寸呢,那怎么测量呢,就是一个小房间一个小房间的测,测量好了每一个小房间,然后大房间的尺寸就有了,这就是onMeasure的作用,OnMeasure怎么测量呢?说穿了就是按照View的层级一个一个的测,View测量仅自己,ViewGroup不仅要测量自己,在测量自己之前还要先测量子View。这个房子你测量完了,那对于ViewGroup来讲就是房间内的每个小房子你如何布置在你的大房子里面,那就需要你的onLayout方法,onLayout就是管理你的子View(包含ViewGroup)的摆放在哪个位置的。但是
那假如说你现在的房子就是一个10平米的小开间(View),那你还需要去布局吗?这样的小空间的房子是没必要进行布局(onLayout)。这也就是View来说我们根本不需要去onLayout。
所以总结下来就是:
自定义View主要是实现 onMeasure + onDraw
自定义ViewGroup主要是实现onMeasure + onLayout
1.1.4 自定义View的绘制流程
View的绘制流程决定了我们对View的理解,当我们在自定义View的过程中,需要使用到一些值的时候,我们就必须了解这个值在哪个过程中进行的赋值,比如我们在onLayout方法中需要用到View大小的时候,就必须要在onMeasure方法之后使用,也就是说有时候我们在开发过程中使用一些值的时候,就会出现一些奇怪的现象,就是我们需要的一些值第一遍使用的时候没有值,第二遍使用的时候就有值了,但是第三遍使用的时候就是另外一个值,导致的使用混乱的效果。所以了解清晰自定义View的绘制流程。
上面讲了一些基础的概念,那么是不是感觉很空洞,似乎没有了解到什么东西或者说都是你知道的东西,确实有这种赶脚~~~,其实自定义View你讲一千遍一万遍,不如自己多动手写几个,这样的话,更能加深我们对自定义View的一些概念的理解。所以我们将会通过实现FlowLayout的布局一点一点的讲解一些概念,先看一下最终的实现效果:
红框中就是FlowLayout想要实现的效果。接下来我们看一下具体实现。
2 自定义FlowLayout
FlowLayout是一个ViewGroup,那么我们在实现时,是继承自ViewGroup的,那么我们先来创建FlowLayout类:
package com.donnycoy.flowlayout;import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;public class FlowLayout extends ViewGroup {// 代码中new创建一个控件时,使用该方法创建Viewpublic FlowLayout(Context context) {super(context);}// xml解析器解析xml文件时,使用反射技术会反射该方法创建Viewpublic FlowLayout(Context context, AttributeSet attrs) {super(context, attrs);}// 自定义Style(主题)时,使用该构造方法创建Viewpublic FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}// 自定义了属性的时候,会使用该构造方法创建View对象public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}
}
我们自定义有一个ViewGroup时,构造方法一般有4个。
- 一个参数:代码中new创建一个控件时,使用该方法创建View。
- 二个参数:xml解析器解析xml文件时,使用反射技术会反射该方法创建View。
- 三个参数:自定义Style(主题)时,使用该构造方法创建View。
- 四个参数:自定义了属性的时候,会使用该构造方法创建View对象,这个构造方法用的很少。
在这个位置我们有一个概念就是xml,也就是我们平时用的布局文件,其实我们平时用的布局文件(例如:)就一个序列化的文件,那么既然是序列化的文件,就要有一个解析器将xml文件中的节点和键值对转换成Java代码的类文件的形式。这个解析器就是我们平时所用的LayoutInflater类,他会将我们写的布局文件进行解析。
接下来看一下核心方法onMeasure和onLayout
public class FlowLayout extends ViewGroup {// 省略部分代码....// 度量@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// TODO}// 布局@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {// TODO}
}
先分析一下onMeasure方法,对于ViewGroup来说onMeasure一般情况下都会先测量孩子的view的大小,再测量ViewGroup自身的大小(有一个特殊的ViewGroup,那就是ViewPaper,在onMeasure中先测量自己再测量孩子)。
那么我们如何实现onMeasure呢?整体思路就是先度量孩子,再度量自己。
2.1 onMeasure实现
在我们实现自定义View的onMeasure方法之前,我们先来了解一些基础概念
2.1.1 layoutParams解析
android:layout_width="10dp"
android:layout_width="match_parent"
android:layout_width="wrap_content"
match_parent、wrap_content、具体值(比如10dp)在Java代码中是如何表现出来,那layoutParams就闪亮登场了,看一下源码:
// android/view/ViewGroup.javapublic static class LayoutParams {/*** Special value for the height or width requested by a View.* FILL_PARENT means that the view wants to be as big as its parent,* minus the parent's padding, if any. This value is deprecated* starting in API Level 8 and replaced by {@link #MATCH_PARENT}.*/@SuppressWarnings({"UnusedDeclaration"})@Deprecatedpublic static final int FILL_PARENT = -1;/*** Special value for the height or width requested by a View.* MATCH_PARENT means that the view wants to be as big as its parent,* minus the parent's padding, if any. Introduced in API Level 8.*/public static final int MATCH_PARENT = -1;/*** Special value for the height or width requested by a View.* WRAP_CONTENT means that the view wants to be just large enough to fit* its own internal content, taking its own padding into account.*/public static final int WRAP_CONTENT = -2;/*** Information about how wide the view wants to be. Can be one of the* constants FILL_PARENT (replaced by MATCH_PARENT* in API Level 8) or WRAP_CONTENT, or an exact size.*/@ViewDebug.ExportedProperty(category = "layout", mapping = {@ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),@ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")})@InspectableProperty(name = "layout_width", enumMapping = {@EnumEntry(name = "match_parent", value = MATCH_PARENT),@EnumEntry(name = "wrap_content", value = WRAP_CONTENT)})public int width;/*** Information about how tall the view wants to be. Can be one of the* constants FILL_PARENT (replaced by MATCH_PARENT* in API Level 8) or WRAP_CONTENT, or an exact size.*/@ViewDebug.ExportedProperty(category = "layout", mapping = {@ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),@ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")})@InspectableProperty(name = "layout_height", enumMapping = {@EnumEntry(name = "match_parent", value = MATCH_PARENT),@EnumEntry(name = "wrap_content", value = WRAP_CONTENT)})public int height;// 省略部分代码.....}
那就是xml文件与Java代码对应的映射关系如下
- match_parent ---> public static final int MATCH_PARENT = -1;
- wrap_content ---->public static final int WRAP_CONTENT = -2;
- 具体值(比如10dp) ----> public int width;
说穿了LayoutPramas就是一些View尺寸参数的Java表现形式,这些值是在LayoutInflater解析器将xml文件解析之后,转变成了LayoutPramas的表现形式。有个LayoutPramas的参数之后就是如何将这些值Java的xml表现形式的值转换成具体的dp或者dip,不要着急,磨刀不误砍柴工。在这之前先了解一下MeasureSpec。
2.1.2 MeasureSpec解析
在理解MeasureSpec之前先了解一下:View的层次机构。View在展现的角度上讲,他是如下的结构:
可以看到当从展示的角度看,View的层次结构是树形结构,ViewGroup既可以作为父节点也可以作为叶子结点。但是最终的叶子节点都是View。
但是从累的继承关系关系上来看的话:
ViewGroup是View的子类。但是在测量的时候都是用的视图层次结构进行测量。在视图层次中计算节点View大小的时候,还受到了其父节点和子节点的大小限制,具体要根据本身设置的三种类型(match_parent、wrap_content、具体值(10dp))的情况具体分析:
那从这2个例子也能分析出来,某一个节点的大小并不是一下就能直接到位具体计算出 xx dp,而是通过递归的方式将当前ViewGroup的大小计算出来:
理解了这里的递归调用,这个位置有个小点要提一下:就是上面说View会返回测量结果,但是看代码发现onMeasure方法的返回值都是void,那这个值是怎么返回的呢?其实测量结果并不是直接通过onMeasure的返回值返回的,而是存储到了当前子View的变量中,平时我们用的getMeasuredWidth() 和 getMeasuredHeight()就是用来获取测量完的子View的宽和高的;
再回到来说,当ViewGroup测量的时候调用自身的onMeasure方法的同时会触发到子View的Measure方法的调用:
当ViewGroup调用孩子(子View)onMeasure方法的时候,会传入2个参数:
这2个参数就是我们要讲解的MeasureSpec(绕了一大圈终于回到正题了),那到底MeasureSpec是个什么东西呢,先看源码:
// android/view/View.javapublic static class MeasureSpec {private static final int MODE_SHIFT = 30;private static final int MODE_MASK = 0x3 << MODE_SHIFT;/** @hide */@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})@Retention(RetentionPolicy.SOURCE)public @interface MeasureSpecMode {}/*** Measure specification mode: The parent has not imposed any constraint* on the child. It can be whatever size it wants.*/public static final int UNSPECIFIED = 0 << MODE_SHIFT;/*** Measure specification mode: The parent has determined an exact size* for the child. The child is going to be given those bounds regardless* of how big it wants to be.*/public static final int EXACTLY = 1 << MODE_SHIFT;/*** Measure specification mode: The child can be as large as it wants up* to the specified size.*/public static final int AT_MOST = 2 << MODE_SHIFT;/*** Creates a measure specification based on the supplied size and mode.** The mode must always be one of the following:* <ul>* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>* </ul>** <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's* implementation was such that the order of arguments did not matter* and overflow in either value could impact the resulting MeasureSpec.* {@link android.widget.RelativeLayout} was affected by this bug.* Apps targeting API levels greater than 17 will get the fixed, more strict* behavior.</p>** @param size the size of the measure specification* @param mode the mode of the measure specification* @return the measure specification based on size and mode*/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);}}// 省略部分代码.....
}
MeasureSpec是View中的内部类,基本都是二进制运算。由于int是32位的,用高两位表示mode,低30位表示size,MODE_SHIFT = 30的作用是移位。
什么意思呢:举个例子,一个View的大小最大能有多大呢?满屏足够了吧?那满屏的大小也无非就是屏幕的宽*屏幕的高,那个计算结果的数值用30位的bit位来存储的话是足够的,也就是说我们约定用一个30位来存储View的大小是绝对够用的。那剩下的2位干什么用:用来存储mode(有3种mode):
-
UNSPECIFIED:不对View大小做限制,系统使用。父View不知道你有多大,子View也不知道自己有多大。
EXACTLY:确切的大小,如:100dp
AT_MOST:大小不可超过某数值,如:matchParent, 最大不能超过你爸爸
也就是说MeasureSpec说穿了就是一个32位的int值,高2位用来存储View宽或者高所设置的模式,低30位用来存储View的大小。其实这么设计的根本原因就是我们上面也介绍了一个View的大小并不能一次性计算出来大小,因为View的大小受到父View和子View的大小的限制,不能一下就直接计算出来,只有到真正显示的时候,才能真正的根据父View和子View计算出来。那这种情况怎么办呢?也就只能先给一个参考View的大小,用30位来存储这个数据,然后再配上一个当前View大小的模式,也就是出现了MeasureSpec。
2.1.2.1 getChildMeasureSpec()算法分析
理解了MeasureSpec的结构,那么是如何将xml在Java代码中的表达形式LayoutParams转化为MeasureSpec的dp或者dip的表达形式呢?那就需要转换算法getChildMeasureSpec(),
理解算法前先举个例子:
假如说你要买房子,你现在钱不够,你需要跟你老爸去借一部分钱,那就有如下的几种情况:
那我们再来看算法getChildMeasureSpec算法:
// android/view/ViewGroup.java/*** Does the hard part of measureChildren: figuring out the MeasureSpec to* pass to a particular child. This method figures out the right MeasureSpec* for one dimension (height or width) of one child view.** The goal is to combine information from our MeasureSpec with the* LayoutParams of the child to get the best possible results. For example,* if the this view knows its size (because its MeasureSpec has a mode of* EXACTLY), and the child has indicated in its LayoutParams that it wants* to be the same size as the parent, the parent should ask the child to* layout given an exact size.** @param spec The requirements for this view* @param padding The padding of this view for the current dimension and* margins, if applicable* @param childDimension How big the child wants to be in the current* dimension* @return a MeasureSpec integer for the child*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {// 获取父View大小的模式int specMode = MeasureSpec.getMode(spec);// 获取父View的大小int specSize = MeasureSpec.getSize(spec);// 去掉父View的padding的值之后,真正子View可以在父View申请到的大小int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;// 首先根据父View的设计模式进行分类申请switch (specMode) {// 父view的空间大小是一个具体的值// Parent has imposed an exact size on uscase MeasureSpec.EXACTLY:if (childDimension >= 0) {// 子View的大小模式也是一个具体的值,那么将直接使用子View的大小作为分配的大小,当然View的大小模式也是实际值的模式resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// 如果子View的大小模式是最大模式,那么就将父View的大小全部分配给子View,当然View的大小模式也是实际值的模式// (因为父View的大小是一个实际值大小,子View的设计模式为全要模式,那肯定就是具体值了)// Child wants to be our size. So be it.resultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 如果子View的大小模式需要根据子子View的大小来确定,那也是将父View可申请的所有大小分配给子View,View的大小模式是最大值模式// 因为无论子子View的大小加起来是多少,不能超过这个父View的最大值// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// 父View的大小模式是最大值模式,也就是父View再向父父View申请最大值// Parent has imposed a maximum size on uscase MeasureSpec.AT_MOST:if (childDimension >= 0) {// 当子View的大小是具体值模式,那么将会直接使用子View的具体值的大小,当然View的大小模式也是实际值的模式// Child wants a specific size... so be itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// 当前子View的大小模式是父View有多少大小我要多少大小模式(最大模式),那么将会将父View的大小全部分配给子View的大小,View的大小模式设置为最大值模式。// 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.// 当子View的大小需要根据子子View大小的和来确定时,将会把当前父View的大小都给子View,然后View的大小模式是最大值模式// 因为无论子子View的大小加起来是多少,不能超过这个父View的最大值resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// 父View大小模式为根据具体子View的大小来确定时// Parent asked to see how big we want to becase MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// 当子View的大小是具体值模式,那么将会直接使用子View的具体值的大小,当然View的大小模式也是实际值的模式// Child wants a specific size... let them have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// 当前子View的大小模式是父View有多少大小我要多少大小模式(最大模式),那么将会将父View的大小全部分配给子View的大小,View的大小模式设置为不确定模式。// 因为无论子子View的大小加起来是多少,不能超过这个父View的最大值// Child wants to be our size... find out how big it should// beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 当子View的大小需要根据子子View大小的和来确定时,将会把当前父View的大小都给子View,然后View的大小模式是不确定模式// 因为无论子子View的大小加起来是多少,不能超过这个父View的最大值// Child wants to determine its own size.... find out how// big it should beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}//noinspection ResourceTypereturn MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
参数:
spec:父View的大小
padding: 父View的Padding的大小
childDimension:就是孩子的LayoutParams中的大小
算法的具体的理解就是上面的解释已经很清楚了,总结起来就是如下的表格:
经过这样的算法你也许会发现,其实有些情况下,计算的结果并不是一个实际具体值,而是一个最大不能超过的值(也就是常说的参考值)。所以这个算法计算结束之后,获取到的是一个参考值,而并非是一个具体的值。那要将上面的参考值变成具体的值,就需要调用子View的measure方法,计算完所有的子子View的结果之后,就会得到具体值,这也就是为什么不到最后展示的时候,getChildMeasureSpec的结果是一个参考值,而并非一个具体值。
经过getChildMeasureSpec的算法转换,就会将xml解析成的LayoutParams的数值转换成了MeasureSpec的具体dp值或者dip值。
2.1.3 测量子View实现
了解了基础的LayoutPrams和MeasureSpec的基础之后,又了解了getChildMeasureSpec算法之后,根据之前的思路先测量子View,再测量自己,那么在代码中先写一个框架代码(后面循序渐进的补充对应的实现)实现:
// com/donnycoy/flowlayout/FlowLayout.java// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 1. 先度量子View// 1.1 获取子View的个数int childCount = getChildCount();// 1.2 循环遍历所有的子Viewfor (int i = 0; i < childCount; i++) {// 1.2.1 获取到其中一个子ViewView childView = getChildAt(i);if (childView == null) {continue;}childView.measure(widthMeasureSpec, heightMeasureSpec);}// 2. 再度量自己并且保存setMeasuredDimension(width, height);
}
框架代码写好了, 那这个肯定是不能运行的,因为很多细节代码逻辑并未处理。先看一下测量孩子(子View)怎么做,测量孩子无非就是将孩子在xml文件中的layout_width和layout_height(如下所示代码)变成代码中具体的大小。
也就是说测量孩子(子View)的目的就是将XML文件中的layout_width和layout_height举个例子:
android:layout_width="10dp"
android:layout_width="match_parent"
android:layout_width="wrap_content"
变成dp或者dip。那这就需要一定的算法。getChildMeasureSpec() 闪亮登场(上面已经介绍过了),经过这个算法的计算,就能够将LayoutParams中的大小转换为MeasureSpec的表达形式的大小,也就是转换成了dp或者dip的大小。接下来将这部分的代码实现一下:
// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 1. 先度量子View// 1.1 获取子View的个数int childCount = getChildCount();// 获取padding的大小int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();// 1.2 循环遍历所有的子Viewfor (int i = 0; i < childCount; i++) {// 1.2.1 获取到其中一个子ViewView childView = getChildAt(i);if (childView == null) {continue;}// 1.2.2 获取子View的LayoutPramasLayoutParams childLP = childView.getLayoutParams();// 1.2.3 将layoutParams转变成为measureSpecint childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight, childLP.width);int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom, childLP.height);// 1.2.3-1:由于上面的方法给的一些值是参考值,并非具体值,需要经过此方法将子子View的大小都计算出来,然后才能将参考值变成具体值childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);}// 2. 再度量自己并且保存setMeasuredDimension(width, height);
}
到此测量子View已经完成了,那么接下来就是测量View自身。
2.1.4 测量本身实现
测量自身的思路就是我们的流式布局每一行最宽的行就是流式布局最大宽度,所有的行再加上行的间距就是流式布局的最大高度。那么看我们具体的实现:
// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 1. 先度量子View// 1.1 获取子View的个数int childCount = getChildCount();// 获取padding的大小int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();// 测量自己List<View> lineViews = new ArrayList<>(); //保存一行中的所有的viewint lineWidthUsed = 0; //记录这行已经使用了多宽的sizeint lineHeight = 0; // 一行的行高int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的宽度int selfHeight = MeasureSpec.getSize(heightMeasureSpec); // ViewGroup解析的父亲给我的高度// 1.2 循环遍历所有的子Viewfor (int i = 0; i < childCount; i++) {// 1.2.1 获取到其中一个子ViewView childView = getChildAt(i);if (childView == null) {continue;}// 1.2.2 获取子View的LayoutPramasLayoutParams childLP = childView.getLayoutParams();// 1.2.3 将layoutParams转变成为measureSpecint childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight, childLP.width);int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom, childLP.height);// 1.2.3-1:由于上面的方法给的一些值是参考值,并非具体值,需要经过此方法将子子View的大小都计算出来,然后才能将参考值变成具体值childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);// 1.2.4 获取到子View的宽和高int childMesauredWidth = childView.getMeasuredWidth();int childMeasuredHeight = childView.getMeasuredHeight();// 2.1 view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局lineViews.add(childView);//每行都会有自己的宽和高lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;lineHeight = Math.max(lineHeight, childMeasuredHeight);}// 2. 再度量自己并且保存setMeasuredDimension(width, height);
}
当一行摆满之后,我们需要换行操作,那就是当当前子View测量的宽度+本行已经使用的宽度+一个水平间隔的宽度 > 父类View给的宽度之后 就需要换行操作:
// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 1. 先度量子View// 1.1 获取子View的个数int childCount = getChildCount();// 获取padding的大小int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();// 测量自己List<View> lineViews = new ArrayList<>(); //保存一行中的所有的viewint lineWidthUsed = 0; //记录这行已经使用了多宽的sizeint lineHeight = 0; // 一行的行高int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的宽度int selfHeight = MeasureSpec.getSize(heightMeasureSpec); // ViewGroup解析的父亲给我的高度// 1.2 循环遍历所有的子Viewfor (int i = 0; i < childCount; i++) {// 1.2.1 获取到其中一个子ViewView childView = getChildAt(i);if (childView == null) {continue;}// 1.2.2 获取子View的LayoutPramasLayoutParams childLP = childView.getLayoutParams();// 1.2.3 将layoutParams转变成为measureSpecint childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight, childLP.width);int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom, childLP.height);// 1.2.3-1:由于上面的方法给的一些值是参考值,并非具体值,需要经过此方法将子子View的大小都计算出来,然后才能将参考值变成具体值childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);// 1.2.4 获取到子View的宽和高int childMesauredWidth = childView.getMeasuredWidth();int childMeasuredHeight = childView.getMeasuredHeight();// 2.2 如果需要换行if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {lineViews = new ArrayList<>();lineWidthUsed = 0;lineHeight = 0;}// 2.1 view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局lineViews.add(childView);//每行都会有自己的宽和高lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;lineHeight = Math.max(lineHeight, childMeasuredHeight);}// 2. 再度量自己并且保存setMeasuredDimension(width, height);
}
到此时我们的换行有了,每行的节点计算也有了,那么最重要的一点就要来了,就是我们遍历完子View之后,每次换行的时候就要统计一下当前已经使用的行的最宽宽度以及所有的行的高度,在这样的基础之上就计算出了当前View的宽度和高度,并且将其保存:
// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 1. 先度量子View// 1.1 获取子View的个数int childCount = getChildCount();// 获取padding的大小int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();// 测量自己List<View> lineViews = new ArrayList<>(); //保存一行中的所有的viewint lineWidthUsed = 0; //记录这行已经使用了多宽的sizeint lineHeight = 0; // 一行的行高int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的宽度int selfHeight = MeasureSpec.getSize(heightMeasureSpec); // ViewGroup解析的父亲给我的高度int parentNeededWidth = 0; // measure过程中,子View要求的父ViewGroup的宽int parentNeededHeight = 0; // measure过程中,子View要求的父ViewGroup的高// 1.2 循环遍历所有的子Viewfor (int i = 0; i < childCount; i++) {// 1.2.1 获取到其中一个子ViewView childView = getChildAt(i);if (childView == null) {continue;}// 1.2.2 获取子View的LayoutPramasLayoutParams childLP = childView.getLayoutParams();// 1.2.3 将layoutParams转变成为measureSpecint childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight, childLP.width);int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom, childLP.height);// 1.2.3-1:由于上面的方法给的一些值是参考值,并非具体值,需要经过此方法将子子View的大小都计算出来,然后才能将参考值变成具体值childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);// 1.2.4 获取到子View的宽和高int childMesauredWidth = childView.getMeasuredWidth();int childMeasuredHeight = childView.getMeasuredHeight();// 2.2 如果需要换行if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {// 2.3 累计统计当前已经使用的View的高度和最宽的宽度parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);// 换行时将统计行信息的变量重置lineViews = new ArrayList<>();lineWidthUsed = 0;lineHeight = 0;}// 2.1 view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局lineViews.add(childView);//每行都会有自己的宽和高lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;lineHeight = Math.max(lineHeight, childMeasuredHeight);}// 2. 再度量自己并且保存setMeasuredDimension(parentNeededWidth, parentNeededHeight);
}
但是上面这样写还有一个问题,就是当FlowLayout的父ViewGroup的宽或者高的大小是一个确切的值,比如说FlowLayout的父ViewGroup的宽高做如下的设置:
android:layout_width="100dp"
android:layout_height="100dp"
那么无论FlowLayout计算出来的View的大小是什么,都将定义为100dp的宽和高。
所以上面的代码在保存自己的宽和高之前,需要先判定一下父ViewGroup的大小的模式,如果是EXACTLY模式,那么都将使用父ViewGroup给的宽和高,代码做如下的修改:
// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 1. 先度量子View// 1.1 获取子View的个数int childCount = getChildCount();// 获取padding的大小int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();int paddingTop = getPaddingTop();int paddingBottom = getPaddingBottom();// 测量自己List<View> lineViews = new ArrayList<>(); //保存一行中的所有的viewint lineWidthUsed = 0; //记录这行已经使用了多宽的sizeint lineHeight = 0; // 一行的行高int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的宽度int selfHeight = MeasureSpec.getSize(heightMeasureSpec); // ViewGroup解析的父亲给我的高度int parentNeededWidth = 0; // measure过程中,子View要求的父ViewGroup的宽int parentNeededHeight = 0; // measure过程中,子View要求的父ViewGroup的高// 1.2 循环遍历所有的子Viewfor (int i = 0; i < childCount; i++) {// 1.2.1 获取到其中一个子ViewView childView = getChildAt(i);if (childView == null) {continue;}// 1.2.2 获取子View的LayoutPramasLayoutParams childLP = childView.getLayoutParams();// 1.2.3 将layoutParams转变成为measureSpecint childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight, childLP.width);int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom, childLP.height);// 1.2.3-1:由于上面的方法给的一些值是参考值,并非具体值,需要经过此方法将子子View的大小都计算出来,然后才能将参考值变成具体值childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);// 1.2.4 获取到子View的宽和高int childMesauredWidth = childView.getMeasuredWidth();int childMeasuredHeight = childView.getMeasuredHeight();// 2.2 如果需要换行if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {// 2.3 累计统计当前已经使用的View的高度和最宽的宽度parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);// 换行时将统计行信息的变量重置lineViews = new ArrayList<>();lineWidthUsed = 0;lineHeight = 0;}// 2.1 view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局lineViews.add(childView);//每行都会有自己的宽和高lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;lineHeight = Math.max(lineHeight, childMeasuredHeight);}// 2.4 根据子View的度量结果,来重新度量自己ViewGroup// 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来度量int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int realWidth = (widthMode == MeasureSpec.EXACTLY) ? selfWidth: parentNeededWidth;int realHeight = (heightMode == MeasureSpec.EXACTLY) ?selfHeight: parentNeededHeight;// 2. 再度量自己并且保存setMeasuredDimension(realWidth, realHeight);
}
到这里整体度量就完成了,我们可以看到,测量完所有的子View之后,获得的数值是FlowLayout的所有子View的需要的一个宽高值,那么在测量自己时,FlowLayout需要根据父ViewGroup的大小模式来最终决定FlowLayout自己的大小(也就是所谓的测量自己)。到此FlowLayout的测量工作完成,那么就到了FlowLayout布局操作。
2.2 onLayout实现
onlayout方法主要就是调用每个子View的layout方法,传入坐标系的4个顶点位置,那么在实现onLayout之前,需要先了解一下Android中的坐标系。
2.2.1 坐标系
android有2种坐标系,Android屏幕坐标系和视图坐标系:
Android屏幕坐标系
屏幕坐标系比较好理解,坐标原点就是屏幕的左上角,向右为X轴,向下为Y轴。
视图坐标系
视图坐标系是以view的父view的左上角作为坐标系的原点。
2.2.2 getMeasureWidth与getWidth的区别
根据上面的解释,那么在onLayout的方法中,摆放View的时候,调用的应该是getMeasureWidth方法,因为此时getWidth方法还没有被赋值,getWidth有值是在调用View的layout方法之后。这个细节一定要注意。
总计起来一句话:没有调用view的layout之前,获取View的宽高都是调用getMeasureWidth方法,调用了view的layout方法之后,获取View的宽高都是调用getWidth方法。
2.2.3 子View摆放
在onlayout方法中,需要调用每一个子View的layout方法,调用layout方法的同时传入左上右下四个距离位置,从而将每一个子View根据参数放到对应的位置上。这样就实现了子View的摆放,那么先来做一下准备工作
在FlowLayout中摆放子View,调用子View的layout方法,首先要获取到每一行摆放的所有View,然后计算每一个子View的坐标位置就可以完成摆放。
获取所有的行的View,那么需要将之前测量过程中换行的数据保存出来,用于layout使用,所以在onMeasure方法中添加如下的代码:
// 2.2 如果需要换行
if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {//一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来allLines.add(lineViews);lineHeights.add(lineHeight);// 2.3 累计统计当前已经使用的View的高度和最宽的宽度parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);// 换行时将统计行信息的变量重置lineViews = new ArrayList<>();lineWidthUsed = 0;lineHeight = 0;
}
这样就获取到了每一行的数据,然后就是在onLayout方法中,获取每一个View的上下左右的位置,然后调用View的layout方法:
// 布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {// 当前有多少行int lineCount = allLines.size();// 获取当前FlowLayout的padding的值int curL = getPaddingLeft();int curT = getPaddingTop();// 通过for嵌套的方式,一行一行的获取出来for (int i = 0; i < lineCount; i++){// 获取到一行的所有的viewList<View> lineViews = allLines.get(i);// 获取到当前行的行高int lineHeight = lineHeights.get(i);// 使用for循环,遍历当前这一行的viewfor (int j = 0; j < lineViews.size(); j++){// 获取到一个viewView view = lineViews.get(j);// 将上一次摆放view之后的距离本FlowLayout的左边距离设置为左边距,第一个view的左边肯定就是默认值paddingint left = curL;// 同一行中View距顶部的高度是一样的,不会发生改变,第一行的话肯定就是padding值,后面的行会加上行高和行间距作为下一行的topint top = curT;// 子View的右侧的距离int right = left + view.getMeasuredWidth();// 子View的底部距离值是已经摆放了行的高度加上当前View的高度int bottom = top + view.getMeasuredHeight();// 摆放子Viewview.layout(left,top,right,bottom);// 更新同一行中View的左侧距离的值,其实就是上一个View的右侧距离+一个间距curL = right + mHorizontalSpacing;}// 下一行的顶部距离就是已经摆好的所有行的高度+本行的高度+行间距curT = curT + lineHeight + mVerticalSpacing;// 每一行的起点左侧距离都是padding leftcurL = getPaddingLeft();}
}
到此,自定义FlowLayout的代码已经基本完成,但是还有一个问题就是没有当最后一行不满的情况下,将会缺失,那么我们在onMeasure方法中单独补充上最后一行:
那么还有一个问题就是数据的清空操作,添加方法:
private void clearMeasureParams() {allLines.clear();lineHeights.clear();
}
运行效果: