在我剛學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即可,很簡單,不再累述。控件提供的更多方法在源碼裏都
有註釋,大家可以自己試一試。
最後附上源碼地址:點擊打開鏈接
這次的內容就到這裏,我們下次再見。