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的流式布局的例子就写的差不多了,我其中也借鉴了其他大神的文章。共勉,我也要下班了,饭还没吃,饿死了。


     

2 0
原创粉丝点击