編輯:關於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()); }
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(); } }); } }
先在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; } }); }
到此關於Android的流式布局的例子就寫的差不多了,我其中也借鑒了其他大神的文章。共勉,我也要下班了,飯還沒吃,餓死了。
ctrl+shift+N 查找文件,以懸浮窗口的形式搜索 contrl+N 查找類,與ctrl+shift+N相似,但是只能查找類 ctrl + E 最近打開的文件,可
對於Web網頁來說,頁面的訪問、加載速度對於用戶體驗來說是很重要的,而如果把Android中的每個Activity都看成是一個頁面的話,Activity的啟動速度憑主觀的
Servlet在不實現SingleThreadModel的情況下運行時是以單個實例模式,如下圖,這種情況下,Wrapper容器只會通過反射實例化一個Servlet對象,對
最近我獨立開發的項目《全醫會》已經在內測當中了,很快將會上架到各大應用市場。之前開發的幾個項目都因為一些原因沒有上架還是比較遺憾的。所以,最近我心情格外的好。 今天在做