詞云(WordCloud)是分析數(shù)據(jù)時(shí)一項(xiàng)有趣的展示方式, 它將數(shù)據(jù)中的關(guān)鍵詞按權(quán)重設(shè)置不同的大小, 放置成一定的形狀(比如圓形). 它包括關(guān)鍵詞的統(tǒng)計(jì)提取和放置, 這里在安卓端實(shí)現(xiàn)一個(gè)放置詞云的View.
Google一下word cloud algorithm
詞云算法, 這里有介紹
https://stackoverflow.com/questions/342687/algorithm-to-implement-a-word-cloud-like-wordle
主要思想是:
- 先放權(quán)重大的詞
- 放好后就不動(dòng)
- 放置時(shí)如果跟已放好的詞重疊了, 就按照螺旋線向外移動(dòng)到下一個(gè)位置
簡(jiǎn)單實(shí)現(xiàn)
使用自定義ViewGroup實(shí)現(xiàn), 每個(gè)關(guān)鍵詞用一個(gè)TextView表示, 放置到ViewGroup中, 這樣的好處是方便處理單個(gè)詞的樣式和事件. 主要就是重寫onLayout()
方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int n = getChildCount();
for(int i = 0; i < n; i++) {
View v = getChildAt(i);
if(placed.contains(v)) {
continue;
}
int w = v.getMeasuredWidth();
int h = v.getMeasuredHeight();
int pivotX = getWidth() / 3 + random.nextInt(getWidth() / 3);
int pivotY = getHeight() / 3 + random.nextInt(getHeight() / 3);
List<Point> spiral = generateSpiral();
for(Point p : spiral) {
pivotX += p.x;
pivotY += p.y;
Log.d("chao", "place " + pivotX + "," + pivotY);
Rect r1 = getVisualRect(pivotX, pivotY, w, h, v.getRotation());
boolean isOverlap = false;
for(View pv : placed) {
Rect r2 = getVisualRect(pv);
if(isOverlap(r1, r2)) {
isOverlap = true;
break;
}
}
if(isOverlap) {
} else {
Log.d("chao", "placed");
Rect r = getRect(pivotX, pivotY, w, h);
v.layout(r.left, r.top, r.right, r.bottom);
break;
}
}
placed.add(v);
}
}
為了同時(shí)有水平擺放和垂直擺放, 添加TextView時(shí)隨機(jī)給90度或270度的旋轉(zhuǎn). 注意雖然TextView旋轉(zhuǎn)了, 但是它的位置屬性還是旋轉(zhuǎn)前的, 因此在檢測(cè)重疊前要轉(zhuǎn)換成旋轉(zhuǎn)后的.
float[] rotates = {
0f,90f,270f
};
public void addTextView(String word, int weight) {
TextView tv = new TextView(getContext());
tv.setText(word);
tv.setTextSize(weight);
tv.setRotation(rotates[random.nextInt(rotates.length)]);
tv.setOnClickListener(this);
addView(tv, params);
}
檢測(cè)兩個(gè)矩形是否重疊的算法
public static boolean isOverlap(Rect r1, Rect r2) {
return r1.right >= r2.left && r2.right >= r1.left
&& r1.bottom >= r2.top && r2.bottom >= r1.top;
}
這樣就實(shí)現(xiàn)了, 看一下效果不是很好, 這是因?yàn)橛玫氖?直角螺旋線算法", 即按照左上右下的順序移動(dòng)坐標(biāo), 距離逐漸增大. 要實(shí)現(xiàn)放置成圓形的效果, 需要加入真正的螺旋線算法.
一般數(shù)學(xué)中沒講過螺旋線, 網(wǎng)上找一下, 寫一個(gè)生成螺旋線點(diǎn)的算法, 用自定義View驗(yàn)證
public class SpiralView extends View {
Paint paint;
List<Point> points;
public SpiralView(Context context) {
this(context, null);
}
public SpiralView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SpiralView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
points = generateSpiral();
}
@Override
protected void onDraw(Canvas canvas) {
int cx = getWidth() / 2;
int cy = getHeight() / 2;
for(Point p : points) {
canvas.drawCircle(cx + p.x, cy + p.y, 5, paint);
}
}
private List<Point> generateSpiral() {
List<Point> res = new ArrayList<>();
int A = 10;
int w = 1;
double sita = Math.PI;
for(double t = 0; t < 10 * Math.PI; t+=0.1) {
int x = Double.valueOf(A * Math.cos(w * t + sita)).intValue();
int y = Double.valueOf(A * Math.sin(w * t + sita)).intValue();
A += 1;
res.add(new Point(x, y));
Log.e("chao", x + ", " + y);
}
return res;
}
}
在把這個(gè)算法加入到onLayout()
方法中就大功告成了
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int n = getChildCount();
for(int i = 0; i < n; i++) {
View v = getChildAt(i);
if(placed.contains(v)) {
continue;
}
int w = v.getMeasuredWidth();
int h = v.getMeasuredHeight();
int pivotX = getWidth() / 3 + random.nextInt(getWidth() / 3);
int pivotY = getHeight() / 3 + random.nextInt(getHeight() / 3);
List<Point> spiral = generateSpiral();
for(Point p : spiral) {
pivotX += p.x;
pivotY += p.y;
Log.d("chao", "place " + pivotX + "," + pivotY);
Rect r1 = getVisualRect(pivotX, pivotY, w, h, v.getRotation());
boolean isOverlap = false;
for(View pv : placed) {
Rect r2 = getVisualRect(pv);
if(isOverlap(r1, r2)) {
isOverlap = true;
break;
}
}
if(isOverlap) {
} else {
Log.d("chao", "placed");
Rect r = getRect(pivotX, pivotY, w, h);
v.layout(r.left, r.top, r.right, r.bottom);
break;
}
}
placed.add(v);
}
}