最近总感觉写博客的激情不高,不知道为啥。放上效果图,demo在最下面
图上那个切换按钮的作用呢,就是模拟改变标签的个数动态变化整个控件的高度。
其实这个控件也算很简单的控件了。关键点只有两个
- 如何控制标签自动换行
- 切换数据源时动态改变控件的高度
再简单的控件也需要一点一点的码出来,咱就从最基础的属性设置开始。
public FlowTagView textColor(int defaultColor, int selectedColor){ this.textColorDefault = defaultColor; this.textColorSelected = selectedColor; return this; } public FlowTagView textSize(int textSize){ this.textSize = textSize; return this; } public FlowTagView backgroundColor(int defaultColor, int selectedColor){ this.backgroundColorDefault = defaultColor; this.backgroundColorSelected = selectedColor; return this; } public FlowTagView padding(int horizontalPadding, int verticalPadding, int textHorizontalPadding){ this.horizontalPadding = horizontalPadding; this.verticalPadding = verticalPadding; this.textHorizontalPadding = textHorizontalPadding; return this; } public FlowTagView itemHeight(int height){ this.itemHeight = height; return this; } public FlowTagView datas(String[] datas){ this.datas = datas; return this; } public FlowTagView listener(OnTagSelectedListener listener){ this.listener = listener; return this; }
上面设置了字体颜色啊,背景颜色啊,标签Item的高度啊,内补白和外部白的一些值,还有一个监听器。有的朋友就说了,我比较懒,就想快点看到效果,不想设置怎么办?怎么办?给默认值呗。
//常亮默认值,这些参数若不调用方法传递,则直接使用默认值 public static final int ROUND_RADIUS = 30; public static final int TEXT_COLOR_DEFAULT = Color.BLACK; public static final int TEXT_COLOR_SELECTED = Color.WHITE; public static final int TEXT_SIZE = 30; public static final int BACKGROUND_COLOR_DEFAULT = Color.GRAY; public static final int BACKGROUND_COLOR_SELECTED = Color.GREEN; public static final int HORIZONTAL_PADDING = 30; public static final int VERTICAL_PADDING = 30; public static final int TEXT_HORIZONTAL_PADDING = 30; public static final int ITEM_HEIGHT = 60; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private int textColorDefault = TEXT_COLOR_DEFAULT; private int textColorSelected = TEXT_COLOR_SELECTED; private int textSize = TEXT_SIZE; private int backgroundColorDefault = BACKGROUND_COLOR_DEFAULT; private int backgroundColorSelected = BACKGROUND_COLOR_SELECTED; //Tag之间的横向和纵向的间隔 private int horizontalPadding = HORIZONTAL_PADDING; private int verticalPadding = VERTICAL_PADDING; //每个Tag内部的横向间隔 private int textHorizontalPadding = TEXT_HORIZONTAL_PADDING; //每个Tag的高度 private int itemHeight = ITEM_HEIGHT;
好了,基本的属性设置的代码完成了,那么就用软件的高内聚低耦合的思想封装一个标签类吧。
public class Tag{ //文本属性 public String text; public int textColorDefault; public int textColorSelected; public int backgroundColorDefault; public int backgroundColorSelected; public boolean isSelected; public Paint paint; //文本的绘制起点 public int drawX; public int drawY; //整个Tag占用的坐标范围 public RectF rect = new RectF(); public Tag(String text, int textSize, int textColorDefault, int textColorSelected,
int backgroundColorDefault, int backgroundColorSelected, Paint paint, int height, int horizontalPadding, int startX, int startY){ this.text = text; this.textColorDefault = textColorDefault; this.textColorSelected = textColorSelected; this.backgroundColorDefault = backgroundColorDefault; this.backgroundColorSelected = backgroundColorSelected; this.paint = paint; //求出整个Tag的宽度 paint.setTextSize(textSize); int textWidth = (int)paint.measureText(text); int width = textWidth + 2 * horizontalPadding; //计算坐标范围,startX,staryY是指左上角的起点 rect.left = startX; rect.top = startY; rect.right = startX + width; rect.bottom = startY + height; //计算居中绘制时的绘制起点 drawX = startX + horizontalPadding; Paint.FontMetrics metrics = paint.getFontMetrics(); drawY = (int)(startY + height / 2 + (metrics.bottom - metrics.top) / 2 - metrics.bottom); } public void draw(Canvas canvas){ if(isSelected){ //绘制背景 paint.setColor(backgroundColorSelected); paint.setStyle(Paint.Style.FILL); canvas.drawRoundRect(rect, ROUND_RADIUS, ROUND_RADIUS, paint); //绘制文本 paint.setColor(textColorSelected); canvas.drawText(text, drawX, drawY, paint); }else{ //绘制背景 paint.setColor(backgroundColorDefault); paint.setStyle(Paint.Style.STROKE); canvas.drawRoundRect(rect, ROUND_RADIUS, ROUND_RADIUS, paint); //绘制文本 paint.setColor(textColorDefault); canvas.drawText(text, drawX, drawY, paint); } } }
这个封装类就两个方法,一个是构造方法,一个是绘制方法。构造方法就是对属性的一些赋值。然后利用startX和startY计算出每个标签的坐标范围和文本的绘制起点。绘制方法draw(Canvas canvas)就简单得绘制一个文本和一个背景。想定制标签的样式的话,就在这个方法进行重写。
好了,这个封装类其实也不算难。接下来就来到最关键的地方了。startX和startY的取值。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ int width = MeasureSpec.getSize(widthMeasureSpec); //算出绘制起点 startX = getPaddingLeft(); startY = getPaddingTop(); tags.clear(); for(int i = 0; i < datas.length; i++){ //判断是否越过边界 if(startX + getRealWidth(paint, textSize, datas[i], textHorizontalPadding) + horizontalPadding
> width - getPaddingRight()){ //在下一行开始绘制 startX = getPaddingLeft(); startY += itemHeight + verticalPadding; } Tag tag = new Tag(datas[i], textSize, textColorDefault, textColorSelected, backgroundColorDefault, backgroundColorSelected, paint, itemHeight, textHorizontalPadding, startX, startY); tags.add(tag); //动态更新值 startX += getRealWidth(paint, textSize, datas[i], textHorizontalPadding) + horizontalPadding; } //算出整个控件需要的高度 int height = startY + itemHeight + getPaddingBottom(); setMeasuredDimension(width, height); }
这里用到了一个工具方法getRealWidth,这个就是用来计算每一个标签的真实宽度的。
/** * 根据参数算出某个Tag所需要占用的宽度值,包括内补白 */ public static int getRealWidth(Paint paint, int textSize, String text, int textHorizontalPadding){ paint.setTextSize(textSize); int textWidth = (int)paint.measureText(text); return textWidth + 2 * textHorizontalPadding; }
代码不多,但是的确是最重要的地方。首先拿到startX和startY的初始值。默认为padding值。然后对文本进行遍历。当当前文本的绘制终点大于该行的最大值,则重置startX,并且将startY累加一次标签的高度值与竖直补白值。然后进行该标签的实例化。然后别忘了对startX进行重新赋值。最后得到整个控件实际得高度,设置该控件的高度。
有的小伙伴就问了,要是我的数据源发生了变化,怎么动态改变高度值以及刷新数据源呢。这也是我刚才提到的第二个重点,这个问题我找了很多办法,最优秀的办法就是利用LayoutParams。
public void commit(){ if(datas == null){ Log.e("FlowTagView", "maybe not invok the method named datas(String[])"); throw new IllegalStateException("maybe not invok the method named datas(String[])"); } paint.setTextSize(textSize); if(datas.length != tags.size()){ //重新实例化 ViewGroup.LayoutParams params = getLayoutParams(); setLayoutParams(params); } }
在外界设置属性的时候,最后一个链一定要调用commit方法进行提交,这里直接得到当前的LayoutParams,然后再次设置回去。这样做有什么用呢?用处就是为了触发onMeasure方法。哈哈,onMeasure方法会自动进行重计算的。机智如我。
接下来就处理点击事件了,首先定义一个自定义的接口。
public interface OnTagSelectedListener{ void onTagSelected(FlowTagView view, int position); }
祭出最最最常用的onTouchEvent方法,前提是有几个成员变量。
//点击事件的滑动距离阈值 private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); //ACTION_DOWN时的坐标值 private float mTouchX; private float mTouchY; //ACTION_DOWN时选中的tag的索引 private int mTouchPosition;
onTouchEvent方法进行事件分发。
@Override public boolean onTouchEvent(MotionEvent event){ switch(event.getAction()){ case MotionEvent.ACTION_DOWN: mTouchX = event.getX(); mTouchY = event.getY(); mTouchPosition = getTagPosition(mTouchX, mTouchY); return true; case MotionEvent.ACTION_UP: float mUpX = event.getX(); float mUpY = event.getY(); //滑动距离小于点击阈值并且点击时的索引值不是非法值,并且up时的索引值和down时的索引值相等时,才触发选中操作 if(Math.abs(mUpX - mTouchX) < mTouchSlop && Math.abs(mUpY - mTouchY) < mTouchSlop && mTouchPosition != -1 && getTagPosition(mUpX, mUpY) == mTouchPosition){ //触发点击选中 setSelect(mTouchPosition); } break; } return super.onTouchEvent(event); }
其实就是一个模拟点击的操作。对于抬起和按下时的坐标不超过一个给定阈值,并且抬起和按下时点击的标签是同一个的话,才触发选中的操作。也就是setSelect方法。
/** * 根本坐标值,返回对应的tag的索引,若不存在则返回-1 */ private int getTagPosition(float x, float y){ for(int i = 0; i < tags.size(); i++){ if(tags.get(i).rect.contains(x, y)){ return i; } } return -1; } public void setSelect(int position){ if(position < 0 || position >= tags.size()){ Log.e("FlowTagView", "the position is illetal"); throw new IllegalArgumentException("the position is illetal"); } for(int i = 0; i < tags.size(); i++){ //关闭其他选择 if(i != position){ tags.get(i).isSelected = false; }else{ tags.get(i).isSelected = true; } } //触发监听器 if(listener != null){ listener.onTagSelected(this, position); } //必须要刷新UI invalidate(); } public int getSelect(){ for(int i = 0; i < tags.size(); i++){ if(tags.get(i).isSelected){ return i; } } return -1; }
好了,这个自定义控件的讲解就结束了,按照我的习惯,此时应该贴出这个控件的完整代码,我相信不少小伙伴儿会因为字多而忽略掉。。
package cc.wxf.component; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; /** * Created by ccwxf on 2016/7/21. */ public class FlowTagView extends View { //常亮默认值,这些参数若不调用方法传递,则直接使用默认值 public static final int ROUND_RADIUS = 30; public static final int TEXT_COLOR_DEFAULT = Color.BLACK; public static final int TEXT_COLOR_SELECTED = Color.WHITE; public static final int TEXT_SIZE = 30; public static final int BACKGROUND_COLOR_DEFAULT = Color.GRAY; public static final int BACKGROUND_COLOR_SELECTED = Color.GREEN; public static final int HORIZONTAL_PADDING = 30; public static final int VERTICAL_PADDING = 30; public static final int TEXT_HORIZONTAL_PADDING = 30; public static final int ITEM_HEIGHT = 60; private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private int textColorDefault = TEXT_COLOR_DEFAULT; private int textColorSelected = TEXT_COLOR_SELECTED; private int textSize = TEXT_SIZE; private int backgroundColorDefault = BACKGROUND_COLOR_DEFAULT; private int backgroundColorSelected = BACKGROUND_COLOR_SELECTED; //Tag之间的横向和纵向的间隔 private int horizontalPadding = HORIZONTAL_PADDING; private int verticalPadding = VERTICAL_PADDING; //每个Tag内部的横向间隔 private int textHorizontalPadding = TEXT_HORIZONTAL_PADDING; //每个Tag的高度 private int itemHeight = ITEM_HEIGHT; //tag的绘制起点,动态计算得值 private int startX; private int startY; //Tag显示的文本 private String[] datas; private List<Tag> tags = new ArrayList<Tag>(); //点击事件的滑动距离阈值 private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); //ACTION_DOWN时的坐标值 private float mTouchX; private float mTouchY; //ACTION_DOWN时选中的tag的索引 private int mTouchPosition; private OnTagSelectedListener listener; public FlowTagView(Context context, AttributeSet attrs, int defStyleAttr){ super(context, attrs, defStyleAttr); } public FlowTagView(Context context, AttributeSet attrs){ super(context, attrs); } public FlowTagView(Context context){ super(context); } public FlowTagView textColor(int defaultColor, int selectedColor){ this.textColorDefault = defaultColor; this.textColorSelected = selectedColor; return this; } public FlowTagView textSize(int textSize){ this.textSize = textSize; return this; } public FlowTagView backgroundColor(int defaultColor, int selectedColor){ this.backgroundColorDefault = defaultColor; this.backgroundColorSelected = selectedColor; return this; } public FlowTagView padding(int horizontalPadding, int verticalPadding, int textHorizontalPadding){ this.horizontalPadding = horizontalPadding; this.verticalPadding = verticalPadding; this.textHorizontalPadding = textHorizontalPadding; return this; } public FlowTagView itemHeight(int height){ this.itemHeight = height; return this; } public FlowTagView datas(String[] datas){ this.datas = datas; return this; } public FlowTagView listener(OnTagSelectedListener listener){ this.listener = listener; return this; } public void commit(){ if(datas == null){ Log.e("FlowTagView", "maybe not invok the method named datas(String[])"); throw new IllegalStateException("maybe not invok the method named datas(String[])"); } paint.setTextSize(textSize); if(datas.length != tags.size()){ //重新实例化 ViewGroup.LayoutParams params = getLayoutParams(); setLayoutParams(params); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ int width = MeasureSpec.getSize(widthMeasureSpec); //算出绘制起点 startX = getPaddingLeft(); startY = getPaddingTop(); tags.clear(); for(int i = 0; i < datas.length; i++){ //判断是否越过边界 if(startX + getRealWidth(paint, textSize, datas[i], textHorizontalPadding) + horizontalPadding > width - getPaddingRight()){ //在下一行开始绘制 startX = getPaddingLeft(); startY += itemHeight + verticalPadding; } Tag tag = new Tag(datas[i], textSize, textColorDefault, textColorSelected, backgroundColorDefault, backgroundColorSelected, paint, itemHeight, textHorizontalPadding, startX, startY); tags.add(tag); //动态更新值 startX += getRealWidth(paint, textSize, datas[i], textHorizontalPadding) + horizontalPadding; } //算出整个控件需要的高度 int height = startY + itemHeight + getPaddingBottom(); setMeasuredDimension(width, height); } /** * 根据参数算出某个Tag所需要占用的宽度值,包括内补白 */ public static int getRealWidth(Paint paint, int textSize, String text, int textHorizontalPadding){ paint.setTextSize(textSize); int textWidth = (int)paint.measureText(text); return textWidth + 2 * textHorizontalPadding; } @Override protected void onDraw(Canvas canvas){ //绘制代理 for(int i = 0; i < tags.size(); i++){ tags.get(i).draw(canvas); } } @Override public boolean onTouchEvent(MotionEvent event){ switch(event.getAction()){ case MotionEvent.ACTION_DOWN: mTouchX = event.getX(); mTouchY = event.getY(); mTouchPosition = getTagPosition(mTouchX, mTouchY); return true; case MotionEvent.ACTION_UP: float mUpX = event.getX(); float mUpY = event.getY(); //滑动距离小于点击阈值并且点击时的索引值不是非法值,并且up时的索引值和down时的索引值相等时,才触发选中操作 if(Math.abs(mUpX - mTouchX) < mTouchSlop && Math.abs(mUpY - mTouchY) < mTouchSlop && mTouchPosition != -1 && getTagPosition(mUpX, mUpY) == mTouchPosition){ //触发点击选中 setSelect(mTouchPosition); } break; } return super.onTouchEvent(event); } /** * 根本坐标值,返回对应的tag的索引,若不存在则返回-1 */ private int getTagPosition(float x, float y){ for(int i = 0; i < tags.size(); i++){ if(tags.get(i).rect.contains(x, y)){ return i; } } return -1; } public void setSelect(int position){ if(position < 0 || position >= tags.size()){ Log.e("FlowTagView", "the position is illetal"); throw new IllegalArgumentException("the position is illetal"); } for(int i = 0; i < tags.size(); i++){ //关闭其他选择 if(i != position){ tags.get(i).isSelected = false; }else{ tags.get(i).isSelected = true; } } //触发监听器 if(listener != null){ listener.onTagSelected(this, position); } //必须要刷新UI invalidate(); } public int getSelect(){ for(int i = 0; i < tags.size(); i++){ if(tags.get(i).isSelected){ return i; } } return -1; } public class Tag{ //文本属性 public String text; public int textColorDefault; public int textColorSelected; public int backgroundColorDefault; public int backgroundColorSelected; public boolean isSelected; public Paint paint; //文本的绘制起点 public int drawX; public int drawY; //整个Tag占用的坐标范围 public RectF rect = new RectF(); public Tag(String text, int textSize, int textColorDefault, int textColorSelected, int backgroundColorDefault, int backgroundColorSelected, Paint paint, int height, int horizontalPadding, int startX, int startY){ this.text = text; this.textColorDefault = textColorDefault; this.textColorSelected = textColorSelected; this.backgroundColorDefault = backgroundColorDefault; this.backgroundColorSelected = backgroundColorSelected; this.paint = paint; //求出整个Tag的宽度 paint.setTextSize(textSize); int textWidth = (int)paint.measureText(text); int width = textWidth + 2 * horizontalPadding; //计算坐标范围,startX,staryY是指左上角的起点 rect.left = startX; rect.top = startY; rect.right = startX + width; rect.bottom = startY + height; //计算居中绘制时的绘制起点 drawX = startX + horizontalPadding; Paint.FontMetrics metrics = paint.getFontMetrics(); drawY = (int)(startY + height / 2 + (metrics.bottom - metrics.top) / 2 - metrics.bottom); } public void draw(Canvas canvas){ if(isSelected){ //绘制背景 paint.setColor(backgroundColorSelected); paint.setStyle(Paint.Style.FILL); canvas.drawRoundRect(rect, ROUND_RADIUS, ROUND_RADIUS, paint); //绘制文本 paint.setColor(textColorSelected); canvas.drawText(text, drawX, drawY, paint); }else{ //绘制背景 paint.setColor(backgroundColorDefault); paint.setStyle(Paint.Style.STROKE); canvas.drawRoundRect(rect, ROUND_RADIUS, ROUND_RADIUS, paint); //绘制文本 paint.setColor(textColorDefault); canvas.drawText(text, drawX, drawY, paint); } } } public interface OnTagSelectedListener{ void onTagSelected(FlowTagView view, int position); } }
最后一段代码一定是放使用方法,这是我的习惯。。
<?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" > <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="切换" /> <cc.wxf.component.FlowTagView android:id="@+id/tagView" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" /> </LinearLayout>
package cc.wxf.androiddemo; import android.app.Activity; import android.content.res.Resources; import android.os.Bundle; import android.view.View; import android.widget.Toast; import cc.wxf.component.FlowTagView; public class MainActivity extends Activity { private int i = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final String[] datas1 = new String[]{ "推荐", "电影", "电视剧", "头条", "娱乐", "动漫", "猜你喜欢", "资讯", "搞笑", "体育", "综艺", "片花", "少儿", "今日头条", "娱乐", "动漫", "猜你喜欢", "资讯", "搞笑", "体育", "综艺" }; final String[] datas2 = new String[]{ "推荐", "电影", "电视剧", "头条", "娱乐", "动漫", "猜你喜欢", "资讯" }; Resources resources = getResources(); final FlowTagView tagView = (FlowTagView) findViewById(R.id.tagView); tagView.datas(datas1) //下面的5个方法若不设置,则会采用默认值 .textColor(resources.getColor(android.R.color.darker_gray), resources.getColor(android.R.color.white)) .textSize(sp2px(15)) .backgroundColor(resources.getColor(android.R.color.darker_gray), resources.getColor(android.R.color.holo_green_light)) .itemHeight(dp2px(40)) .padding(dp2px(10), dp2px(10), dp2px(15)) //上面的5个方法若不设置,则会采用默认值 .listener(new FlowTagView.OnTagSelectedListener() { @Override public void onTagSelected(FlowTagView view, int position) { Toast.makeText(MainActivity.this, "选中了:" + position, Toast.LENGTH_SHORT).show(); } }) //commit必须调用 .commit(); //模拟标签的个数发生变化,造成控件的自动伸展 findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { i ++; //commit必须调用 tagView.datas(i % 2 == 0 ? datas1 : datas2).commit(); } }); } public int sp2px(int sp){ float density = getResources().getDisplayMetrics().scaledDensity; return (int) (sp * density + 0.5f); } public int dp2px(int dp){ float density = getResources().getDisplayMetrics().density; return (int) (dp * density + 0.5f); } }
Over,最后是demo的下载地址。哎,最近写博客没激情,闭关一段时间算了。