效果圖:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiYWan5CM2pGM4gTOwkjN1IzMx81MwMzNvwFNvwVMwITMwIzLcRnbl1GajFGd0F2LcRXZu5ibkN3YukGavw1LcpDc0RHaiojIsJye.gif)
實作該效果需要解決以下五點:
1.布局的選用。
2.确定動畫區域,即布局的寬高。
3.對關鍵字坐标的随機配置設定。
4.對随機配置設定的坐标進行向中心靠攏。
5.動畫的實作。
本文内容歸csdn部落格部落客sodino 所有
轉載請注明出處:http://blog.csdn.net/sodino/article/details/7176796
下面各個擊破:
在五種常用布局中,可實作此效果的有absolutelayout、framelayout、relativelayout三種。一開始我選用的absolutelayout,運作結果出來後,發現absolutelayout下的textview一旦超出其顯示範圍,超出的範圍将無法顯示,而餘下的兩種布局,其超出的範圍會自動換行顯示出來(textview長度超出父元件顯示範圍可在代碼中避免,此處僅是舉例,說明absolutelayout的先天不足)。另,官方已不再推薦使用absolutelayout,是以本處憑個人喜好我選用framelayout。
framelayout如何實作absolutelayout對其子元件進行定點放置呢?答案在framelayout.layoutparams上。該類有相關屬性為leftmargin及topmargin。要将子元件左上角定點放置在其父元件中的(x,y)處,僅需對leftmargin指派為x,對topmargin指派為y即可。
在對顯示關鍵字textview進行配置設定坐标之前,應該要先知道父元件的寬高各有多少可供随機配置設定。
擷取寬高使用到ongloballayoutlistener。本例中keywordsflow繼承自framelayout,同時也實作了ongloballayoutlistener接口,在其初始化方法init()中設定了監聽getviewtreeobserver().addongloballayoutlistener(this);
當監聽事件被觸發時,即可擷取而已的寬高。
[java]
view plaincopyprint?
public void ongloballayout() {
int tmpw = getwidth();
int tmph = getheight();
if (width != tmpw || height != tmph) {
width = tmpw;
height = tmph;
show();
}
textview坐标的随機是否到位配置設定決定着整體效果的好壞。
本例設定關鍵字最多為10個,在布局的x y軸上各自進行10等分。每個關鍵字依照其添加順序随機各自在x軸和y軸上選擇等分後的10點中的某個點為margin的值。此值為糙值,需要對x軸進行越界修正,對y軸進行向中心靠攏修正。對x軸坐标的修正為如下:
// 擷取文本長度
paint paint = txt.getpaint();
int strwidth = (int) math.ceil(paint.measuretext(keyword));
xy[idx_txt_length] = strwidth;
// 第一次修正:修正x坐标
if (xy[idx_x] + strwidth > width - (xitem >>
1)) {
int basex = width - strwidth;
// 減少文本右邊緣一樣的機率
xy[idx_x] = basex - xitem + random.nextint(xitem >>
1);
} else if (xy[idx_x] ==
0) {
// 減少文本左邊緣一樣的機率
xy[idx_x] = math.max(random.nextint(xitem), xitem / 3);
if (xy[idx_x] + strwidth > width - (xitem >> 1)) {
xy[idx_x] = basex - xitem + random.nextint(xitem >> 1);
} else if (xy[idx_x] == 0) {
此操作将修正y軸坐标。
由于随機配置設定中,可能出現某個關鍵字在朝中心點方向上的空間中再沒有其它關鍵字了,此時該關鍵字在y軸上應該朝中心點靠攏。實作代碼如下:
// 第二次修正:修正y坐标
int ydistance = ixy[idx_y] - ycenter;
// 對于最靠近中心點的,其值不會大于yitem<br/>
// 對于可以一路下降到中心點的,則該值也是其應調整的大小<br/>
int ymove = math.abs(ydistance);
inner: for (int k = i -
1; k >= 0; k--) {
int[] kxy = (int[]) listtxt.get(k).gettag();
int startx = kxy[idx_x];
int endx = startx + kxy[idx_txt_length];
// y軸以中心點為分隔線,在同一側
if (ydistance * (kxy[idx_y] - ycenter) >
// log.d("android_lab", "compare:" +
// listtxt.get(k).gettext());
if (isxmixed(startx, endx, ixy[idx_x], ixy[idx_x] + ixy[idx_txt_length])) {
int tmpmove = math.abs(ixy[idx_y] - kxy[idx_y]);
if (tmpmove > yitem) {
ymove = tmpmove;
} else if (ymove >
// 取消預設值。
ymove = 0;
// log.d("android_lab", "break");
break inner;
// log.d("android_lab", txt.gettext() + " ymove=" + ymove);
if (ymove > yitem) {
int maxmove = ymove - yitem;
int randommove = random.nextint(maxmove);
int realmove = math.max(randommove, maxmove >>
1) * ydistance / math.abs(ydistance);
ixy[idx_y] = ixy[idx_y] - realmove;
ixy[idx_dis_y] = math.abs(ixy[idx_y] - ycenter);
// 已經調整過前i個需要再次排序
sortxylist(listtxt, i + 1);
inner: for (int k = i - 1; k >= 0; k--) {
if (ydistance * (kxy[idx_y] - ycenter) > 0) {
} else if (ymove > 0) {
int realmove = math.max(randommove, maxmove >> 1) * ydistance / math.abs(ydistance);
每個textview的動畫都有包括三部分:伸縮動畫scaleanimation、透明度漸變動畫alphaanimation及位移動畫translateanimation。以上三個動畫中除了位移動畫是獨立的,其它兩種動畫都是可以共用的。三種動畫的組合使用animationset拼裝在一起同時作用在textview上。動畫的實作如下:
public animationset getanimationset(int[] xy,
int xcenter, int ycenter,
int type) {
animationset animset = new animationset(true);
animset.setinterpolator(interpolator);
if (type == outside_to_location) {
animset.addanimation(animalpha2opaque);
animset.addanimation(animscalelarge2normal);
translateanimation translate = new translateanimation(
(xy[idx_x] + (xy[idx_txt_length] >> 1) - xcenter) <<
1, 0, (xy[idx_y] - ycenter) <<
1, 0);
animset.addanimation(translate);
} else if (type == location_to_outside) {
animset.addanimation(animalpha2transparent);
animset.addanimation(animscalenormal2large);
translateanimation translate = new translateanimation(0,
} else if (type == location_to_center) {
animset.addanimation(animscalenormal2zero);
translateanimation translate = new translateanimation(0, (-xy[idx_x] + xcenter),
0, (-xy[idx_y] + ycenter));
} else if (type == center_to_location) {
animset.addanimation(animscalezero2normal);
translateanimation translate = new translateanimation((-xy[idx_x] + xcenter),
0, (-xy[idx_y] + ycenter), 0);
animset.setduration(animduration);
return animset;
public animationset getanimationset(int[] xy, int xcenter, int ycenter, int type) {
(xy[idx_x] + (xy[idx_txt_length] >> 1) - xcenter) << 1, 0, (xy[idx_y] - ycenter) << 1, 0);
(xy[idx_x] + (xy[idx_txt_length] >> 1) - xcenter) << 1, 0, (xy[idx_y] - ycenter) << 1);
translateanimation translate = new translateanimation(0, (-xy[idx_x] + xcenter), 0, (-xy[idx_y] + ycenter));
translateanimation translate = new translateanimation((-xy[idx_x] + xcenter), 0, (-xy[idx_y] + ycenter), 0);
最後有個小點需要再次提醒下,使用keywordsflow時,在eclipse開發環境下導出混淆包時,需要在proguard.cfg中添加:-keep public class * extends android.widget.framelayout
否則将會提示無法找到該類。
好了,文嗦嗦的東西到此結束,貼上java代碼如下,xml代碼請根據效果圖自己鼓搗吧。
actkeywordanim.java
package lab.sodino.searchkeywordanim;
import java.util.random;
import android.app.activity;
import android.content.intent;
import android.net.uri;
import android.os.bundle;
import android.view.view;
import android.view.view.onclicklistener;
import android.widget.button;
import android.widget.textview;
/**
* @author sodino e-mail:[email protected]
* @version time:2011-12-26 下午03:34:16
*/
public class actkeywordanim
extends activity implements onclicklistener {
public static
final string[] keywords = { "qq",
"sodino", "apk",
"gfw", "鉛筆",//
"短信", "桌面精靈",
"macbook pro", "平闆電腦", "雅詩蘭黛",//
"卡西歐 tr-100", "筆記本",
"spy mouse", "thinkpad e40",
"捕魚達人",//
"記憶體清理", "地圖",
"導航", "鬧鐘", "主題",//
"通訊錄", "播放器",
"csdn leak", "安全",
"3d",//
"美女", "天氣",
"4743g", "戴爾", "聯想",//
"歐朋", "浏覽器",
"憤怒的小鳥", "mmshow",
"網易公開課",//
"iciba", "油水關系",
"網遊app", "網際網路", "365月曆",//
"臉部識别", "chrome",
"safari", "中國版siri",
"a5處理器",//
"iphone4s", "摩托 me525",
"魅族 m9", "尼康 s2500" };
private keywordsflow keywordsflow;
private button btnin, btnout;
public void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.main);
btnin = (button) findviewbyid(r.id.btnin);
btnout = (button) findviewbyid(r.id.btnout);
btnin.setonclicklistener(this);
btnout.setonclicklistener(this);
keywordsflow = (keywordsflow) findviewbyid(r.id.keywordsflow);
keywordsflow.setduration(800l);
keywordsflow.setonitemclicklistener(this);
// 添加
feedkeywordsflow(keywordsflow, keywords);
keywordsflow.go2show(keywordsflow.animation_in);
private static
void feedkeywordsflow(keywordsflow keywordsflow, string[] arr) {
random random = new random();
for (int i =
0; i < keywordsflow.max; i++) {
int ran = random.nextint(arr.length);
string tmp = arr[ran];
keywordsflow.feedkeyword(tmp);
@override
public void onclick(view v) {
if (v == btnin) {
keywordsflow.rubkeywords();
// keywordsflow.ruballviews();
} else if (v == btnout) {
keywordsflow.go2show(keywordsflow.animation_out);
} else if (v
instanceof textview) {
string keyword = ((textview) v).gettext().tostring();
intent intent = new intent();
intent.setaction(intent.action_view);
intent.addcategory(intent.category_default);
intent.setdata(uri.parse("http://www.google.com.hk/#q=" + keyword));
startactivity(intent);
public class actkeywordanim extends activity implements onclicklistener {
public static final string[] keywords = { "qq", "sodino", "apk", "gfw", "鉛筆",//
"短信", "桌面精靈", "macbook pro", "平闆電腦", "雅詩蘭黛",//
"卡西歐 tr-100", "筆記本", "spy mouse", "thinkpad e40", "捕魚達人",//
"記憶體清理", "地圖", "導航", "鬧鐘", "主題",//
"通訊錄", "播放器", "csdn leak", "安全", "3d",//
"美女", "天氣", "4743g", "戴爾", "聯想",//
"歐朋", "浏覽器", "憤怒的小鳥", "mmshow", "網易公開課",//
"iciba", "油水關系", "網遊app", "網際網路", "365月曆",//
"臉部識别", "chrome", "safari", "中國版siri", "a5處理器",//
"iphone4s", "摩托 me525", "魅族 m9", "尼康 s2500" };
private static void feedkeywordsflow(keywordsflow keywordsflow, string[] arr) {
for (int i = 0; i < keywordsflow.max; i++) {
} else if (v instanceof textview) {
keywordsflow.java
import java.util.linkedlist;
import java.util.vector;
import android.content.context;
import android.graphics.paint;
import android.util.attributeset;
import android.util.typedvalue;
import android.view.gravity;
import android.view.viewtreeobserver.ongloballayoutlistener;
import android.view.animation.alphaanimation;
import android.view.animation.animation;
import android.view.animation.animation.animationlistener;
import android.view.animation.animationset;
import android.view.animation.animationutils;
import android.view.animation.interpolator;
import android.view.animation.scaleanimation;
import android.view.animation.translateanimation;
import android.widget.framelayout;
* 注意,出包時出混淆包,應在proguard.cfg中加入:<br/>
* -keep public class * extends android.widget.framelayout<br/>
*
public class keywordsflow
extends framelayout implements ongloballayoutlistener {
final int idx_x =
0;
final int idx_y = 1;
final int idx_txt_length =
2;
final int idx_dis_y = 3;
/** 由外至内的動畫。 */
final int animation_in =
1;
/** 由内至外的動畫。 */
final int animation_out =
/** 位移動畫類型:從外圍移動到坐标點。 */
final int outside_to_location =
/** 位移動畫類型:從坐标點移動到外圍。 */
final int location_to_outside =
/** 位移動畫類型:從中心點移動到坐标點。 */
final int center_to_location =
3;
/** 位移動畫類型:從坐标點移動到中心點。 */
final int location_to_center =
4;
final long anim_duration = 800l;
final int max = 10;
final int text_size_max =
25;
final int text_size_min =
15;
private onclicklistener itemclicklistener;
private static interpolator interpolator;
private static alphaanimation animalpha2opaque;
private static alphaanimation animalpha2transparent;
private static scaleanimation animscalelarge2normal, animscalenormal2large, animscalezero2normal,
animscalenormal2zero;
/** 存儲顯示的關鍵字。 */
private vector<string> veckeywords;
private int width, height;
* go2show()中被指派為true,辨別開發人員觸發其開始動畫顯示。<br/>
* 本辨別的作用是防止在填充keywrods未完成的過程中擷取到width和height後提前啟動動畫。<br/>
* 在show()方法中其被指派為false。<br/>
* 真正能夠動畫顯示的另一必要條件:width 和 height不為0。<br/>
private boolean enableshow;
private random random;
* @see animation_in
* @see animation_out
* @see outside_to_location
* @see location_to_outside
* @see location_to_center
* @see center_to_location
* */
private int txtanimintype, txtanimouttype;
/** 最近一次啟動動畫顯示的時間。 */
private long laststartanimationtime;
/** 動畫運作時間。 */
private long animduration;
public keywordsflow(context context, attributeset attrs,
int defstyle) {
super(context, attrs, defstyle);
init();
public keywordsflow(context context, attributeset attrs) {
super(context, attrs);
public keywordsflow(context context) {
super(context);
private void init() {
laststartanimationtime = 0l;
animduration = anim_duration;
random = new random();
veckeywords = new vector<string>(max);
getviewtreeobserver().addongloballayoutlistener(this);
interpolator = animationutils.loadinterpolator(getcontext(), android.r.anim.decelerate_interpolator);
animalpha2opaque = new alphaanimation(0.0f,
1.0f);
animalpha2transparent = new alphaanimation(1.0f,
0.0f);
animscalelarge2normal = new scaleanimation(2,
1, 2,
animscalenormal2large = new scaleanimation(1,
2, 1,
2);
animscalezero2normal = new scaleanimation(0,
1, 0,
animscalenormal2zero = new scaleanimation(1,
0, 1,
0);
public long getduration() {
return animduration;
public void setduration(long duration) {
animduration = duration;
public boolean feedkeyword(string keyword) {
boolean result =
false;
if (veckeywords.size() < max) {
result = veckeywords.add(keyword);
return result;
* 開始動畫顯示。<br/>
* 之前已經存在的textview将會顯示退出動畫。<br/>
* @return 正常顯示動畫傳回true;反之為false。傳回false原因如下:<br/>
* 1.時間上不允許,受laststartanimationtime的制約;<br/>
* 2.未擷取到width和height的值。<br/>
public boolean go2show(int animtype) {
if (system.currenttimemillis() - laststartanimationtime > animduration) {
enableshow = true;
if (animtype == animation_in) {
txtanimintype = outside_to_location;
txtanimouttype = location_to_center;
} else if (animtype == animation_out) {
txtanimintype = center_to_location;
txtanimouttype = location_to_outside;
disapper();
boolean result = show();
return false;
private void disapper() {
int size = getchildcount();
for (int i = size -
1; i >= 0; i--) {
final textview txt = (textview) getchildat(i);
if (txt.getvisibility() == view.gone) {
removeview(txt);
continue;
framelayout.layoutparams layparams = (layoutparams) txt.getlayoutparams();
// log.d("android_lab", txt.gettext() + " leftm=" +
// layparams.leftmargin + " topm=" + layparams.topmargin
// + " width=" + txt.getwidth());
int[] xy = new
int[] { layparams.leftmargin, layparams.topmargin, txt.getwidth() };
animationset animset = getanimationset(xy, (width >> 1), (height >>
1), txtanimouttype);
txt.startanimation(animset);
animset.setanimationlistener(new animationlistener() {
public void onanimationstart(animation animation) {
public void onanimationrepeat(animation animation) {
public void onanimationend(animation animation) {
txt.setonclicklistener(null);
txt.setclickable(false);
txt.setvisibility(view.gone);
});
private boolean show() {
if (width > 0 && height >
0 && veckeywords != null && veckeywords.size() >
0 && enableshow) {
enableshow = false;
laststartanimationtime = system.currenttimemillis();
int xcenter = width >> 1, ycenter = height >>
int size = veckeywords.size();
int xitem = width / size, yitem = height / size;
// log.d("android_lab", "--------------------------width=" + width +
// " height=" + height + " xitem=" + xitem
// + " yitem=" + yitem + "---------------------------");
linkedlist<integer> listx = new linkedlist<integer>(), listy =
new linkedlist<integer>();
0; i < size; i++) {
// 準備随機候選數,分别對應x/y軸位置
listx.add(i * xitem);
listy.add(i * yitem + (yitem >> 2));
// textview[] txtarr = new textview[size];
linkedlist<textview> listtxttop = new linkedlist<textview>();
linkedlist<textview> listtxtbottom = new linkedlist<textview>();
string keyword = veckeywords.get(i);
// 随機顔色
int rancolor = 0xff000000 | random.nextint(0x0077ffff);
// 随機位置,糙值
int xy[] = randomxy(random, listx, listy, xitem);
// 随機字型大小
int txtsize = text_size_min + random.nextint(text_size_max - text_size_min +
// 執行個體化textview
final textview txt = new textview(getcontext());
txt.setonclicklistener(itemclicklistener);
txt.settext(keyword);
txt.settextcolor(rancolor);
txt.settextsize(typedvalue.complex_unit_sp, txtsize);
txt.setshadowlayer(2,
2, 2, 0xff696969);
txt.setgravity(gravity.center);
xy[idx_dis_y] = math.abs(xy[idx_y] - ycenter);
txt.settag(xy);
if (xy[idx_y] > ycenter) {
listtxtbottom.add(txt);
} else {
listtxttop.add(txt);
attach2screen(listtxttop, xcenter, ycenter, yitem);
attach2screen(listtxtbottom, xcenter, ycenter, yitem);
return true;
/** 修正textview的y坐标将将其添加到容器上。 */
private void attach2screen(linkedlist<textview> listtxt,
int yitem) {
int size = listtxt.size();
sortxylist(listtxt, size);
textview txt = listtxt.get(i);
int[] ixy = (int[]) txt.gettag();
// log.d("android_lab", "fix[ " + txt.gettext() + " ] x:" +
// ixy[idx_x] + " y:" + ixy[idx_y] + " r2="
// + ixy[idx_dis_y]);
framelayout.layoutparams layparams = new framelayout.layoutparams(framelayout.layoutparams.wrap_content,
framelayout.layoutparams.wrap_content);
layparams.gravity = gravity.left | gravity.top;
layparams.leftmargin = ixy[idx_x];
layparams.topmargin = ixy[idx_y];
addview(txt, layparams);
// 動畫
animationset animset = getanimationset(ixy, xcenter, ycenter, txtanimintype);
* 根據與中心點的距離由近到遠進行冒泡排序。
* @param endidx
* 起始位置。
* @param txtarr
* 待排序的數組。
private void sortxylist(linkedlist<textview> listtxt,
int endidx) {
0; i < endidx; i++) {
for (int k = i +
1; k < endidx; k++) {
if (((int[]) listtxt.get(k).gettag())[idx_dis_y] < ((int[]) listtxt.get(i).gettag())[idx_dis_y]) {
textview itmp = listtxt.get(i);
textview ktmp = listtxt.get(k);
listtxt.set(i, ktmp);
listtxt.set(k, itmp);
/** a線段與b線段所代表的直線在x軸映射上是否有交集。 */
private boolean isxmixed(int starta,
int enda, int startb,
int endb) {
if (startb >= starta && startb <= enda) {
result = true;
} else if (endb >= starta && endb <= enda) {
} else if (starta >= startb && starta <= endb) {
} else if (enda >= startb && enda <= endb) {
private int[] randomxy(random ran, linkedlist<integer> listx, linkedlist<integer> listy,
int xitem) {
int[] arr = new
int[4];
arr[idx_x] = listx.remove(ran.nextint(listx.size()));
arr[idx_y] = listy.remove(ran.nextint(listy.size()));
return arr;
public vector<string> getkeywords() {
return veckeywords;
public void rubkeywords() {
veckeywords.clear();
/** 直接清除所有的textview。在清除之前不會顯示動畫。 */
public void ruballviews() {
removeallviews();
public void setonitemclicklistener(onclicklistener listener) {
itemclicklistener = listener;
// public void ondraw(canvas canvas) {
// super.ondraw(canvas);
// paint p = new paint();
// p.setcolor(color.black);
// canvas.drawcircle((width >> 1) - 2, (height >> 1) - 2, 4, p);
// p.setcolor(color.red);
// }
}