自定义控件—自定义ViewGroup
ViewGroup的基本概念
ViewGroup绘制流程分为三部分:测量、布局、绘制。分别对应 onMeasure 、onLayout 、 onDraw 函数。
这三个函数的作用分别如下;
- onMeasure : 测量控件大小,为正式布局提供建议。至于用不用还要看onLayout函数
- onLayout : 对所有子控件进行布局
- onDraw : 根据布局的位置绘图。
onMeasure 函数与 MeasureSpec
onMeasure 函数的声明如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
其中 widthMeasureSpec 和 heightMeasureSpec 是由mode + size组成,其中前两位代表模式,后30位代表数值。
模式的分类
MeasureSpec.UNSPECIFIED (未指定 ):父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
MeasureSpec.AT_MOST (wrap_content):根据子控件确定自身大小
MeasureSpec.EXACTLY (精确值:100dp、match_parent):子元素至多达到指定的大小
他们的二进制值分别是:
MeasureSpec.UNSPECIFIED:32个0
MeasureSpec.EXACTLY:01+30个0
MeasureSpec.AT_MOST :10+30个0
前面两位代表模式,他们分别代表十进制的0、1、2
模式提取
widthMeasureSpec、heightMeasureSpec是由模式和大小组成,前两位代表模式。下面看看如何提取模式和大小;
首先我们想到的是和Mask运算,去掉不需要的部分。
上面是我们自己实现的,实际Android已经为我们实现了提取模式和大小的实现。
例如:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
另外,模式的取值为:
- MeasureSpec.UNSPECIFIED
- MeasureSpec.AT_MOST
- MeasureSpec.EXACTLY
模式的用处及对应关系
实际上在xml中定义的宽高,就是在这些模式中的一个。下面为模式的对应关系
wrap_content -> MeasureSpec.AT_MOST
match_parent 和 具体的数值 -> MeasureSpec.EXACTLY
当用户指定值为:wrap_content 的时候我们需要计算控件的值,否则应该尊重,选用用户的值。
getMeasuredxxx和getxxx(xxx代表宽高)
两者主要有如下区别:
- getMeasuredHeight():在measure过程结束后才能拿到数值。而getHight()需要在layout过程后才能拿到数值
- getMeasuredHeight():数值通过setMeasuredDimension()指定的。而getHeight是通过layout()指定的
让子控件支持margin值
如果想要支持子控件的layout_margin属性,则自定义的ViewGroup必须重写generateLayoutParams()函数
因为默认的layoutParams值只会获取宽高,不会设置margin属性值。所以,我们需要一个MarginLayoutParams类来让子控件获取margin值。详细请看MarginLayoutParams源码。
添加如下代码,让你的ViewGroup支持margin
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
计算ViewGroup大小示例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
height += childHeight;
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
摆放子控件位置:onLayout
在onLayout中设置子控件的layout属性,进而控制子控件位置摆放。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
自定义LinearLayout实现完整代码:
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
height += childHeight;
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//int childHeight = child.getMeasuredHeight();
//int childWidth = child.getMeasuredWidth();
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}
}
自定义RadioGroup(实现指定列)
本示例实现指定列的布局测量和摆放方法。
实现思路:
自定义布局,主要实现的方法有:测量和布局。
测量高度:测量每行的最大高度值进行累加。
测量宽度:本示例为实现
布局行:每隔指定列 top值进行累加。
实现代码:
public class YfTypeLayout extends RadioGroup {
private int rowNum = 3;
public YfTypeLayout(Context context) {
super(context);
}
public YfTypeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
int width = 0;
int count = getChildCount();
int rowMaxHeight = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
rowMaxHeight = Math.max(rowMaxHeight, childHeight);
if (i % rowNum == rowNum - 1 || i == count - 1) {
height += rowMaxHeight;
rowMaxHeight = 0;
}
width = Math.max(childWidth, width);
}
setMeasuredDimension(
(measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int left;
int parentWidth = getMeasuredWidth();
int count = getChildCount();
int rowMaxHeight = 0;
int region = parentWidth / 3;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
if (i % rowNum == 0) {
left = region / 2 - childWidth / 2;
top += rowMaxHeight;
} else {
left = region * (i % rowNum) + region / 2 - childWidth / 2;
}
rowMaxHeight = rowMaxHeight > childHeight ? rowMaxHeight : childHeight;
child.layout(left, top, left + childWidth, top + childHeight);
}
}
}