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

Android流式標簽布局,自定義標簽控件tagView

編輯:關於Android編程

我們在一些項目中會用到自定義流式布局,我個人覺得流式布局將呆板的布局錯綜排列,來提升用戶體驗度.(還可以不辜負美工妹子們的期望,人家畢竟也辛辛苦苦設計半天)。今天終於有時間來做做了。寫的不好,很多地方值得改進望大家一起交流。

這是效果圖:

\

 

實現基本功能:

首先來說明幾點:

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() {
			@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;
			}
		});
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		_adjustText();
	}

	@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) {
		// 繪制背景
		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;

	@Override
	protected 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();

			@Override
			public void onClick(View arg0) {
				mTagGroup.addTag(mTagWords[random.nextInt(mTagWords.length)]);
			}
		});
		mBtnClean.setOnClickListener(new View.OnClickListener() {

			@Override
			public 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() {

			@Override
			public void onTagLongClick(String text) {
				Log.w("MainActivity", text);
				Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT)
						.show();
			}

			@Override
			public 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的流式布局的例子就寫的差不多了,我其中也借鑒了其他大神的文章。共勉,我也要下班了,飯還沒吃,餓死了。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved