我们在一些项目中会用到自定义流式布局,我个人觉得流式布局将呆板的布局错综排列,来提升用户体验度.(还可以不辜负美工妹子们的期望,人家毕竟也辛辛苦苦设计半天)。今天终于有时间来做做了。写的不好,很多地方值得改进望大家一起交流。
这是效果图:
实现基本功能:
首先来说明几点:
1.标签视图TagView直接继承TextView,这样有几个好处:不用去重写onMeasure()接口, 不用自己绘制Text,对Text控制也方便;
2.标签布局TagGroup继承ViewGroup,需要重写onMeasure()和onLayout()方法来控制 TagView的显示;
1. 实现TagView:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208public 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 { } }
2.ViewGroup的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158public 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); } }
然后再看onLayout(),这个和onMeasure()其实挺像的,同样要计算上padding和间隙值,然后就是一个一个算出每个TagView的上下左右坐标,再调用TagView的layout()方法来设置到布局中的相应位置。
在写测试的时候我遇到一个问题:字符串过长的问题,因此需要裁剪。我的思路是这样:
首先太长的字符串截取前面的部分,并在后面补上3个“.”,就类似省略号;既然要裁剪就要知道最大可用的布局宽度,这个要从父布局中获取,需要TagGroup提供接口;最后计算的时候也要算上TagView的padding值,然后一个字符一个字符测量到符合要求;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/** * 调整内容,如果超出可显示的范围则做裁剪 */ 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60public 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回调方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public 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流式标签布局内容请搜索靠谱客的其他文章。
发表评论 取消回复