服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - android LabelView实现标签云效果

android LabelView实现标签云效果

2022-02-27 16:28亓斌 Android

这篇文章主要为大家详细介绍了android LabelView实现标签云效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

今天我们来做一个android上的标签云效果, 虽然还不是很完美,但是已经足够可以展现标签云的效果了,首先来看看效果吧。

android LabelView实现标签云效果

额,录屏只能录到这个份上了,凑活着看吧。今天我们就来实现一下这个效果, 这次我选择直接继承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
public class LabelView extends View {
 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

延伸 · 阅读

精彩推荐