最近需要做一個類似標簽選擇的功能,類似餓了么的評價頁面的標簽選擇,沒辦法,項目需要,代碼擼起來
先來看看實現效果:
最終的效果就是上面那個樣子,接下來來看看實現步驟:
既然里面是一個個的標簽,那我們就先把標簽做出來,我這邊是直接繼承 RadioButton 來實現這個標簽效果的
修改 RadioButton 的樣式達到我們想要的效果,代碼如下:
class LableItem extends RadioButton {
public LableItem(Context context) {
super(context);
initStyle();
Click();
}
public LableItem(Context context, AttributeSet attrs) {
super(context, attrs);
initStyle();
Click();
}
/**
* 設置標簽item的樣式 需要修改樣式 直接修改drawable文件夾下select文件即可
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void initStyle() {
Bitmap bitmap = null;
setButtonDrawable(new BitmapDrawable(bitmap));
setBackground(getResources().getDrawable(R.drawable.select));
setTextSize(textsize);
setTextColor(no_textcolor);
setPadding(padding, padding, padding, padding);
setGravity(Gravity.CENTER);
}
}
這里我們用到了 selector 文件和兩個 shape 文件來更改 RadioButton 的選中和不選中的樣式
selector 代碼:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:drawable="@drawable/checked"></item>
<item android:state_checked="false" android:drawable="@drawable/uncheck"></item>
</selector>
未選中時的 shape 代碼:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="3dp"></corners>
<solid android:color="#ffffff"></solid>
<stroke android:color="#bdbdbd" android:width="0.5dp"></stroke>
</shape>
選中時的 shape 代碼:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="3dp"></corners>
<solid android:color="#fff5e9"></solid>
<stroke android:color="#ff8977" android:width="0.5dp"></stroke>
</shape>
這樣就達到了選中和不選中時的樣式改變
然后我們接著給他設置點擊事件 讓點擊選中,再次點擊取消選中:
public void Click() {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (select == 0) {
setChecked(true);
select = 1;
setTextColor(select_textcolor);
if (onItemSelectClick != null) {
onItemSelectClick.selectclick(getText().toString(), Integer.parseInt(getTag().toString()));
}
} else if (select == 1) {
setChecked(false);
select = 0;
setTextColor(no_textcolor);
}
}
});
這樣就可以讓他點擊選中,再次點擊取消選中
這樣標簽我們就做好了,完整的標簽代碼:
class LableItem extends RadioButton {
private int select = 0;
public LableItem(Context context) {
super(context);
initStyle();
Click();
}
public LableItem(Context context, AttributeSet attrs) {
super(context, attrs);
initStyle();
Click();
}
/**
* 設置標簽item的樣式 需要修改樣式 直接修改drawable文件夾下select文件即可
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void initStyle() {
Bitmap bitmap = null;
setButtonDrawable(new BitmapDrawable(bitmap));
setBackground(getResources().getDrawable(R.drawable.select));
setTextSize(textsize);
setTextColor(no_textcolor);
setPadding(padding, padding, padding, padding);
setGravity(Gravity.CENTER);
}
public void Click() {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!ismultiple){
clearSelect();
}
if (select == 0) {
setChecked(true);
select = 1;
setTextColor(select_textcolor);
if (onItemSelectClick != null) {
onItemSelectClick.selectclick(getText().toString(), Integer.parseInt(getTag().toString()));
}
} else if (select == 1) {
setChecked(false);
select = 0;
setTextColor(no_textcolor);
if (onCancelSelectClick != null) {
onCancelSelectClick.cancelselectclick(getText().toString(), Integer.parseInt(getTag().toString()));
}
}
}
});
}
/**
* 判斷是否選中
*
* @return
*/
public boolean isSelect() {
return select == 0 ? false : true;
}
public void setSelect(int select){
this.select=select;
}
}
接下來我們開始做裝這個標簽的瀑布流,這邊我是選擇繼承的 ViewGroup 然后把標簽一個一個添加進去,然后進行計算排序,首先我們把要用到的自定義屬性在 attrs 文件中定義好
自定義屬性:
<declare-styleable name="Lable">
<attr name="lable_textSize" format="dimension"></attr> //文字的大小
<attr name="lable_opttextColor" format="color"></attr> //選中時文字的顏色
<attr name="lable_notextColor" format="color"></attr> //未選中時文字的顏色
<attr name="lable_padding" format="integer"></attr> //內邊距(int型)
<attr name="lable_margin" format="integer"></attr> //外邊距(int型)
<attr name="lable_ismultiple" format="boolean"></attr> //是否可多選 false-單選 true-多選
</declare-styleable>
我這邊的自定義屬性就暫時只定義了這么多,然后我們在自定義控件里面獲取一下它們:
public Lable(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.Lable);
textsize=px2dip(typedArray.getDimensionPixelSize(R.styleable.Lable_lable_textSize,dip2px(16)));
select_textcolor=typedArray.getColor(R.styleable.Lable_lable_opttextColor, Color.BLACK);
no_textcolor=typedArray.getColor(R.styleable.Lable_lable_notextColor,Color.BLACK);
padding=typedArray.getInteger(R.styleable.Lable_lable_padding,10);
margin=typedArray.getInteger(R.styleable.Lable_lable_margin,8);
ismultiple=typedArray.getBoolean(R.styleable.Lable_lable_ismultiple,true);
}
接著我們把標簽添加進來:
public void initLableItem() {
if (listcount == null || listcount.size() == 0)
return;
for (int i = 0; i < listcount.size(); i++) {
LableItem lableItem = new LableItem(mContext);
lableItem.setText(listcount.get(i));
lableItem.setTag(i + 1);
addView(lableItem);
}
}
這個 listcount 就是一個 List<String> 里面裝的就是標簽的文字信息,有多少條內容就創建多少個標簽并添加到我們的 View當中,添加完成之后,我們就要去在 onMeasure 方法中測量我們控件的寬高,在這里我們用 view.getWidth() 是獲取不到我們標簽的真實寬高的,獲取不到,我們就無法做到瀑布流的效果,那么我們就用Paint 去獲取文字寬高進行計算得到標簽的真實寬高
我們先初始化一下 Paint :
public void init() {
paint = new Paint();
paint.setTextSize(dip2px(textsize));
scenewidth = getScene(SCENEWIDTH);
}
然后我們把要用到的計算封裝成方法,方便以后調用
獲取屏幕的寬高:
/**
* 獲取屏幕的高寬
*
* @param i
* @return
*/
public int getScene(int i) {
WindowManager wm = (WindowManager) getContext()
.getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
if (i == SCENEWIDTH) {
return width;
} else if (i == SCENEHEIGHT) {
return height;
}
return 0;
}
獲取標簽的高度:
/**
* 獲取標簽的高度
*
* @param paint
* @return
*/
public int getLableHeight(Paint paint) {
Paint.FontMetrics fm = paint.getFontMetrics();
return (int) Math.ceil(fm.descent - fm.ascent) + padding;
}
獲取標簽的寬度:
/**
* 獲取標簽的寬度
*
* @param paint
* @param str
* @return
*/
public int getLableWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet + padding;
}
因為我們后面會讓他們有外邊距和內邊距的效果,所以把 padding 直接在這里計算了
接著我們真正的在 onMeasure 里面來測量控件的寬高:
/**
* 控件的寬高測量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = getLableHeight(paint) + margin + padding;
int widths = 0;
if (listcount.size() == 0) {
width = 0;
height = 0;
} else {
for (int i = 0; i < listcount.size(); i++) {
int itemWidth = getLableWidth(paint, listcount.get(i));
if (widths + itemWidth > scenewidth) { //判斷有沒有換行
height += getLableHeight(paint) + margin + padding;
widths = margin;
isbr = false; //換行標識
}
if (isbr) {
width += itemWidth + margin;//如果沒有換行 就按實際的寬度去測量
} else {
width = widthSize; //只要換行了 就證明寬度是充滿的
}
widths += itemWidth + margin;
}
height += margin;//在底部加上一個外邊距
if (isbr)
width += margin;//這是為了在沒有換行的情況下 最后在加上一個外邊距
}
setMeasuredDimension(width, height);
}
寬高測量好之后我們就要去在 onLayout 里面安排標簽的位置:
/**
* 安排擺放位置
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int width = margin;
int height = margin;
if (count == 0) {
width = 0;
height = 0;
}
lableItems.clear();
// 獲取標簽高度
int itemheight = getLableHeight(paint);
for (int i = 0; i < count; i++) {
LableItem lableItem = (LableItem) getChildAt(i);
lableItems.add(lableItem);
// 獲取標簽寬度
int itemWidth = getLableWidth(paint, listcount.get(i))+margin;
if (width + itemWidth > scenewidth) {
//如果item的寬度加上下一個item的寬度大于屏幕寬度的話 那么這個時候就要換行了
height += itemheight + margin + padding;
width = margin;
}
lableItem.layout(width, height, width + itemWidth, height + itemheight + padding);
width += itemWidth + margin;
}
}
這樣就實現了瀑布流的效果,然后我們再給他加上一些供使用者調用的方法:
獲取選中的內容字符串:
public List<String> getSelectContent() {
List<String> list = new ArrayList<>();
for (int i = 0; i < lableItems.size(); i++) {
if (lableItems.get(i).isSelect()) {
list.add(lableItems.get(i).getText().toString());
}
}
return list;
}
取消全部選中:
public void clearSelect(){
for (int i=0;i<getChildCount();i++){
LableItem lableItem= (LableItem) getChildAt(i);
if (lableItem.isSelect()){
lableItem.setChecked(false);
lableItem.setTextColor(no_textcolor);
lableItem.setSelect(0);
if (onCancelAllSelectListener!=null){
onCancelAllSelectListener.cancelalllistener();
}
}
}
}
然后添加對外部使用的點擊事件的回調等,就大功告成了!
全部代碼如下:
package com.example.yinshuai.thelabel;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.RadioButton;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Created by 尹帥 on 2017/8/7.
*
* 瀑布流的標簽選擇
*
* 作者:尹帥 1317972280@qq.com
*/
public class Lable extends ViewGroup {
private final static int SCENEWIDTH = 1;
private final static int SCENEHEIGHT = 2;
private float textsize = 0;
private int select_textcolor=0;
private int no_textcolor=0;
private int padding = 0;
private int margin = 0;
private boolean ismultiple=true;
private int scenewidth;
private boolean isbr = true;
private Paint paint;
private List<String> listcount = new ArrayList<>();
private Context mContext;
private List<LableItem> lableItems = new ArrayList<>();
public OnItemSelectClickListener onItemSelectClick;
public OnCancelSelectClickListener onCancelSelectClick;
public OnCancelAllSelectListener onCancelAllSelectListener;
public Lable(Context context) {
super(context);
mContext = context;
init();
}
public Lable(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.Lable);
textsize=px2dip(typedArray.getDimensionPixelSize(R.styleable.Lable_lable_textSize,dip2px(16)));
select_textcolor=typedArray.getColor(R.styleable.Lable_lable_opttextColor, Color.BLACK);
no_textcolor=typedArray.getColor(R.styleable.Lable_lable_notextColor,Color.BLACK);
padding=typedArray.getInteger(R.styleable.Lable_lable_padding,10);
margin=typedArray.getInteger(R.styleable.Lable_lable_margin,8);
ismultiple=typedArray.getBoolean(R.styleable.Lable_lable_ismultiple,true);
init();
}
/**
* 對外提供選中監聽的接口
*/
public interface OnItemSelectClickListener {
void selectclick(String text, int position);
}
public void setOnItemSelectClickListener(OnItemSelectClickListener onItemClick) {
this.onItemSelectClick = onItemClick;
}
/**
* 對外提供取消item選中的接口
*/
public interface OnCancelSelectClickListener {
void cancelselectclick(String text, int position);
}
public void setOnCancelSelectClickListener(OnCancelSelectClickListener onCancelSelectClick) {
this.onCancelSelectClick = onCancelSelectClick;
}
public interface OnCancelAllSelectListener{
void cancelalllistener();
}
public void setOnCancelAllSelectListener(OnCancelAllSelectListener l){
onCancelAllSelectListener=l;
}
/**
* 初始化
*/
public void initLableItem() {
if (listcount == null || listcount.size() == 0)
return;
for (int i = 0; i < listcount.size(); i++) {
LableItem lableItem = new LableItem(mContext);
lableItem.setText(listcount.get(i));
lableItem.setTag(i + 1);
addView(lableItem);
}
}
public void init() {
paint = new Paint();
paint.setTextSize(dip2px(textsize));
scenewidth = getScene(SCENEWIDTH);
}
/**
* 設置數據
* @param data
*/
public void setDataList(List<String> data) {
this.listcount.addAll(data);
initLableItem();
}
/**
* 控件的寬高測量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = getLableHeight(paint) + margin + padding;
int widths = 0;
if (listcount.size() == 0) {
width = 0;
height = 0;
} else {
for (int i = 0; i < listcount.size(); i++) {
int itemWidth = getLableWidth(paint, listcount.get(i));
if (widths + itemWidth > scenewidth) { //判斷有沒有換行
height += getLableHeight(paint) + margin + padding;
widths = margin;
isbr = false; //換行標識
}
if (isbr) {
width += itemWidth + margin;//如果沒有換行 就按實際的寬度去測量
} else {
width = widthSize; //只要換行了 就證明寬度是充滿的
}
widths += itemWidth + margin;
}
height += margin;//在底部加上一個外邊距
if (isbr)
width += margin;//這是為了在沒有換行的情況下 最后在加上一個外邊距
}
setMeasuredDimension(width, height);
}
/**
* 安排擺放位置
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int width = margin;
int height = margin;
if (count == 0) {
width = 0;
height = 0;
}
lableItems.clear();
// 獲取標簽高度
int itemheight = getLableHeight(paint);
for (int i = 0; i < count; i++) {
LableItem lableItem = (LableItem) getChildAt(i);
lableItems.add(lableItem);
// 獲取標簽寬度
int itemWidth = getLableWidth(paint, listcount.get(i))+margin;
if (width + itemWidth > scenewidth) { //如果item的寬度加上下一個item的寬度大于屏幕寬度的話 那么這個時候就要換行了
height += itemheight + margin + padding;
width = margin;
}
lableItem.layout(width, height, width + itemWidth, height + itemheight + padding);
width += itemWidth + margin;
}
}
/**
* 獲取選中的內容字符串
*/
public List<String> getSelectContent() {
List<String> list = new ArrayList<>();
for (int i = 0; i < lableItems.size(); i++) {
if (lableItems.get(i).isSelect()) {
list.add(lableItems.get(i).getText().toString());
}
}
return list;
}
/**
* 取消全部選中
*/
public void clearSelect(){
for (int i=0;i<getChildCount();i++){
LableItem lableItem= (LableItem) getChildAt(i);
if (lableItem.isSelect()){
lableItem.setChecked(false);
lableItem.setTextColor(no_textcolor);
lableItem.setSelect(0);
if (onCancelAllSelectListener!=null){
onCancelAllSelectListener.cancelalllistener();
}
}
}
}
/**
* 獲取標簽的高度
*
* @param paint
* @return
*/
public int getLableHeight(Paint paint) {
Paint.FontMetrics fm = paint.getFontMetrics();
return (int) Math.ceil(fm.descent - fm.ascent) + padding;
}
/**
* 獲取標簽的寬度
*
* @param paint
* @param str
* @return
*/
public int getLableWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet + padding;
}
/**
* 標簽的Item
*/
class LableItem extends RadioButton {
private int select = 0;
public LableItem(Context context) {
super(context);
initStyle();
Click();
}
public LableItem(Context context, AttributeSet attrs) {
super(context, attrs);
initStyle();
Click();
}
/**
* 設置標簽item的樣式 需要修改樣式 直接修改drawable文件夾下select文件即可
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void initStyle() {
Bitmap bitmap = null;
setButtonDrawable(new BitmapDrawable(bitmap));
setBackground(getResources().getDrawable(R.drawable.select));
setTextSize(textsize);
setTextColor(no_textcolor);
setPadding(padding, padding, padding, padding);
setGravity(Gravity.CENTER);
}
public void Click() {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!ismultiple){
clearSelect();
}
if (select == 0) {
setChecked(true);
select = 1;
setTextColor(select_textcolor);
if (onItemSelectClick != null) {
onItemSelectClick.selectclick(getText().toString(), Integer.parseInt(getTag().toString()));
}
} else if (select == 1) {
setChecked(false);
select = 0;
setTextColor(no_textcolor);
if (onCancelSelectClick != null) {
onCancelSelectClick.cancelselectclick(getText().toString(), Integer.parseInt(getTag().toString()));
}
}
}
});
}
/**
* 判斷是否選中
*
* @return
*/
public boolean isSelect() {
return select == 0 ? false : true;
}
public void setSelect(int select){
this.select=select;
}
}
/**
* 獲取屏幕的高寬
*
* @param i
* @return
*/
public int getScene(int i) {
WindowManager wm = (WindowManager) getContext()
.getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
if (i == SCENEWIDTH) {
return width;
} else if (i == SCENEHEIGHT) {
return height;
}
return 0;
}
/**
* dp-->px
*
* @param dipValue
* @return
*/
private int dip2px(float dipValue) {
final float scale = mContext.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* sp轉px
*
* @return
*/
public int sp2px(float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, mContext.getResources().getDisplayMetrics());
}
/**
* 根據手機的分辨率從 px(像素) 的單位 轉成為 dp
*/
public int px2dip( float pxValue) {
final float scale = mContext.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 將px值轉換為sp值,保證文字大小不變
*
* @param pxValue
* @return
*/
public int px2sp( float pxValue) {
final float fontScale = mContext.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
}