小米手機用戶可以看到,小米手機在應用卸載時會有一個粒子爆炸的特效效果,對此類動畫效果垂涎已久,奈何一直沒有機會用。正好最近項目里需要用到粒子爆炸的特效,于是便抽時間自己也試著仿寫了一個效果出來。
先看下效果:
How to use:
Step 1. Add the JitPack repository to your build file
Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Step 2.Add the dependency
dependencies {
compile 'com.github.zhaolei9527:Particle-master:v1.0.1'
}
代碼中這樣使用:
首先提供了響應式觸發的方式,首先進行埋雷,當控件被點擊時,觸發爆炸。是不是很刺激?
//目前提供了六種的粒子爆炸特效
explosionSite1 = new ExplosionSite(this, new BooleanFactory());
explosionSite2 = new ExplosionSite(this, new ExplodeParticleFactory());
explosionSite3 = new ExplosionSite(this, new FallingParticleFactory());
explosionSite4 = new ExplosionSite(this, new FlyawayFactory());
explosionSite5 = new ExplosionSite(this, new InnerFallingParticleFactory());
explosionSite6 = new ExplosionSite(this, new VerticalAscentFactory());
//爆炸激活方式一:將View或ViewGroup添加至雷管監聽,View被點擊時,觸發爆炸
explosionSite1.addListener(img_1);
explosionSite2.addListener(img_2);
explosionSite3.addListener(img_6);
explosionSite4.addListener(img_4);
explosionSite5.addListener(img_5);
explosionSite6.addListener(img_3);
其次針對一些情況,提供了另外一種直接的觸發方式。是不是更刺激?
//爆炸激活方式二:將View或ViewGroup直接點燃爆炸
explosionSite1.explode(img_1);
explosionSite2.explode(img_2);
explosionSite3.explode(img_3);
explosionSite4.explode(img_4);
explosionSite5.explode(img_5);
explosionSite6.explode(img_6);
看下原理:GitHub項目地址
網上實現類似相同效果的很多,基本規則也都差不多。
主要對象如下:
ExplosionSite:爆炸效果發生的場地,是一個View。當一個控件需要爆炸時,需要為控件生成一個ExplosionSite,這個ExplosionSite覆蓋整個屏幕,于是我們才能看到完整的爆炸效果,在ExplosionField的構造函數中,傳入不同的ParticleFactory,就可以生成不同的爆炸效果。
ExplosionAnimator:爆炸動畫,其實是一個計時器,繼承自ValueAnimator。0x400s內,完成爆炸動畫,每次計時,就更新所有粒子的運動狀態。draw()方法是它最重要的方法,也就是使所有粒子重繪自身,從而實現動畫效果。
ParticleFactory:是一個抽象類。用于產生粒子數組,不同的ParticleFactory可以產生不同類型的粒子數組。
Particle:抽象的粒子類。代表粒子本身,必須擁有的屬性包括,當前自己的cx,cy坐標和顏色color。必須實現兩個方法,draw()方法選擇怎么繪制自身(圓形還是方形等),caculate()計算當前時間,自己所處的位置。
實現原理:
1、獲取當前控件背景bitmap
例如,例子中使用的是imageview,對于控件,在Utils中提供有一個工具類,可以獲得其背景的Bitmap對象
public static Bitmap createBitmapFromView(View view) {
view.clearFocus();
Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1);
if (bitmap != null) {
synchronized (sCanvas) {
Canvas canvas = sCanvas;
canvas.setBitmap(bitmap);
view.draw(canvas);
canvas.setBitmap(null);
}
}
return bitmap;
}
public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {
try {
return Bitmap.createBitmap(width, height, config);
} catch (OutOfMemoryError e) {
e.printStackTrace();
if (retryCount > 0) {
System.gc();
return createBitmapSafely(width, height, config, retryCount - 1);
}
return null;
}
}
2、將View鏡像轉換成粒子對象
獲取Bitmap以后,我們交給工廠對象進行加工,根據Bitmap生產Particle數組。
我們知道Bitmap可以看成是一個像素矩陣,矩陣上面的點,就是一個個帶有顏色的像素,于是我們可以獲取到每個點的顏色和位置,組裝成一個對象Particle,這么一來,Particle就代表帶有顏色的點了。
public abstract class ParticleFactory {
public abstract Particle[][] generateParticles(Bitmap bitmap, Rect bound);
}
簡單來說都一樣,就是拿要爆炸的View制作一個鏡像出來然后返回。
然后來看一個粒子效果的具體實現。
public Particle[][] generateParticles(Bitmap bitmap, Rect bound) {
int w = bound.width();
int h = bound.height();
int partW_Count = w / PART_WH; //橫向個數
int partH_Count = h / PART_WH; //豎向個數
int bitmap_part_w = bitmap.getWidth() / partW_Count;
int bitmap_part_h = bitmap.getHeight() / partH_Count;
Particle[][] particles = new Particle[partH_Count][partW_Count];
for (int row = 0; row < partH_Count; row ++) { //行
for (int column = 0; column < partW_Count; column ++) { //列
//取得當前粒子所在位置的顏色
int color = bitmap.getPixel(column * bitmap_part_w, row * bitmap_part_h);
float x = bound.left + BooleanFactory.PART_WH * column;
float y = bound.top + BooleanFactory.PART_WH * row;
particles[row][column] = new BooleanParticle(color,x,y,bound);
}
}
return particles;
}
很簡單,其中Rect類型的bound,是代表原來View控件的寬高信息。
根據我們設定的每個粒子的大小,和控件的寬高,我們就可以計算出,有多少個粒子組成這個控件的背景。
我們取得每個粒子所在位置的顏色,位置,用于生產粒子,這就是BooleanParticle。
3、爆炸對象及主要流程
public class ExplosionSite extends View {
private static final String TAG = "ExplosionField";
private ArrayList<ExplosionAnimator> explosionAnimators;
private HashMap<View, ExplosionAnimator> explosionAnimatorsMap;
private OnClickListener onClickListener;
private ParticleFactory mParticleFactory;
public ExplosionSite(Context context, ParticleFactory particleFactory) {
super(context);
init(particleFactory);
}
public ExplosionSite(Context context, AttributeSet attrs, ParticleFactory particleFactory) {
super(context, attrs);
init(particleFactory);
}
private void init(ParticleFactory particleFactory) {
explosionAnimators = new ArrayList<ExplosionAnimator>();
explosionAnimatorsMap = new HashMap<View, ExplosionAnimator>();
mParticleFactory = particleFactory;
attach2Activity((Activity) getContext());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (ExplosionAnimator animator : explosionAnimators) {
animator.draw(canvas);
}
}
/**
* 爆破
*
* @param view 使得該view爆破
*/
public void explode(final View view) {
//防止重復點擊
if (explosionAnimatorsMap.get(view) != null && explosionAnimatorsMap.get(view).isStarted()) {
return;
}
if (view.getVisibility() != View.VISIBLE || view.getAlpha() == 0) {
return;
}
final Rect rect = new Rect();
view.getGlobalVisibleRect(rect); //得到view相對于整個屏幕的坐標
int contentTop = ((ViewGroup) getParent()).getTop();
Rect frame = new Rect();
((Activity) getContext()).getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
rect.offset(0, -contentTop - statusBarHeight);//去掉狀態欄高度和標題欄高度
if (rect.width() == 0 || rect.height() == 0) {
return;
}
//震動動畫
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
Random random = new Random();
@Override
public void onAnimationUpdate(ValueAnimator animation) {
view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f);
view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f);
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
explode(view, rect);
}
});
animator.start();
}
private void explode(final View view, Rect rect) {
final ExplosionAnimator animator = new ExplosionAnimator(this, Utils.createBitmapFromView(view), rect, mParticleFactory);
explosionAnimators.add(animator);
explosionAnimatorsMap.put(view, animator);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
//縮小,透明動畫
view.animate().setDuration(150).scaleX(0f).scaleY(0f).alpha(0f).start();
}
@Override
public void onAnimationEnd(Animator animation) {
view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(150).start();
//動畫結束時從動畫集中移除
explosionAnimators.remove(animation);
explosionAnimatorsMap.remove(view);
animation = null;
}
});
animator.start();
}
/**
* 給Activity加上全屏覆蓋的ExplosionField
*/
private void attach2Activity(Activity activity) {
ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
rootView.addView(this, lp);
}
/**
* 希望誰有破碎效果,就給誰加Listener
*
* @param view 可以是ViewGroup
*/
public void addListener(View view) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int count = viewGroup.getChildCount();
for (int i = 0; i < count; i++) {
addListener(viewGroup.getChildAt(i));
}
} else {
view.setClickable(true);
view.setOnClickListener(getOnClickListener());
}
}
private OnClickListener getOnClickListener() {
if (null == onClickListener) {
onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
ExplosionSite.this.explode(v);
}
};
}
return onClickListener;
}
}
總結:總體來說實現很簡單,就是根據工廠類,生成粒子數組。
而其實質是一個ValueAnimator,在一定時間內,從0數到1。
然后提供了一個draw()方法,方法里面調用了每個粒子的advance()方法,并且傳入了當前數到的數字。
advance()方法里,其實調用了draw()方法和caculate()方法。
上面的實現,其實是一個固定的流程,添加了爆炸場地以后,我們就開始從0數到1,在這個過程中,粒子會根據當前時間,繪制自己的位置,所以粒子的位置,其實是它自己決定的,和流程無關。
也就是說,我們只要用不同的算法,繪制粒子的位置即可,實現了流程和粒子運動的分離。
所以除了提供的六種爆炸特效之外,只要遵循這個原則,就能生成更多的粒子爆炸特效。
有了需求才有了功能,有了想法才有了創作,你的反饋會是使我進步的最大動力。
覺得還不夠方便?還想要什么功能?告訴我!歡迎反饋,歡迎Star。源碼入口:GitHub項目地址