今天我們來做一個android上的標簽云效果, 雖然還不是很完美,但是已經足夠可以展現標簽云的效果了,首先來看看效果吧。
額,錄屏只能錄到這個份上了,湊活著看吧。今天我們就來實現一下這個效果, 這次我選擇直接繼承view來, 什么? 這樣的效果不是SurfaceView擅長的嗎? 為什么要view,其實都可以了, 我選擇view,是因為:額,我對SurfaceView還不是很熟悉。
廢話少說, 下面開始上代碼
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
|
private static final int DIRECTION_LEFT = 0 ; // 向左 private static final int DIRECTION_RIGHT = 1 ; // 向右 private static final int DIRECITON_TOP = 2 ; // 向上 private static final int DIRECTION_BOTTOM = 3 ; // 向下 private boolean isStatic; // 是否靜止, 默認false, 可用干xml : label:is_static="false" private int [][] mLocations; // 每個label的位置 x/y private int [][] mDirections; // 每個label的方向 x/y private int [][] mSpeeds; // 每個label的x/y速度 x/y private int [][] mTextWidthAndHeight; // 每個labeltext的大小 width/height private String[] mLabels; // 設置的labels private int [] mFontSizes; // 每個label的字體大小 // 默認配色方案 private int [] mColorSchema = { 0XFFFF0000 , 0XFF00FF00 , 0XFF0000FF , 0XFFCCCCCC , 0XFFFFFFFF }; private int mTouchSlop; // 最小touch private int mDownX = - 1 ; private int mDownY = - 1 ; private int mDownIndex = - 1 ; // 點擊的index private Paint mPaint; private Thread mThread; private OnItemClickListener mListener; // item點擊事件 public LabelView(Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public LabelView(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LabelView, defStyleAttr, 0 ); isStatic = ta.getBoolean(R.styleable.LabelView_is_static, false ); ta.recycle(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mPaint = new Paint(); mPaint.setAntiAlias( true ); } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom) { super .onLayout(changed, left, top, right, bottom); init(); } @Override protected void onDraw(Canvas canvas) { if (!hasContents()) { return ; } for ( int i = 0 ; i < mLabels.length; i++) { mPaint.setTextSize(mFontSizes[i]); if (i < mColorSchema.length) mPaint.setColor(mColorSchema[i]); else mPaint.setColor(mColorSchema[i-mColorSchema.length]); canvas.drawText(mLabels[i], mLocations[i][ 0 ], mLocations[i][ 1 ], mPaint); } } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = ( int ) ev.getX(); mDownY = ( int ) ev.getY(); mDownIndex = getClickIndex(); break ; case MotionEvent.ACTION_UP: int nowX = ( int ) ev.getX(); int nowY = ( int ) ev.getY(); if (nowX - mDownX < mTouchSlop && nowY - mDownY < mTouchSlop && mDownIndex != - 1 && mListener != null ) { mListener.onItemClick(mDownIndex, mLabels[mDownIndex]); } mDownX = mDownY = mDownIndex = - 1 ; break ; } return true ; } /** * 獲取當前點擊的label的位置 * @return label的位置,沒有點中返回-1 */ private int getClickIndex() { Rect downRect = new Rect(); Rect locationRect = new Rect(); for ( int i= 0 ;i<mLocations.length;i++) { downRect.set(mDownX - mTextWidthAndHeight[i][ 0 ], mDownY - mTextWidthAndHeight[i][ 1 ], mDownX + mTextWidthAndHeight[i][ 0 ], mDownY + mTextWidthAndHeight[i][ 1 ]); locationRect.set(mLocations[i][ 0 ], mLocations[i][ 1 ], mLocations[i][ 0 ] + mTextWidthAndHeight[i][ 0 ], mLocations[i][ 1 ] + mTextWidthAndHeight[i][ 1 ]); if (locationRect.intersect(downRect)) { return i; } } return - 1 ; } /** * 開啟子線程不斷刷新位置并postInvalidate */ private void run() { if (mThread != null && mThread.isAlive()) { return ; } mThread = new Thread(mStartRunning); mThread.start(); } private Runnable mStartRunning = new Runnable() { @Override public void run() { for (;;) { SystemClock.sleep( 100 ); for ( int i = 0 ; i < mLabels.length; i++) { if (mLocations[i][ 0 ] <= getPaddingLeft()) { mDirections[i][ 0 ] = DIRECTION_RIGHT; } if (mLocations[i][ 0 ] >= getMeasuredWidth() - getPaddingRight() - mTextWidthAndHeight[i][ 0 ]) { mDirections[i][ 0 ] = DIRECTION_LEFT; } if (mLocations[i][ 1 ] <= getPaddingTop() + mTextWidthAndHeight[i][ 1 ]) { mDirections[i][ 1 ] = DIRECTION_BOTTOM; } if (mLocations[i][ 1 ] >= getMeasuredHeight() - getPaddingBottom()) { mDirections[i][ 1 ] = DIRECITON_TOP; } int xSpeed = 1 ; int ySpeed = 2 ; if (i < mSpeeds.length) { xSpeed = mSpeeds[i][ 0 ]; ySpeed = mSpeeds[i][ 1 ]; } else { xSpeed = mSpeeds[i-mSpeeds.length][ 0 ]; ySpeed = mSpeeds[i-mSpeeds.length][ 1 ]; } mLocations[i][ 0 ] += mDirections[i][ 0 ] == DIRECTION_RIGHT ? xSpeed : -xSpeed; mLocations[i][ 1 ] += mDirections[i][ 1 ] == DIRECTION_BOTTOM ? ySpeed : -ySpeed; } postInvalidate(); } } }; /** * 初始化位置、方向、label寬高 * 并開啟線程 */ private void init() { if (!hasContents()) { return ; } int minX = getPaddingLeft(); int minY = getPaddingTop(); int maxX = getMeasuredWidth() - getPaddingRight(); int maxY = getMeasuredHeight() - getPaddingBottom(); Rect textBounds = new Rect(); for ( int i = 0 ; i < mLabels.length; i++) { int [] location = new int [ 2 ]; location[ 0 ] = minX + ( int ) (Math.random() * maxX); location[ 1 ] = minY + ( int ) (Math.random() * maxY); mLocations[i] = location; mFontSizes[i] = 15 + ( int ) (Math.random() * 30 ); mDirections[i][ 0 ] = Math.random() > 0.5 ? DIRECTION_RIGHT : DIRECTION_LEFT; mDirections[i][ 1 ] = Math.random() > 0.5 ? DIRECTION_BOTTOM : DIRECITON_TOP; mPaint.setTextSize(mFontSizes[i]); mPaint.getTextBounds(mLabels[i], 0 , mLabels[i].length(), textBounds); mTextWidthAndHeight[i][ 0 ] = textBounds.width(); mTextWidthAndHeight[i][ 1 ] = textBounds.height(); } if (!isStatic) run(); } /** * 是否設置label * @return true or false */ private boolean hasContents() { return mLabels != null && mLabels.length > 0 ; } /** * 設置labels * @see setLabels(String[] labels) * @param labels */ public void setLabels(List<String> labels) { setLabels((String[]) labels.toArray()); } /** * 設置labels * @param labels */ public void setLabels(String[] labels) { mLabels = labels; mLocations = new int [labels.length][ 2 ]; mFontSizes = new int [labels.length]; mDirections = new int [labels.length][ 2 ]; mTextWidthAndHeight = new int [labels.length][ 2 ]; mSpeeds = new int [labels.length][ 2 ]; for ( int speed[] : mSpeeds) { speed[ 0 ] = speed[ 1 ] = 1 ; } requestLayout(); } /** * 設置配色方案 * @param colorSchema */ public void setColorSchema( int [] colorSchema) { mColorSchema = colorSchema; } /** * 設置每個item的x/y速度 * <p> * speeds.length > labels.length 忽略多余的 * <p> * speeds.length < labels.length 將重復使用 * * @param speeds */ public void setSpeeds( int [][] speeds) { mSpeeds = speeds; } /** * 設置item點擊的監聽事件 * @param l */ public void setOnItemClickListener(OnItemClickListener l) { getParent().requestDisallowInterceptTouchEvent( true ); mListener = l; } /** * item的點擊監聽事件 */ public interface OnItemClickListener { public void onItemClick( int index, String label); } } |
上來先弄了4個常量上去,干嘛用的呢? 是要判斷每個item的方向的,因為當達到某個邊界的時候,item要向相反的方向移動。
第二個構造方法中, 獲取了一個自定義屬性,還有就是初始化的Paint。
繼續看onLayout, 其實onLayout我們什么都沒干,只是調用了init方法, 來看看init方法。
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
|
/** * 初始化位置、方向、label寬高 * 并開啟線程 */ private void init() { if (!hasContents()) { return ; } int minX = getPaddingLeft(); int minY = getPaddingTop(); int maxX = getMeasuredWidth() - getPaddingRight(); int maxY = getMeasuredHeight() - getPaddingBottom(); Rect textBounds = new Rect(); for ( int i = 0 ; i < mLabels.length; i++) { int [] location = new int [ 2 ]; location[ 0 ] = minX + ( int ) (Math.random() * maxX); location[ 1 ] = minY + ( int ) (Math.random() * maxY); mLocations[i] = location; mFontSizes[i] = 15 + ( int ) (Math.random() * 30 ); mDirections[i][ 0 ] = Math.random() > 0.5 ? DIRECTION_RIGHT : DIRECTION_LEFT; mDirections[i][ 1 ] = Math.random() > 0.5 ? DIRECTION_BOTTOM : DIRECITON_TOP; mPaint.setTextSize(mFontSizes[i]); mPaint.getTextBounds(mLabels[i], 0 , mLabels[i].length(), textBounds); mTextWidthAndHeight[i][ 0 ] = textBounds.width(); mTextWidthAndHeight[i][ 1 ] = textBounds.height(); } if (!isStatic) run(); } |
init方法中,上來先判斷一下,是否設置了標簽,如果沒有設置直接返回,省得事多。
10~13行,目的就是獲取item在該view中移動的上下左右邊界,畢竟item還是要在整個view中移動的嘛,不能超出了view的邊界。
17行,開始一個for循環,去遍歷所有的標簽。
18~20行,是隨機初始化一個位置,所以,我們的標簽每次出現的位置都是隨機的,并沒有什么規律,但接下來的移動是有規律的,總不能到處亂蹦吧。
接著,22行,保存了這個位置,因為我們下面要不斷的去修改這個位置。
23行,隨機了一個字體大小,24、25行,隨機了該標簽x/y初始的方向。
27行,去設置了當前標簽的字體大小,28行,是獲取標簽的寬度和高度,并在下面保存在了一個二維數組中,為什么是二維數組,我們有多個標簽嘛, 每個標簽都要保存它的寬度和高度。
最后,如果我們沒有顯示的聲明labelview是靜止的,則去調用run方法。
繼續跟進代碼,看看run方法的內臟。
1
2
3
4
5
6
7
8
9
10
11
|
/** * 開啟子線程不斷刷新位置并postInvalidate */ private void run() { if (mThread != null && mThread.isAlive()) { return ; } mThread = new Thread(mStartRunning); mThread.start(); } |
5~7行,如果線程已經開啟,直接return 防止多個線程共存,這樣造成的后果就是標簽越來越快。
9、10行,去啟動一個線程,并有一個mStartRunning的Runnable參數。
那么我們繼續來看看這個Runnable。
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
|
private Runnable mStartRunning = new Runnable() { @Override public void run() { for (;;) { SystemClock.sleep( 100 ); for ( int i = 0 ; i < mLabels.length; i++) { if (mLocations[i][ 0 ] <= getPaddingLeft()) { mDirections[i][ 0 ] = DIRECTION_RIGHT; } if (mLocations[i][ 0 ] >= getMeasuredWidth() - getPaddingRight() - mTextWidthAndHeight[i][ 0 ]) { mDirections[i][ 0 ] = DIRECTION_LEFT; } if (mLocations[i][ 1 ] <= getPaddingTop() + mTextWidthAndHeight[i][ 1 ]) { mDirections[i][ 1 ] = DIRECTION_BOTTOM; } if (mLocations[i][ 1 ] >= getMeasuredHeight() - getPaddingBottom()) { mDirections[i][ 1 ] = DIRECITON_TOP; } int xSpeed = 1 ; int ySpeed = 2 ; if (i < mSpeeds.length) { xSpeed = mSpeeds[i][ 0 ]; ySpeed = mSpeeds[i][ 1 ]; } else { xSpeed = mSpeeds[i-mSpeeds.length][ 0 ]; ySpeed = mSpeeds[i-mSpeeds.length][ 1 ]; } mLocations[i][ 0 ] += mDirections[i][ 0 ] == DIRECTION_RIGHT ? xSpeed : -xSpeed; mLocations[i][ 1 ] += mDirections[i][ 1 ] == DIRECTION_BOTTOM ? ySpeed : -ySpeed; } postInvalidate(); } } }; |
這個Runnable其實才是標簽云實現的關鍵,我們就是在這個線程中去修改每個標簽的位置,并通知view去重繪的。
而且可以看到,在run中是一個死循環,這樣我們的標簽才能無休止的移動,接下來就是讓線程去休息100ms,總不能一個勁的去移動吧,速度太快了也不好,也要考慮性能問題。
接下來第7行,去遍歷所有的標簽,8~23行,通過判斷當前的位置是不是達到了某個邊界,如果到了,則修改方向為相反的方向,例如現在到了view的最上面,那接下來,這個標簽就得往下移動了。
25、26行,默認了x/y的速度,為什么是說默認了呢, 因為每個標簽的x/y速度我們都可以通過方法去設置。
接下來28~34行,做了一個判斷,大體意思就是:如果設置的那些速度總數大于當前標簽在標簽s中的位置,則去找對應位置的速度,否則,重新從前面獲取速度。
36、37行就是根據x/y上的方向去修改當前標簽的坐標了。
最后,調用了postInvalidate(),通知view去刷新界面,這里是用的postInvalidate()因為我們是在線程中調用的,切記。
postInvalidate()后,肯定就要走onDraw()去繪制這些標簽了,那么我們就來看看onDraw吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Override protected void onDraw(Canvas canvas) { if (!hasContents()) { return ; } for ( int i = 0 ; i < mLabels.length; i++) { mPaint.setTextSize(mFontSizes[i]); if (i < mColorSchema.length) mPaint.setColor(mColorSchema[i]); else mPaint.setColor(mColorSchema[i-mColorSchema.length]); canvas.drawText(mLabels[i], mLocations[i][ 0 ], mLocations[i][ 1 ], mPaint); } } |
上來還是判斷了一下,如果沒有設置標簽,直接返回。 如果有標簽,那么去遍歷所有標簽,并設置對應的字體大小,還記得嗎? 我們在初始化的時候隨機了每個標簽的字體大小,接下來去設置該標簽的顏色,一個if else 原理和設置速度那個是一樣的,最關鍵的就是下面,調用了canvas.drawText()將該標簽畫到屏幕上,mLocations中我們是保存了每個標簽的位置,而且是在線程中不斷的去修改這個位置的。
到這里,其實我們的LabelView就能動起來了,不過那幾個設置標簽,速度,顏色的方法還有說。其實很簡單,來看一下吧。
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
|
/** * 設置labels * @see setLabels(String[] labels) * @param labels */ public void setLabels(List<String> labels) { setLabels((String[]) labels.toArray()); } /** * 設置labels * @param labels */ public void setLabels(String[] labels) { mLabels = labels; mLocations = new int [labels.length][ 2 ]; mFontSizes = new int [labels.length]; mDirections = new int [labels.length][ 2 ]; mTextWidthAndHeight = new int [labels.length][ 2 ]; mSpeeds = new int [labels.length][ 2 ]; for ( int speed[] : mSpeeds) { speed[ 0 ] = speed[ 1 ] = 1 ; } requestLayout(); } /** * 設置配色方案 * @param colorSchema */ public void setColorSchema( int [] colorSchema) { mColorSchema = colorSchema; } /** * 設置每個item的x/y速度 * <p> * speeds.length > labels.length 忽略多余的 * <p> * speeds.length < labels.length 將重復使用 * * @param speeds */ public void setSpeeds( int [][] speeds) { mSpeeds = speeds; } |
這幾個蛋疼的方法中,唯一可說的就是setLabels(String[] labels)了,因為在這個方法中還做了點工作。 仔細觀察,這方法除了設置了標簽s外,其他的就是初始化了幾個數組,都表示什么,相信都應該很清楚了,還有就是在這里我們初始化了默認速度為1。
剛上來做演示的時候,LabelView還能item點擊,這是怎么做到的呢? 普通的onClick肯定是不行的,因為我們并不知道點擊的x/y坐標,所以只能通過onTouchEvent入手了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = ( int ) ev.getX(); mDownY = ( int ) ev.getY(); mDownIndex = getClickIndex(); break ; case MotionEvent.ACTION_UP: int nowX = ( int ) ev.getX(); int nowY = ( int ) ev.getY(); if (nowX - mDownX < mTouchSlop && nowY - mDownY < mTouchSlop && mDownIndex != - 1 && mListener != null ) { mListener.onItemClick(mDownIndex, mLabels[mDownIndex]); } mDownX = mDownY = mDownIndex = - 1 ; break ; } return true ; } |
在onTouch中我們只關心了down和up事件,因為一次點擊就是down和up的組合嘛。
在down中,我們獲取了當前事件發生的x/y坐標,并且獲取了當前點擊的item,當前是通過getClickIndex()方法去獲取的,這個方法稍候說;再來看看up,在up中,我們通過當前的x/y和在down時的x/y對比,如果這兩點的距離小于系統認為的最小滑動距離,才能說明點擊有效,如果你down了以后,拉了一個長線,再up,那肯定不是一次有效的點擊,當然點擊有效了還不能說明一切,只有命中標簽了才行,所以還去判斷了mDownIndex是否為一個有效的值,然后如果設置了ItemClick,就去回調它。
那mDownIndex到底是怎么獲取的呢? 我們來getClickIndex()一探究竟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/** * 獲取當前點擊的label的位置 * @return label的位置,沒有點中返回-1 */ private int getClickIndex() { Rect downRect = new Rect(); Rect locationRect = new Rect(); for ( int i= 0 ;i<mLocations.length;i++) { downRect.set(mDownX - mTextWidthAndHeight[i][ 0 ], mDownY - mTextWidthAndHeight[i][ 1 ], mDownX + mTextWidthAndHeight[i][ 0 ], mDownY + mTextWidthAndHeight[i][ 1 ]); locationRect.set(mLocations[i][ 0 ], mLocations[i][ 1 ], mLocations[i][ 0 ] + mTextWidthAndHeight[i][ 0 ], mLocations[i][ 1 ] + mTextWidthAndHeight[i][ 1 ]); if (locationRect.intersect(downRect)) { return i; } } return - 1 ; } |
首先定義了兩個Rect,一個是點擊的rect,另一個是標簽的rect,然后去遍歷保存的最新的每個標簽的位置,在循環中,我們通過Rect.set()方法分別設置了down的矩形的上下左右和當前標簽的上下左右,然后通過Rect.intersect()方法去判斷這兩個矩形是否有交集,有交集就證明點擊到了該標簽,直接返回該標簽在標簽s中的位置,如果都沒有返回-1表示你丫亂點!
好了,到這里,整個LabelView就弄好了,趕緊去下載源碼體驗一把吧,當然還不算很完美,完美的解決方案等用到它的時候再去解決,嘿嘿,反正我們已經有一個思路了。
哦,對了,還沒給出源碼的下載地址,看這里
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/qibin0506/article/details/43739723