最近需要做一个类似标签选择的功能,类似饿了么的评价页面的标签选择,没办法,项目需要,代码撸起来
先来看看实现效果:
最终的效果就是上面那个样子,接下来来看看实现步骤:
既然里面是一个个的标签,那我们就先把标签做出来,我这边是直接继承 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);
}
}