在我刚学Android的时候,看到b站的手机端app里,在显示搜索热词的时候有这样一个效果:
我当时觉得很神奇,直到后来某一天我突然想明白是怎么回事了。
这次就带来这样一个控件:可以自定义添加标签,并且新添加的标签可以根据其长度,如果当前行放不下的话自动换到下一行。
首先说一下实现思路:我们可以把整个东西看成是一个纵向排布的LinearLayout,里面的每一行内容就是一个横向排布的子
LinearLayout里装若干个TextView,所谓的”自动换行“事实上就是判断新增加的TextView在放入后是否会超出其右边界,如
超出则新建一个横向排布的子LinearLayout,即新的一行,把TextView放入其中。现在的问题就变成有没有办法知道一个已知内
容的TextView的宽度?当然有办法,Paint类下的measureText方法提供了这个功能。
原理讲完,下面上控件本体:
SelfAdaptionColunmLayout.java:
public class SelfAdaptionColumnLayout extends LinearLayout {
// 图标位于标签左边
public static final int ICON_LEFT = 0x001;
// 图标位于标签右边
public static final int ICON_RIGHT = 0x002;
private static final String KEY_TEXTVIEW = "KAY_TEXTVIEW";
private static final String KEY_TEXTITEM = "KEY_TEXTITEM";
private Context context;
private ArrayList<HashMap> list;
private int layoutWidth;
// 行间距
private int lineMargin = 10;
// 列间距,即同一行相邻标签之间的距离
private int columnMargin = 10;
// 默认标签文字颜色
private int defaultColor = Color.parseColor("#000000");
// 默认标签文字大小
private int defaultSize = 16;
// 标签内的图标位置
private int iconGravity = ICON_LEFT;
// 标签内的图标距离文字的距离
private int iconPadding = 5;
private int currentLength = 0;
// 标签点击回调
private OnItemClickListener listener;
public SelfAdaptionColumnLayout(Context context) {
this(context, null);
}
public SelfAdaptionColumnLayout(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public SelfAdaptionColumnLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
/**
* 初始化方法
*
* @param context
*/
private void init(Context context) {
this.context = context;
list = new ArrayList<>();
setOrientation(VERTICAL);
setGravity(Gravity.LEFT);
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
layoutWidth = getWidth();
notifyDataSetChanged();
}
});
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 0) {
throw new RuntimeException("layout should not have any child");
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
layoutWidth = getMeasuredWidth();
}
/**
* 绘制标签
*
* @param position
* @param textview
* @param item
*/
private void drawText(final int position, TextView textview, TextItem item) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(item.getTextSize() != 0 ? item.getTextSize() : defaultSize);
if (item.getIcon() != null && item.isShowIcon()) {
item.getIcon().setBounds(0, 0, dp2px(item.getIconSize()), dp2px(item.getIconSize()));
}
if (getChildCount() == 0) {
LinearLayout parent = new LinearLayout(context);
parent.setGravity(Gravity.CENTER_VERTICAL);
parent.setOrientation(HORIZONTAL);
textview.setText(item.getText());
textview.setGravity(Gravity.CENTER_VERTICAL);
textview.setSingleLine(true);
textview.setTextSize(item.getTextSize() != 0 ? item.getTextSize() : defaultSize);
textview.setTextColor(item.getTextColor() != 0 ? item.getTextColor() : defaultColor);
textview.setPadding(dp2px(item.getTextPaddingLeftRight()), dp2px(item.getTextPaddingTopBottom()), dp2px(item.getTextPaddingLeftRight()), dp2px(item.getTextPaddingTopBottom()));
textview.setCompoundDrawables(item.getIcon() != null && item.isShowIcon() && iconGravity == ICON_LEFT ? item.getIcon() : null, null,
item.getIcon() != null && item.isShowIcon() && iconGravity == ICON_RIGHT ? item.getIcon() : null, null);
textview.setCompoundDrawablePadding(dp2px(iconPadding));
textview.setBackgroundDrawable(item.getTextBackground());
textview.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onItemClick(position, ((TextItem) list.get(position).get(KEY_TEXTITEM)).getText());
}
}
});
if (textview.getParent() != null) {
((LinearLayout) textview.getParent()).removeView(textview);
}
parent.addView(textview, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
addView(parent, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
if (item.getIcon() != null && item.isShowIcon()) {
currentLength = dp2px(iconPadding) + 2 * dp2px(item.getTextPaddingLeftRight()) + dp2px(paint.measureText(item.getText())) + dp2px(item.getIconSize());
} else {
currentLength = 2 * dp2px(item.getTextPaddingLeftRight()) + dp2px(paint.measureText(item.getText()));
}
} else {
LinearLayout parent = (LinearLayout) getChildAt(getChildCount() - 1);
boolean isNeedWrap;
if (item.getIcon() != null && item.isShowIcon()) {
isNeedWrap = currentLength + dp2px(columnMargin) + dp2px(iconPadding) + 2 * dp2px(item.getTextPaddingLeftRight())
+ dp2px(paint.measureText(item.getText())) + dp2px(item.getIconSize()) + getPaddingRight() > layoutWidth;
} else {
isNeedWrap = currentLength + dp2px(columnMargin) + 2 * dp2px(item.getTextPaddingLeftRight())
+ dp2px(paint.measureText(item.getText())) + getPaddingRight() > layoutWidth;
}
if (isNeedWrap) {
parent = new LinearLayout(context);
parent.setGravity(Gravity.CENTER_VERTICAL);
parent.setOrientation(HORIZONTAL);
textview.setText(item.getText());
textview.setGravity(Gravity.CENTER);
textview.setSingleLine(true);
textview.setTextSize(item.getTextSize() != 0 ? item.getTextSize() : defaultSize);
textview.setTextColor(item.getTextColor() != 0 ? item.getTextColor() : defaultColor);
textview.setPadding(dp2px(item.getTextPaddingLeftRight()), dp2px(item.getTextPaddingTopBottom()), dp2px(item.getTextPaddingLeftRight()), dp2px(item.getTextPaddingTopBottom()));
textview.setCompoundDrawables(item.getIcon() != null && item.isShowIcon() && iconGravity == ICON_LEFT ? item.getIcon() : null, null,
item.getIcon() != null && item.isShowIcon() && iconGravity == ICON_RIGHT ? item.getIcon() : null, null);
textview.setCompoundDrawablePadding(dp2px(iconPadding));
textview.setBackgroundDrawable(item.getTextBackground());
textview.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onItemClick(position, ((TextItem) list.get(position).get(KEY_TEXTITEM)).getText());
}
}
});
if (textview.getParent() != null) {
((LinearLayout) textview.getParent()).removeView(textview);
}
parent.addView(textview, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
addView(parent, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
((LinearLayout.LayoutParams) parent.getLayoutParams()).setMargins(0, dp2px(lineMargin), 0, 0);
if (item.getIcon() != null && item.isShowIcon()) {
currentLength = dp2px(iconPadding) + 2 * dp2px(item.getTextPaddingLeftRight()) + dp2px(paint.measureText(item.getText())) + dp2px(item.getIconSize());
} else {
currentLength = 2 * dp2px(item.getTextPaddingLeftRight()) + dp2px(paint.measureText(item.getText()));
}
} else {
textview.setText(item.getText());
textview.setGravity(Gravity.CENTER);
textview.setSingleLine(true);
textview.setTextSize(item.getTextSize() != 0 ? item.getTextSize() : defaultSize);
textview.setTextColor(item.getTextColor() != 0 ? item.getTextColor() : defaultColor);
textview.setPadding(dp2px(item.getTextPaddingLeftRight()), dp2px(item.getTextPaddingTopBottom()), dp2px(item.getTextPaddingLeftRight()), dp2px(item.getTextPaddingTopBottom()));
textview.setCompoundDrawables(item.getIcon() != null && item.isShowIcon() && iconGravity == ICON_LEFT ? item.getIcon() : null, null,
item.getIcon() != null && item.isShowIcon() && iconGravity == ICON_RIGHT ? item.getIcon() : null, null);
textview.setCompoundDrawablePadding(dp2px(iconPadding));
textview.setBackgroundDrawable(item.getTextBackground());
textview.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onItemClick(position, ((TextItem) list.get(position).get(KEY_TEXTITEM)).getText());
}
}
});
if (textview.getParent() != null) {
((LinearLayout) textview.getParent()).removeView(textview);
}
parent.addView(textview, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
((LinearLayout.LayoutParams) textview.getLayoutParams()).setMargins(dp2px(columnMargin), 0, 0, 0);
if (item.getIcon() != null && item.isShowIcon()) {
currentLength += dp2px(columnMargin) + dp2px(iconPadding) + 2 * dp2px(item.getTextPaddingLeftRight()) + dp2px(paint.measureText(item.getText())) + dp2px(item.getIconSize());
} else {
currentLength += dp2px(columnMargin) + 2 * dp2px(item.getTextPaddingLeftRight()) + dp2px(paint.measureText(item.getText()));
}
}
}
}
/**
* 添加一个标签
* 添加后必须调用notifyDataSetChanged()方法才会生效
*
* @param text 标签文字
*/
public void addItem(@NonNull String text) {
HashMap temp = new HashMap();
temp.put(KEY_TEXTVIEW, new TextView(context));
temp.put(KEY_TEXTITEM, new TextItem(text));
list.add(temp);
}
/**
* 添加一个标签
* 添加后必须调用notifyDataSetChanged()方法才会生效
*
* @param text 标签文字
* @param textSize 标签文字大小,单位sp
* @param textColor 标签文字颜色
* @param textPaddingTopBottom 标签文字距离上下边的距离,单位dp
* @param textPaddingLeftRight 标签文字距离左右边的距离,单位dp
* @param textBackground 标签背景
*/
public void addItem(@NonNull String text, int textSize, int textColor, int textPaddingTopBottom, int textPaddingLeftRight, Drawable textBackground) {
HashMap temp = new HashMap();
temp.put(KEY_TEXTVIEW, new TextView(context));
temp.put(KEY_TEXTITEM, new TextItem(text, textSize, textColor, textPaddingTopBottom, textPaddingLeftRight, textBackground));
list.add(temp);
}
/**
* 添加一个标签
* 添加后必须调用notifyDataSetChanged()方法才会生效
*
* @param icon 标签图标
* @param iconSize 标签图标大小,即正方形图标的边长,单位dp
* @param isShowIcon 标签图标是否显示
* @param text 标签文字
* @param textSize 标签文字大小,单位sp
* @param textColor 标签文字颜色
* @param textPaddingTopBottom 标签文字距离上下边的距离,单位dp
* @param textPaddingLeftRight 标签文字距离左右边的距离,单位dp
* @param textBackground 标签背景
*/
public void addItem(Drawable icon, int iconSize, boolean isShowIcon, @NonNull String text, int textSize, int textColor, int textPaddingTopBottom, int textPaddingLeftRight, Drawable textBackground) {
HashMap temp = new HashMap();
temp.put(KEY_TEXTVIEW, new TextView(context));
temp.put(KEY_TEXTITEM, new TextItem(icon, iconSize, isShowIcon, text, textSize, textColor, textPaddingTopBottom, textPaddingLeftRight, textBackground));
list.add(temp);
}
/**
* 删除指定指定标签
* 删除后必须调用notifyDataSetChanged()方法才会生效
*
* @param position 标签坐标
*/
public void removeItem(int position) {
list.remove(position);
}
/**
* 获取指定标签实体
*
* @param position 标签序号
* @return 标签实体
*/
public TextItem getItem(int position) {
return (TextItem) list.get(position).get(KEY_TEXTITEM);
}
/**
* 修改指定标签
* 修改后必须调用notifyDataSetChanged()方法才会生效
*
* @param position 标签序号
* @param item 标签实体
*/
public void setItem(int position, TextItem item) {
list.get(position).put(KEY_TEXTITEM, item);
}
/**
* 清除所有标签
* 清除后必须调用notifyDataSetChanged()方法才会生效
*/
public void clearAllItem() {
list.clear();
}
/**
* 获取标签总数
*
* @return 标签总数
*/
public int getItemCount() {
return list.size();
}
/**
* 设置行间距
*
* @param lineMargin 行间距,单位dp
*/
public void setLineMargin(int lineMargin) {
this.lineMargin = lineMargin;
}
/**
* 设置列间距,即同一行相邻标签之间的距离
*
* @param columnMargin 列间距,单位dp
*/
public void setColumnMargin(int columnMargin) {
this.columnMargin = columnMargin;
}
/**
* 设置默认标签文字颜色
*
* @param defaultColor,颜色值
*/
public void setDefaultColor(int defaultColor) {
this.defaultColor = defaultColor;
}
/**
* 设置默认标签文字大小
*
* @param defaultSize,大小值,单位sp
*/
public void setDefaultSize(int defaultSize) {
this.defaultSize = defaultSize;
}
/**
* 设置标签内的图标位置
*
* @param iconGravity 图标位置,可选项:ICON_LEFT、ICON_RIGHT
*/
public void setIconGravity(int iconGravity) {
this.iconGravity = iconGravity;
}
/**
* 标签内图标距离文字的距离
*
* @param iconPadding 距离值,单位dp
*/
public void setIconPadding(int iconPadding) {
this.iconPadding = iconPadding;
}
/**
* 更新视图
* 在对标签增、删、改操作后必须调用此方法才会生效
*/
public void notifyDataSetChanged() {
if (layoutWidth != 0) {
currentLength = 0;
removeAllViews();
for (int i = 0; i < list.size(); i++) {
drawText(i, (TextView) list.get(i).get(KEY_TEXTVIEW), (TextItem) list.get(i).get(KEY_TEXTITEM));
}
}
}
/**
* 标签实体
*/
class TextItem {
// 标签内图标
private Drawable icon;
// 标签内图标的大小,即正方形图标的边长,单位dp
private int iconSize;
// 标签内图标是否显示
private boolean isShowIcon;
// 标签文字
private String text;
// 标签文字大小,单位sp
private int textSize;
// 标签文字颜色
private int textColor;
// 标签文字距离上下边的距离,单位dp
private int textPaddingTopBottom;
// 标签文字距离左右边的距离,单位dp
private int textPaddingLeftRight;
// 标签背景
private Drawable textBackground;
public TextItem(@NonNull String text) {
this.text = text;
}
public TextItem(@NonNull String text, int textSize, int textColor, int textPaddingTopBottom, int textPaddingLeftRight, Drawable textBackground) {
this.text = text;
this.textSize = textSize;
this.textColor = textColor;
this.textPaddingTopBottom = textPaddingTopBottom;
this.textPaddingLeftRight = textPaddingLeftRight;
this.textBackground = textBackground;
}
public TextItem(Drawable icon, int iconSize, boolean isShowIcon, @NonNull String text, int textSize, int textColor, int textPaddingTopBottom, int textPaddingLeftRight, Drawable textBackground) {
this.icon = icon;
this.iconSize = iconSize;
this.text = text;
this.isShowIcon = isShowIcon;
this.textSize = textSize;
this.textColor = textColor;
this.textPaddingTopBottom = textPaddingTopBottom;
this.textPaddingLeftRight = textPaddingLeftRight;
this.textBackground = textBackground;
}
public Drawable getIcon() {
return icon;
}
public void setIcon(Drawable icon) {
this.icon = icon;
}
public int getIconSize() {
return iconSize;
}
public void setIconSize(int iconSize) {
this.iconSize = iconSize;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public boolean isShowIcon() {
return isShowIcon;
}
public void setIsShowIcon(boolean isShowIcon) {
this.isShowIcon = isShowIcon;
}
public int getTextSize() {
return textSize;
}
public void setTextSize(int textSize) {
this.textSize = textSize;
}
public int getTextColor() {
return textColor;
}
public int getTextPaddingTopBottom() {
return textPaddingTopBottom;
}
public void setTextPaddingTopBottom(int textPaddingTopBottom) {
this.textPaddingTopBottom = textPaddingTopBottom;
}
public int getTextPaddingLeftRight() {
return textPaddingLeftRight;
}
public void setTextPaddingLeftRight(int textPaddingLeftRight) {
this.textPaddingLeftRight = textPaddingLeftRight;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
}
public Drawable getTextBackground() {
return textBackground;
}
public void setTextBackground(Drawable textBackground) {
this.textBackground = textBackground;
}
}
private int sp2px(float spValue) {
float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
private int dp2px(float dipValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
public interface OnItemClickListener {
void onItemClick(int position, String text);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
}
只有一个文件,复制进项目就能用了,上MainActivity.java和activity_main.xml:
MainActivity.java:
public class MainActivity extends Activity {
private EditText editText;
private Button button;
private SelfAdaptionColumnLayout layout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
private void initData() {
}
private void initView() {
editText = (EditText) findViewById(R.id.edittext);
button = (Button) findViewById(R.id.add_button);
layout = (SelfAdaptionColumnLayout) findViewById(R.id.layout);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.addItem(editText.getText().toString());
layout.notifyDataSetChanged();
editText.setText("");
}
});
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.min.selfadaptioncolumnlayout.SelfAdaptionColumnLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="10dp"
android:layout_weight="1"
android:orientation="vertical" />
<EditText
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp" />
<Button
android:id="@+id/add_button"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:text="add" />
</LinearLayout>
代码很简单,唯一需要注意的是在执行完addItem方法添加一个标签后,一定要执行notifyDataSetChanged刷新一下,添加才
会生效,这就跟ListView里改动数据后需要执行adapter. notifyDataSetChanged()一样。
运行看一下效果:
这里说明一下,控件提供的添加标签方法addItem有三种调用方式,即:
public void addItem(@NonNull String text)
public void addItem(@NonNull String text, int textSize, int textColor, int textPaddingTopBottom, int textPaddingLeftRight, Drawable textBackground)
public void addItem(Drawable icon, int iconSize, boolean isShowIcon, @NonNull String text, int textSize, int textColor, int textPaddingTopBottom, int textPaddingLeftRight, Drawable textBackground)
以上的例子只是调用了第一种也是最简单的一种,下面用第二种方法添加标签,先定义几个资源文件:
backgroud_up.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#dddddd" />
<solid android:color="#ffffff" />
</shape>
backgroud_down.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#888888" />
<solid android:color="#ffffff" />
</shape>
background_selector.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/backgroud_up" android:state_pressed="false"></item>
<item android:drawable="@drawable/backgroud_down" android:state_pressed="true"></item>
</selector>
然后修改页面中按钮的点击事件:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layout.addItem(editText.getText().toString(), 20, Color.parseColor("#333333"),
5, 15, getResources().getDrawable(R.drawable.background_selector));
layout.notifyDataSetChanged();
editText.setText("");
}
});
完成,看一下效果:
第三种调用方法支持在标签左侧或右侧添加一个小图标,以下也做一个示范:
先添加一个图标:
hot.png:
然后继续修改按钮点击事件:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (layout.getItemCount() < 3) {
layout.addItem(getResources().getDrawable(R.drawable.hot), 20, true, editText.getText().toString(),
20, Color.parseColor("#3F51B5"), 5, 15, getResources().getDrawable(R.drawable.background_selector));
} else {
layout.addItem(editText.getText().toString(), 16, Color.parseColor("#333333"),
3, 10, getResources().getDrawable(R.drawable.background_selector));
}
layout.notifyDataSetChanged();
editText.setText("");
}
});
再次运行:
如果需要监听标签的点击事件,只需要设置setOnItemClickListener即可,很简单,不再累述。控件提供的更多方法在源码里都
有注释,大家可以自己试一试。
最后附上源码地址:点击打开链接
这次的内容就到这里,我们下次再见。