前言部分
本文主要介绍如何自定义一个常见的labels标签,功能上主要支持,单选、多选、点击三种模式。因为这个使用率很高,并且这个是比较典型学习自定义ViewGroup的例子,所以特意动手实践,加深对Android的认识。这个项目主要是为了自己学习使用,所以并不是很完善,先上一个效果图,了解一下:
内容部分
-
ViewGroup的定义主要还是分布在两个部分,一个是测量,另一个是布局。layout子view是作为容器最基本的工作。
-
测量的部分主要还是遵循view的三种测量模式来不同处理。介绍测量规则的博客很多,这里不多解释。
{@link android.view.View.MeasureSpec#UNSPECIFIED}, {@link android.view.View.MeasureSpec#AT_MOST} or {@link android.view.View.MeasureSpec#EXACTLY}
-
布局部分主要是需要我们特殊处理的地方,因为我们的每个labels的大小是根据内容决定的,所以我们要自己根据view的尺寸进行摆放。
代码实现
首先介绍onMeasure方法中的实现,根绝子view的尺寸来决定容器view的测量情况。
private void measureMyChild(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//宽度
maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int count = getChildCount();
//总高度
int contentHeight = 0;
//记录最宽的行宽
int maxLineWidth = 0;
// 每行宽度
int startLayoutWidth = 0;
//一行中子控件最高的高度,用于决定下一行高度应该在目前基础上累加多少
int maxChildHeight = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LogUtil.i("onLayout--getPaddingRight:" +
child.getPaddingRight() +
"getPaddingLeft:" + child.getPaddingLeft());
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//测量的宽高
int childMeasureWidth = child.getMeasuredWidth();
int childMeasureHeight = child.getMeasuredHeight();
LogUtil.i("onLayout--width:" + maxWidth + "startLayoutWidth:");
if (startLayoutWidth + childMeasureWidth < maxWidth) {
//如果一行没有排满,继续往右排列
startLayoutWidth += childMeasureWidth + margiHorizontal;
} else {
// 初始化为0
maxChildHeight = 0;
startLayoutWidth = 0;
}
if (childMeasureHeight > maxChildHeight) {
maxChildHeight = childMeasureHeight;
}
//获取总的高度
contentHeight += maxChildHeight + margiVertical;
//获取最长的行总的宽度
maxLineWidth = Math.max(maxLineWidth, startLayoutWidth);
}
//如果没有子元素,就设置宽高都为0(简化处理)
if (getChildCount() == 0) {
setMeasuredDimension(0, 0);
} else
//宽和高都是AT_MOST,则设置宽度最宽的字元素的宽度的和;高度设置为最高的元素的高度;
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(maxLineWidth, contentHeight);
}
//如果宽度是wrap_content,则宽度为最宽的一行的宽度
else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(maxLineWidth, heightSize);
}
//如果高度是wrap_content,则高度为最高的字元素的高度
else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, contentHeight);
}
}
宽度的测量过程是,最大的宽度直接是取的容器view的尺寸。然后我们计算每一行的子view尺寸,通过求和来和容器view的最大宽度进行比较(公式:startLayoutWidth + childMeasureWidth < maxWidth)累加的宽度加上下一个字view的宽度和最大宽度比较来决定是否进行换行。
高度的测量过程是,先把所有的子view的高度进行相加求和,在根据容器view的mode来进行尺寸选择。这里内容都是常规的测量原则。主要区别还说在于宽高的取值。
宽高的取值我们的原则是,高度我们取值为最高的字元素;宽度我们取值为字元素相加,最宽的一行字元素。
测量部分的内容就是这些,主要还说需要我们对测量规则进行了解。然后根据我们自己的需求来进行选择。
下面介绍布局子view的部分代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
LogUtil.i("onLayout--width:" + maxWidth);
final int count = getChildCount();
int childMeasureWidth = 0;
int childMeasureHeight = 0;
// 开始的X位置
int startLayoutWidth = getPaddingLeft();
// 开始的Y位置
int startLayoutHeight = getPaddingTop();
//一行中子控件最高的高度,用于决定下一行高度应该在目前基础上累加多少
int maxChildHeight = 0;
for (int i = 0; i < count; i++) {
int position = i;
TextView child = (TextView) getChildAt(i);
//注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
childMeasureWidth = child.getMeasuredWidth() + child.getPaddingLeft() + child.getPaddingRight();
childMeasureHeight = child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();
LogUtil.i("onLayout--width:" + maxWidth + "startLayoutWidth:" + startLayoutWidth);
if (startLayoutWidth + childMeasureWidth < maxWidth - getPaddingRight()) {
//如果一行没有排满,继续往右排列
left = startLayoutWidth;
right = left + childMeasureWidth;
top = startLayoutHeight;
bottom = top + childMeasureHeight;
} else {
//排满后换行
startLayoutWidth = getPaddingLeft();
startLayoutHeight += maxChildHeight + margiVertical;
maxChildHeight = 0;
left = startLayoutWidth;
right = left + childMeasureWidth;
top = startLayoutHeight;
bottom = top + childMeasureHeight;
}
//宽度累加
startLayoutWidth += childMeasureWidth + margiHorizontal;
if (childMeasureHeight > maxChildHeight) {
maxChildHeight = childMeasureHeight;
}
//确定子控件的位置,四个参数分别代表(左上右下)点的坐标值
child.layout(left, top, right, bottom);
initListener(child, position);
}
}
上面内容主要分为两部分,一部分是宽度的布局,一部分是高度的布局。
宽度布局:主要是根据子view的宽度和容器view的宽度的比较,来决定什么时候进行换行。这里需要注意的一些边界值,如我们经常使用的margin和padding值。
高度布局:主要是根据最高的子view的高度决定每一行的高度,这样可以让我们每一行都保持一样的高度。
完成上面两个大的步骤,基本上这个view也就完成的差不多了。
额外注意的点:
因为标签是通过创建textview设置属性添加到容器中,所以这里设置文字颜色变化的方法和在xml中有些区别:
//设置每一个标签
private void drawTextView() {
for (String text : textList) {
TextView label = new TextView(context);
label.setPadding(30, 30, 0, 0);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 40);
label.setBackgroundResource(R.drawable.selector_text_bg);
label.setText(text);
label.setTextColor(createColorStateList("#ffffffff", "#ff44e6ff"));
addView(label);
}
}
//工具方法
private static ColorStateList createColorStateList(String selected, String normal) {
int[] colors = new int[]{Color.parseColor(selected), Color.parseColor(normal)};
int[][] states = new int[2][];
states[0] = new int[]{android.R.attr.state_selected};
states[1] = new int[]{};
ColorStateList colorList = new ColorStateList(states, colors);
return colorList;
}
主要是介绍ColorStateList的使用,通过映射关系来进行颜色变化。
以上的步骤也可以通过填充xml中的textview来实现,这种实现方式你可以更轻松的设置你的textview。以前定义过的一个蚂蚁森林能量球效果,就说这种方式实现。蚂蚁森林效果
结束语
读万卷书,行万里路。虽然这些东西早就有人实现了,我们也许也使用过,但是亲自实践的必要性还是在的。
你的鼓励是我前进的动力。