Android流式标签布局,自定义标签控件tagView
来源:互联网 发布:linux重启oracle监听 编辑:程序博客网 时间:2024/04/27 01:21
我们在一些项目中会用到自定义流式布局,我个人觉得流式布局将呆板的布局错综排列,来提升用户体验度.(还可以不辜负美工妹子们的期望,人家毕竟也辛辛苦苦设计半天)。今天终于有时间来做做了。写的不好,很多地方值得改进望大家一起交流。
这是效果图:
实现基本功能:
首先来说明几点:
1.标签视图TagView直接继承TextView,这样有几个好处:不用去重写onMeasure()接口, 不用自己绘制Text,对Text控制也方便;
2.标签布局TagGroup继承ViewGroup,需要重写onMeasure()和onLayout()方法来控制 TagView的显示;
1. 实现TagView:
public class TagView extends TextView {// 3种模式:圆角矩形、圆弧、直角矩形public final static int MODE_ROUND_RECT = 1;public final static int MODE_ARC = 2;public final static int MODE_RECT = 3;private Paint mPaint;// 背景色private int mBgColor;// 边框颜色private int mBorderColor;// 边框大小private float mBorderWidth;// 边框角半径private float mRadius;// Tag内容private CharSequence mTagText;// 字体水平空隙private int mHorizontalPadding;// 字体垂直空隙private int mVerticalPadding;// 边框矩形private RectF mRect;// 调整标志位,只做一次private boolean mIsAdjusted = false;// 点击监听器private OnTagClickListener mTagClickListener;// 显示模式private int mTagMode = MODE_ROUND_RECT;public TagView(Context context, String text) {super(context);setText(text);_init(context);}public TagView(Context context, AttributeSet attrs) {super(context, attrs);_init(context);}/** * 初始化 * * @param context */private void _init(Context context) {mRect = new RectF();mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mTagText = getText();// 设置字体占中setGravity(Gravity.CENTER);setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {if (mTagClickListener != null) {mTagClickListener.onTagClick(String.valueOf(mTagText));}}});setOnLongClickListener(new OnLongClickListener() {@Overridepublic boolean onLongClick(View v) {if (mTagClickListener != null) {mTagClickListener.onTagLongClick(String.valueOf(mTagText));}return true;}});}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);_adjustText();}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);// 设置矩形边框mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h- mBorderWidth);}@Overrideprotected void onDraw(Canvas canvas) {// 绘制背景mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(mBgColor);float radius = mRadius;if (mTagMode == MODE_ARC) {radius = mRect.height() / 2;} else if (mTagMode == MODE_RECT) {radius = 0;}canvas.drawRoundRect(mRect, radius, radius, mPaint);// 绘制边框mPaint.setStyle(Paint.Style.STROKE);mPaint.setStrokeWidth(mBorderWidth);mPaint.setColor(mBorderColor);canvas.drawRoundRect(mRect, radius, radius, mPaint);super.onDraw(canvas);}/** * 调整内容,如果超出可显示的范围则做裁剪 */private void _adjustText() {if (mIsAdjusted) {return;}mIsAdjusted = true;// 获取可用宽度int availableWidth = ((TagGroup) getParent()).getAvailableWidth();mPaint.setTextSize(getTextSize());// 计算字符串长度float textWidth = mPaint.measureText(String.valueOf(mTagText));// 如果可用宽度不够用,则做裁剪处理,末尾不3个.if (textWidth + mHorizontalPadding * 2 > availableWidth) {float pointWidth = mPaint.measureText(".");// 计算能显示的字体长度float maxTextWidth = availableWidth - mHorizontalPadding * 2- pointWidth * 3;float tmpWidth = 0;StringBuilder strBuilder = new StringBuilder();for (int i = 0; i < mTagText.length(); i++) {char c = mTagText.charAt(i);float cWidth = mPaint.measureText(String.valueOf(c));// 计算每个字符的宽度之和,如果超过能显示的长度则退出if (tmpWidth + cWidth > maxTextWidth) {break;}strBuilder.append(c);tmpWidth += cWidth;}// 末尾添加3个.并设置为显示字符strBuilder.append("...");setText(strBuilder.toString());}}/******************************************************************/public int getBgColor() {return mBgColor;}public void setBgColor(int bgColor) {mBgColor = bgColor;}public int getBorderColor() {return mBorderColor;}public void setBorderColor(int borderColor) {mBorderColor = borderColor;}public float getBorderWidth() {return mBorderWidth;}public void setBorderWidth(float borderWidth) {mBorderWidth = borderWidth;}public float getRadius() {return mRadius;}public void setRadius(float radius) {mRadius = radius;}public int getHorizontalPadding() {return mHorizontalPadding;}public void setHorizontalPadding(int horizontalPadding) {mHorizontalPadding = horizontalPadding;setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding,mVerticalPadding);}public int getVerticalPadding() {return mVerticalPadding;}public void setVerticalPadding(int verticalPadding) {mVerticalPadding = verticalPadding;setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding,mVerticalPadding);}public CharSequence getTagText() {return mTagText;}public void setTagText(CharSequence tagText) {mTagText = tagText;}/********************************* 点击监听 *********************************/public OnTagClickListener getTagClickListener() {return mTagClickListener;}public void setTagClickListener(OnTagClickListener tagClickListener) {mTagClickListener = tagClickListener;}/** * 点击监听器 */public interface OnTagClickListener {void onTagClick(String text);void onTagLongClick(String text);}/********************************* 显示模式 *********************************/public int getTagMode() {return mTagMode;}public void setTagMode(@TagMode int tagMode) {mTagMode = tagMode;}@IntDef({ MODE_ROUND_RECT, MODE_ARC, MODE_RECT })@Retention(RetentionPolicy.SOURCE)@Target(ElementType.PARAMETER)public @interface TagMode {}}其实还是很简单的,主要通过一些属性来设置绘制的效果,包括背景、边框和文字。在代码中设置了文字占中,并在onSizeChanged()方法中设置了边框矩形,其它就没什么了看代码就好了。
2.ViewGroup的实现:
public class TagGroup extends ViewGroup { private Paint mPaint; // 背景色 private int mBgColor; // 边框颜色 private int mBorderColor; // 边框大小 private float mBorderWidth; // 边框角半径 private float mRadius; // Tag之间的垂直间隙 private int mVerticalInterval; // Tag之间的水平间隙 private int mHorizontalInterval; // 边框矩形 private RectF mRect; public TagGroup(Context context) { this(context, null); } public TagGroup(Context context, AttributeSet attrs) { this(context, attrs, -1); } public TagGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); _init(context); } private void _init(Context context) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBgColor = Color.parseColor("#11FF0000"); mBorderColor = Color.parseColor("#22FF0000"); mBorderWidth = MeasureUtils.dp2px(context, 1f); mRadius = MeasureUtils.dp2px(context, 5f); int defaultInterval = (int) MeasureUtils.dp2px(context, 5f); mHorizontalInterval = defaultInterval; mVerticalInterval = defaultInterval; mRect = new RectF(); // 如果想要自己绘制内容,则必须设置这个标志位为false,否则onDraw()方法不会调用 setWillNotDraw(false); setPadding(defaultInterval, defaultInterval, defaultInterval, defaultInterval); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); // 计算可用宽度,为测量宽度减去左右padding值 int availableWidth = widthSpecSize - getPaddingLeft() - getPaddingRight(); // 测量子视图 measureChildren(widthMeasureSpec, heightMeasureSpec); int childCount = getChildCount(); int tmpWidth = 0; int measureHeight = 0; int maxLineHeight = 0; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); // 记录该行的最大高度 if (maxLineHeight == 0) { maxLineHeight = child.getMeasuredHeight(); } else { maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight()); } // 统计该行TagView的总宽度 tmpWidth += child.getMeasuredWidth() + mHorizontalInterval; // 如果超过可用宽度则换行 if (tmpWidth - mHorizontalInterval > availableWidth) { // 统计TagGroup的测量高度,要加上垂直间隙 measureHeight += maxLineHeight + mVerticalInterval; // 重新赋值 tmpWidth = child.getMeasuredWidth() + mHorizontalInterval; maxLineHeight = child.getMeasuredHeight(); } } // 统计TagGroup的测量高度,加上最后一行 measureHeight += maxLineHeight; // 设置测量宽高,记得算上padding if (childCount == 0) { setMeasuredDimension(0, 0); } else if (heightSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize, measureHeight + getPaddingTop() + getPaddingBottom()); } else { setMeasuredDimension(widthSpecSize, heightSpecSize); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); if (childCount <= 0) { return; } int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); // 当前布局使用的top坐标 int curTop = getPaddingTop(); // 当前布局使用的left坐标 int curLeft = getPaddingLeft(); int maxHeight = 0; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (maxHeight == 0) { maxHeight = child.getMeasuredHeight(); } else { maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); } int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); // 超过一行做换行操作 if (width + curLeft > availableWidth) { curLeft = getPaddingLeft(); // 计算top坐标,要加上垂直间隙 curTop += maxHeight + mVerticalInterval; maxHeight = child.getMeasuredHeight(); } // 设置子视图布局 child.layout(curLeft, curTop, curLeft + width, curTop + height); // 计算left坐标,要加上水平间隙 curLeft += width + mHorizontalInterval; } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h - mBorderWidth); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制背景 mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mBgColor); canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint); // 绘制边框 mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mBorderWidth); mPaint.setColor(mBorderColor); canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint); } /******************************************************************/ /** * 添加Tag * @param text tag内容 */ public void addTag(String text) { addView(new TagView(getContext(), text)); } public void addTags(String... textList) { for (String text : textList) { addTag(text); } } public void cleanTags() { removeAllViews(); postInvalidate(); } public void setTags(String... textList) { cleanTags(); addTags(textList); }}其实代码主要看onMeasure()和onLayout()两个方法。在onMeasure()我们要对布局进行测量,遍历所有子视图来计算布局的最终宽高,需要注意的是要把布局的padding属性计算上去,所以布局可用宽度为测量宽度减去左右两边的padding值,除了padding需要计算外,还要计算上TagView之间的间隙值。具体的测量过程代码注释的挺清楚,看下就懂了。
然后再看onLayout(),这个和onMeasure()其实挺像的,同样要计算上padding和间隙值,然后就是一个一个算出每个TagView的上下左右坐标,再调用TagView的layout()方法来设置到布局中的相应位置。
在写测试的时候我遇到一个问题:字符串过长的问题,因此需要裁剪。我的思路是这样:
首先太长的字符串截取前面的部分,并在后面补上3个“.”,就类似省略号;既然要裁剪就要知道最大可用的布局宽度,这个要从父布局中获取,需要TagGroup提供接口;最后计算的时候也要算上TagView的padding值,然后一个字符一个字符测量到符合要求;
/** * 调整内容,如果超出可显示的范围则做裁剪 */private void _adjustText() { if (mIsAdjusted) { return; } mIsAdjusted = true; // 获取可用宽度 int availableWidth = ((TagGroup) getParent()).getAvailableWidth(); mPaint.setTextSize(getTextSize()); // 计算字符串长度 float textWidth = mPaint.measureText(String.valueOf(mTagText)); // 如果可用宽度不够用,则做裁剪处理,末尾不3个. if (textWidth + mHorizontalPadding * 2 > availableWidth) { float pointWidth = mPaint.measureText("."); // 计算能显示的字体长度 float maxTextWidth = availableWidth - mHorizontalPadding * 2 - pointWidth * 3; float tmpWidth = 0; StringBuilder strBuilder = new StringBuilder(); for (int i = 0; i < mTagText.length(); i++) { char c = mTagText.charAt(i); float cWidth = mPaint.measureText(String.valueOf(c)); // 计算每个字符的宽度之和,如果超过能显示的长度则退出 if (tmpWidth + cWidth > maxTextWidth) { break; } strBuilder.append(c); tmpWidth += cWidth; } // 末尾添加3个.并设置为显示字符 strBuilder.append("..."); setText(strBuilder.toString()); }
3.这是MainActivity:
public class MainActivity extends Activity {private String[] mTagWords = new String[] {"Hello","Android","我是TagView","This is a long string, This is a long string, This is a long string","这是长字符串,这是长字符串,这是长字符串,这是长字符串", "故事开始在最初的那个梦中", "赛任的歌会让人忘记初衷","我会想奥德修斯一样" };private TagGroup mTagGroup;private Button mBtnAdd;private Button mBtnClean;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mTagGroup = (TagGroup) findViewById(R.id.tag_group);mBtnAdd = (Button) findViewById(R.id.btn_add);mBtnClean = (Button) findViewById(R.id.btn_clean);mBtnAdd.setOnClickListener(new View.OnClickListener() {Random random = new Random();@Overridepublic void onClick(View arg0) {mTagGroup.addTag(mTagWords[random.nextInt(mTagWords.length)]);}});mBtnClean.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View arg0) {mTagGroup.cleanTags();}});mTagGroup.setTags(mTagWords);mTagGroup.setTagBgColor(getResources().getColor(android.R.color.holo_red_light));mTagGroup.setTagBorderColor(getResources().getColor(android.R.color.holo_red_dark));mTagGroup.setTagTextColor(Color.WHITE);mTagGroup.setTagMode(TagView.MODE_ARC);mTagGroup.setBgColor(getResources().getColor(android.R.color.holo_orange_light));mTagGroup.setBorderColor(getResources().getColor(android.R.color.holo_blue_dark));mTagGroup.setBorderWidth(1);mTagGroup.setOnTagClickListener(new TagView.OnTagClickListener() {@Overridepublic void onTagLongClick(String text) {Log.w("MainActivity", text);Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();}@Overridepublic void onTagClick(String text) {Log.e("MainActivity", text);Toast.makeText(MainActivity.this, "长点击:" + text,Toast.LENGTH_SHORT).show();}});}}
add与clear的监听事件:
先在TagView中实现监听器接口OnTagClickListener,并对外提供方法来设置监听器,其实和大部分设置监听器一个样。然后给TagView设置OnClickListener和OnLongClickListener,并来执行OnTagClickListener回调方法。
public OnTagClickListener getTagClickListener() { return mTagClickListener;}public void setTagClickListener(OnTagClickListener tagClickListener) { mTagClickListener = tagClickListener;}/** * 点击监听器 */public interface OnTagClickListener{ void onTagClick(String text); void onTagLongClick(String text);}/** * 初始化 * @param context */private void _init(Context context) { // 略...... setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mTagClickListener != null) { mTagClickListener.onTagClick(String.valueOf(mTagText)); } } }); setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { if (mTagClickListener != null) { mTagClickListener.onTagLongClick(String.valueOf(mTagText)); } return true; } });}
现在要做的就是通过TagGroup来对外提供OnTagClickListener的设置接口,但是有一点要注意的是,如果你先添加Tags再设置监听器就可能出现前面设置的Tags没办法响应点击,所以你需要在设置监听器的地方为前面设置的Tags都重新添加上监听器,当然了你需要在之前保存好设置过的TagView。
到此关于Android的流式布局的例子就写的差不多了,我其中也借鉴了其他大神的文章。共勉,我也要下班了,饭还没吃,饿死了。
- Android流式标签布局,自定义标签控件tagView
- Android笔记(23)TagView标签
- Android自定义流式标签控件
- Android--View自定义—标签<流式布局>
- 自定义流式布局,标签展示
- 自定义Android商品标签控件
- Android 标签数字自定义控件
- android流式布局--流式标签
- Android 流式布局-动态标签实现
- android 流式布局(热门标签)
- 标签流式布局
- 标签流式布局
- 流式布局--标签
- TagGroup自定义标签布局
- Android 三角标签(自定义Textview控件)
- Android自定义标签列表控件LabelsView解析
- android自定义view实现流式布局(FlowLayout)和热门标签
- Android自定义View实现流式布局(热门标签效果)
- JS学习45:howdo已支持浏览器端的异步流程控制
- 关于if(a)
- RabbitMq、ActiveMq、ZeroMq、kafka之间的比较,资料汇总
- 基数树实现位串的字典排序
- [Sqoop]Sqoop使用
- Android流式标签布局,自定义标签控件tagView
- nginx查看日志访问IP最高的20个IP记录
- 异常
- JS学习46:不同script导致IE8以下全局变量引用断开
- 《疯狂Android讲义》 -- Android 动画系列之逐帧(Frame)动画
- Android JNI判断当前是否附着于Main Thread
- Ubuntu系统下安装Apache
- hibernate进阶 (7)多对一映射
- 第十周项目1-二叉树算法库