转载请注意:http://blog.csdn.net/wjzj000/article/details/65936007
我和一帮应届生同学维护了一个公众号:IT面试填坑小分队。旨在帮助应届生从学生过度到开发者,并且每周树立学习目标,一同进步!
写在前面
这段时间被国足踢赢韩国刷屏,确实很解气。天朝有一百种方式赢棒子,非逼我们用这种方式出手…
这次博客记录一个自定义的ViewGroup,比较常见的效果:流式布局。
简单效果如下:
此效果来自开源库:https://github.com/crazyandcoder/MultiLineChoose
关于使用,各位看官感兴趣可以移步到大神的GitHub上一睹王者之霸气。本篇博客是我在看大神源码过程之中的总结和记录。
开始
针对这种布局,我们可以想到:继承ViewGroup重写onLayout方法,计算子控件的宽度,如果大于父控件的宽度,那我们就让子控件在下一行进行layout。Ok,让我们带着思路来看一下大神的源码是怎么做的。
首先如我们所想:
继承了ViewGroup
public class MultiLineChooseLayout extends ViewGroup
紧接着就是正常的在构造方法中完成对自定义属性的初始化:
final TypedArray attrsArray = context.obtainStyledAttributes(attrs,
R.styleable.MultiLineChooseItemTags,defStyleAttr,R.style.MultiLineChooseItemTags);
textColor = attrsArray.getColor(R.styleable.MultiLineChooseItemTags_item_textColor,default_text_color);
backgroundColor =attrsArray.getColor(R.styleable.MultiLineChooseItemTags_item_backgroundColor,default_background_color);
selectedTextColor =attrsArray.getColor(R.styleable.MultiLineChooseItemTags_item_selectedTextColor,default_checked_text_color);
//省略部分初始化
onMeasure方法:
在这里我们需要小小的注意一下。onMeasure之中我们在处理warp_content
时要考虑子View各个位置的情况,因为子View有可能排列时会大于一行,也有可能不足一行。
源码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//调用此方法后,我们可以获取通过子View的getMeasuredWidth/getMeasuredHeight获取子View的宽高信息。
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
int row = 0; // The row counter.
int rowWidth = 0; // Calc the current row width.
int rowMaxHeight = 0; // Calc the max tag height, in current row.
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
if (child.getVisibility() != GONE) {
rowWidth += childWidth;
if (rowWidth > widthSize) {
// 下一行
rowWidth = childWidth;
// 下一行宽度。
height += rowMaxHeight + verticalSpacing;
rowMaxHeight = childHeight;
// 下一行最大高度。
row++;
}
else {
// 这一行。
rowMaxHeight = Math.max(rowMaxHeight, childHeight);
}
rowWidth += horizontalSpacing;
}
}
height += rowMaxHeight;
height += getPaddingTop() + getPaddingBottom();
if (row == 0) {
//只有一行item
width = rowWidth;
width += getPaddingLeft() + getPaddingRight();
}
else {
// 如果分组的标签超过一行,请设置宽度以匹配父级。
width = widthSize;
}
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
上诉的代码都是套路性的东西。通过遍历子View来计算是否占够一行的宽度,如果够那么就下一行。并且设置父View的宽度为行宽;否则便是子View一共多宽父View就有多宽。
onLayout方法:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//设置父View的起始和终止点
final int parentLeft = getPaddingLeft();
final int parentRight = r - l - getPaddingRight();
final int parentTop = getPaddingTop();
final int parentBottom = b - t - getPaddingBottom();
int childLeft = parentLeft;
int childTop = parentTop;
int rowMaxHeight = 0;
final int count = getChildCount();
//遍历子View进行,计算宽高,然后调用layout
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
if (child.getVisibility() != GONE) {
// 如果当前View的位置大于父View的宽度,那么就放置到下一行
if (childLeft + width > parentRight) {
childLeft = parentLeft;
childTop += rowMaxHeight + verticalSpacing;
rowMaxHeight = height;
}
else {
rowMaxHeight = Math.max(rowMaxHeight, height);
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
childLeft += width + horizontalSpacing;
}
}
}
Ok,截止到这,我们可以在MultiLineChooseLayout这个布局之中加入子View进行正常的流式显示。但是我们需要的是动态的添加子View,因此仅有这些是肯定不够的。
动态添加子View
我们正常使用的时候是这样色的:
List<String> mDataList = new ArrayList<>();
mDataList.add("赵云");
mDataList.add("关羽");
mDataList.add("张飞");
mDataList.add("黄忠");
mDataList.add("马超");
mDataList.add("吕布");
mDataList.add("高顺");
mDataList.add("张辽");
mDataList.add("诸葛亮");
singleChoose.setList(mDataList);
通过使用我们可以看出来,我们通过MultiLineChooseLayout的setList方法进行动态设置显示内容。
接下来让我们看代码:
public void setList(List<String> tagList) {
setList(tagList.toArray(new String[tagList.size()]));
}
//setList
public void setList(String... tags) {
removeAllViews();
for (final String tag : tags) {
addItem(tag);
}
}
//addItem
private void addItem(CharSequence tag) {
final ItemView item = new ItemView(getContext(), tag);
item.setOnClickListener(mInternalTagClickListener);
//此处,通过new自定义的ItemView,然后设置我们setList中的值。调用addView传入这个对象
addView(item);
}
ItemView:
class ItemView extends TextView
//构造方法之中,除了进行一些初始化以为。就属这行代码比较特殊
setLayoutParams(new MultiLineChooseLayout.LayoutParams(itemWidth, itemHeight));
可以看出,MultiLineChooseLayout.LayoutParams仅仅是继承了ViewGroup.LayoutParams的一个类而已,并未做特殊处理。
public static class LayoutParams extends ViewGroup.LayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
}
接下来我们就看一下onDraw方法:
@Override
protected void onDraw(Canvas canvas) {
//在正常绘制TextView之前,我们要进行一些自己的绘制处理
if (!animUpdateDrawable) {
updateDrawable();
}
super.onDraw(canvas);
}
//updateDrawable()
private void updateDrawable() {
//进行画框
mStrokeColor = mStrokeColor == null ? ColorStateList.valueOf(Color.TRANSPARENT) : mStrokeColor;
mCheckedStrokeColor = mCheckedStrokeColor == null ? mStrokeColor : mCheckedStrokeColor;
updateDrawable(!isChecked ? mStrokeColor.getDefaultColor() : mCheckedStrokeColor.getDefaultColor());
}
//updateDrawable(int strokeColor)
private void updateDrawable(int strokeColor) {
int mbackgroundColor;
if (isChecked) {
mbackgroundColor = selectedBackgroundColor;
}
else {
mbackgroundColor = backgroundColor;
}
GradientDrawable drawable = new GradientDrawable();
drawable.setCornerRadii(mRadius);
drawable.setColor(mbackgroundColor);
drawable.setStroke(mStrokeWidth, strokeColor);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) {
this.setBackgroundDrawable(drawable);
}
else {
this.setBackground(drawable);
}
}
然后就是onTouchEvent()方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
getDrawingRect(mOutRect);
invalidatePaint();
invalidate();
break;
}
case MotionEvent.ACTION_MOVE: {
if (!mOutRect.contains((int) event.getX(), (int) event.getY())) {
invalidatePaint();
invalidate();
}
break;
}
case MotionEvent.ACTION_UP: {
invalidatePaint();
invalidate();
break;
}
}
return super.onTouchEvent(event);
}
//invalidatePaint()
private void invalidatePaint() {
animUpdateDrawable = false;
if (isChecked) {
mBackgroundPaint.setColor(selectedBackgroundColor);
setTextColor(selectedTextColor);
}
else {
mBackgroundPaint.setColor(backgroundColor);
setTextColor(textColor);
}
}
到此我们自定义的这个字View就被添加到了父View之中,说白了就是addView的功能。当然我们也可以使用LayoutInflate来构建自己的子View。
监听事件
监听事件的使用也和我们正常写回调的方式没有什么不同,而且我们在分析addView的时候我们就已经见到了监听的使用。
private void addItem(CharSequence tag) {
final ItemView item = new ItemView(getContext(), tag);
item.setOnClickListener(mInternalTagClickListener);
addView(item);
}
让我们来看一看ItemClicker的代码是怎么写的:
public void setOnItemClickListener(onItemClickListener l) {
mOnItemClickLisener = l;
}
class ItemClicker implements OnClickListener {
@Override
public void onClick(View v) {
final ItemView tag = (ItemView) v;
int position = -1;
//getSelectedItem()方法会通过遍历所有的ItemView中的isChecked来拿到被选中的ItemView
final ItemView checkedTag = getSelectedItem();
if (!multiChooseable) {
//单选
if (checkedTag != null) {
checkedTag.setItemSelected(false);
}
tag.setItemSelected(true);
position = getSelectedIndex();
}
else {
//多选
tag.setItemSelected(!tag.isChecked);
position = -1;
}
//外部点击事件,完成回调
if (mOnItemClickLisener != null) {
mOnItemClickLisener.onItemClick(position, tag.getText().toString());
}
}
}
而我们使用的时候,就可以直接set
singleChoose.setOnItemClickListener(new MultiLineChooseLayout.onItemClickListener() {
@Override
public void onItemClick(int position, String text) {
singleChooseTv.setText("结果:position: " + position + " " + text);
}
});
尾声
Ok,整体View的思路就是如此,当然我们还需要处理细节之处,因为整个项目是开源的。因此各位看官如果有特殊需求服务….移步大神的GitHub。
最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp