一、自定义View之前要了解的知识点
1、自定义 View 和自定义 ViewGroup 之间的区别
自定义父类控件 | 需重写的方法 |
---|---|
自定义View | onMeasure() + onDraw() |
自定义ViewGroup | onMeasure() + onLayout() |
View 在Activity 中显示出来要经历测量、布局和绘制三个步骤,分别对应measure、layout、draw,其中
- 测量:onMeasure() 决定 View 的大小
- 布局:onLayout()决定 View 在 ViewGroup 中的位置
- 绘制: onDraw()决定绘制这个 View
各个方法的功能都知道了,为啥自定义 View 和自定义 ViewGroup重写的方法不一样呢?
这是个人理解,因为View是布局的最小单位,是各种组件的基类,而ViewGroup继承自View,是一个布局容器,负责将控制容器中的View的位置就行(对应onLayout());
2、自定义View的绘制流程
3、View的构造方法
view的构造方法一共有四个(第四个很少用到,具体什么时候用不太清楚)
// 如果View是在Java代码里面new的,则调用第一个构造函数
public XFlowLayout(Context context) {
super(context);
}
// 如果View是在.xml里声明的,则调用第二个构造函数 , 自定义属性是从AttributeSet传进来的
public XFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用 , 一般是在第二个构造函数里主动调用
public XFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
4、Android坐标系的认识
Android坐标系:屏幕左上角为坐标原点,向右为 x 增大方向,向下为 y 增大方向,其 View 的位置是相对父控件而言的:
- top : 子View上边界到父View上边界的距离(View 的位置通过view.getTop()获取)
- bottom: 子View下边界到父View上边界的距离(View 的位置通过view.getBottom()获取)
- left : 子View左边界到父View左边界的距离(View 的位置通过view.getLeft()获取)
- right : 子View右边界到父View左边界的距离(View 的位置通过view.getRight()获取)
另外,还有一个概念要分清,MotionEvent中 get()和getRaw()的区别
- get 是触摸点相对于其所在组件坐标系的坐标—— event.getX(); event.getY();
- getRow 是触摸点相对于屏幕默认坐标系的坐标—— event.getRawX(); event.getRawY();
5、LayoutParams
这是一个布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己,将对应宽高的值传给下面方法的第三个参数(其中MATCH_PARENT的值是-1 ,WRAP_CONTENT的值是-2,若设置了具体的宽高对应的值则是大于0),重点就是将LayoutParams转换成MeasureSpec,代码逻辑如下(ViewGroup源码):
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);
}
以上逻辑可以通过生活中的一个现象来帮助理解:
6、MeasureSpec
MeasureSpec 是 View下的内部类,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:
- UNSPECIFIED:父控件没有任何限制----相当于生活事例中的第三种情况
- EXACTLY:父控件已经知道你所需的精确大小,你的最终大小应该就是这么大----相当于生活事例中的第一种情况
- AT_MOST:你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现----相当于生活事例中的第一种情况
二、自定义 FlowLayout 的实现
第一步,创建好新的类继承ViewPager,写好构造函数,重写 onMeasure() 和 onLayout()方法;
第二步:准备处理 onMeasure()的度量逻辑,在此之前要确定是先度量子View的宽高来确定父View的宽高,还是先度量父View来确定子View的宽高(目前只有ViewPager是这样),选择大势所趋的先子后父。先搭出框架:
public class XFlowLayout extends ViewGroup {
public XFlowLayout(Context context) {
super(context);
}
public XFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public XFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
// 度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();