Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義流式標簽控件

Android自定義流式標簽控件

編輯:關於Android編程

最近總感覺寫博客的激情不高,不知道為啥。放上效果圖,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 tags = new ArrayList();

    //點擊事件的滑動距離阈值
    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);
    }
}

最後一段代碼一定是放使用方法,這是我的習慣。。


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的下載地址。哎,最近寫博客沒激情,閉關一段時間算了。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved