5分鐘快速實(shí)現(xiàn)Android爆炸破碎酷炫動(dòng)畫特效的示例
這個(gè)破碎動(dòng)畫,是一種類似小米系統(tǒng)刪除應(yīng)用時(shí)的爆炸破碎效果的動(dòng)畫。
效果圖展示
先來看下是怎樣的動(dòng)效,要是感覺不是理想的學(xué)習(xí)目標(biāo),就跳過,避免浪費(fèi)大家的時(shí)間。��

源碼在這里:point_right: https://github.com/ReadyShowShow/explosion
一行代碼即可調(diào)用該動(dòng)畫
new ExplosionField(this).explode(view, null))
下面開始我們酷炫的Android動(dòng)畫特效正式講解:point_down:
先來個(gè)整體結(jié)構(gòu)的把握
整體結(jié)構(gòu)非常簡單明了,新老從業(yè)者都可快速看懂,容易把握學(xué)習(xí)。
./ |-- explosion | |-- MainActivity.java (測試爆炸破碎動(dòng)效的主界面) | |-- animation(爆炸破碎動(dòng)效有關(guān)的類均在這里) | | |-- ExplosionAnimator.java(爆炸動(dòng)畫) | | |-- ExplosionField.java(爆炸破碎動(dòng)畫所依賴的View) | | `-- ParticleModel.java(每個(gè)破碎后的粒子的model,顏色、位置、大小等) | `-- utils | `-- UIUtils.java(計(jì)算狀態(tài)欄高度的工具類) `-- tree.txt
庖丁解牛
下面開始每個(gè)類的詳細(xì)分析
本著從簡到繁、由表及里的原則,詳細(xì)講解每個(gè)類
MainActivity.java
MainActivity.java是測試動(dòng)效的界面,該Activity內(nèi)部有7個(gè)測試按鈕。該類做的事情非常單純,就是給每個(gè)View分別綁定click點(diǎn)擊事件,讓View在點(diǎn)擊時(shí)能觸發(fā)爆炸破碎動(dòng)畫。
/**
* 說明:測試的界面
* 作者:Jian
* 時(shí)間:2017/12/26.
*/
public class MainActivity extends AppCompatActivity {
/**
* 加載布局文件,添加點(diǎn)擊事件
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViewsClick();
}
/**
* 添加點(diǎn)擊事件的實(shí)現(xiàn)
*/
private void initViewsClick() {
// 為單個(gè)View添加點(diǎn)擊事件
final View title = findViewById(R.id.title_tv);
title.setOnClickListener(v ->
new ExplosionField(MainActivity.this).explode(title, null));
// 為中間3個(gè)View添加點(diǎn)擊事件
setSelfAndChildDisappearOnClick(findViewById(R.id.title_disappear_ll));
// 為下面3個(gè)View添加點(diǎn)擊事件
setSelfAndChildDisappearAndAppearOnClick(findViewById(R.id.title_disappear_and_appear_ll));
// 跳轉(zhuǎn)到github網(wǎng)頁的點(diǎn)擊事件
findViewById(R.id.github_tv).setOnClickListener((view) -> {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
Uri content_url = Uri.parse(getString(R.string.github));
intent.setData(content_url);
startActivity(intent);
});
}
/**
* 為自己以及子View添加破碎動(dòng)畫,動(dòng)畫結(jié)束后,把View消失掉
* @param view 可能是ViewGroup的view
*/
private void setSelfAndChildDisappearOnClick(final View view) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
setSelfAndChildDisappearOnClick(viewGroup.getChildAt(i));
}
} else {
view.setOnClickListener(v ->
new ExplosionField(MainActivity.this).explode(view,
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
view.setVisibility(View.GONE);
}
}));
}
}
/**
* 為自己以及子View添加破碎動(dòng)畫,動(dòng)畫結(jié)束后,View自動(dòng)出現(xiàn)
* @param view 可能是ViewGroup的view
*/
private void setSelfAndChildDisappearAndAppearOnClick(final View view) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
setSelfAndChildDisappearAndAppearOnClick(viewGroup.getChildAt(i));
}
} else {
view.setOnClickListener(v ->
new ExplosionField(MainActivity.this).explode(view, null));
}
}
}
ParticleModel.java
ParticleModel.java是包含一個(gè)粒子的所有信息的model。advance方法根據(jù)值動(dòng)畫返回的進(jìn)度計(jì)算出粒子的位置和顏色等信息
/**
* 說明:爆破粒子,每個(gè)移動(dòng)與漸變的小塊
* 作者:Jian
* 時(shí)間:2017/12/26.
*/
class ParticleModel {
// 默認(rèn)小球?qū)捀?
static final int PART_WH = 8;
// 隨機(jī)數(shù),隨機(jī)出位置和大小
static Random random = new Random();
//center x of circle
float cx;
//center y of circle
float cy;
// 半徑
float radius;
// 顏色
int color;
// 透明度
float alpha;
// 整體邊界
Rect mBound;
ParticleModel(int color, Rect bound, Point point) {
int row = point.y; //行是高
int column = point.x; //列是寬
this.mBound = bound;
this.color = color;
this.alpha = 1f;
this.radius = PART_WH;
this.cx = bound.left + PART_WH * column;
this.cy = bound.top + PART_WH * row;
}
// 每一步動(dòng)畫都得重新計(jì)算出自己的狀態(tài)值
void advance(float factor) {
cx = cx + factor * random.nextInt(mBound.width()) * (random.nextFloat() - 0.5f);
cy = cy + factor * random.nextInt(mBound.height() / 2);
radius = radius - factor * random.nextInt(2);
alpha = (1f - factor) * (1 + random.nextFloat());
}
}
ExplosionAnimation.java
ExlosionAnimation.java是動(dòng)畫類,是一個(gè)值動(dòng)畫,在值動(dòng)畫每次產(chǎn)生一個(gè)值的時(shí)候,就計(jì)算出整個(gè)爆炸破碎動(dòng)效內(nèi)的全部粒子的狀態(tài)。這些狀態(tài)交由使用的View在渲染時(shí)進(jìn)行顯示。
/**
* 說明:爆炸動(dòng)畫類,讓離子移動(dòng)和控制離子透明度
* 作者:Jian
* 時(shí)間:2017/12/26.
*/
class ExplosionAnimator extends ValueAnimator {
private static final int DEFAULT_DURATION = 1500;
private ParticleModel[][] mParticles;
private Paint mPaint;
private View mContainer;
public ExplosionAnimator(View view, Bitmap bitmap, Rect bound) {
setFloatValues(0.0f, 1.0f);
setDuration(DEFAULT_DURATION);
mPaint = new Paint();
mContainer = view;
mParticles = generateParticles(bitmap, bound);
}
// 生成粒子,按行按列生成全部粒子
private ParticleModel[][] generateParticles(Bitmap bitmap, Rect bound) {
int w = bound.width();
int h = bound.height();
// 橫向粒子的個(gè)數(shù)
int horizontalCount = w / ParticleModel.PART_WH;
// 豎向粒子的個(gè)數(shù)
int verticalCount = h / ParticleModel.PART_WH;
// 粒子寬度
int bitmapPartWidth = bitmap.getWidth() / horizontalCount;
// 粒子高度
int bitmapPartHeight = bitmap.getHeight() / verticalCount;
ParticleModel[][] particles = new ParticleModel[verticalCount][horizontalCount];
for (int row = 0; row < verticalCount; row++) {
for (int column = 0; column < horizontalCount; column++) {
//取得當(dāng)前粒子所在位置的顏色
int color = bitmap.getPixel(column * bitmapPartWidth, row * bitmapPartHeight);
Point point = new Point(column, row);
particles[row][column] = new ParticleModel(color, bound, point);
}
}
return particles;
}
// 由view調(diào)用,在View上繪制全部的粒子
void draw(Canvas canvas) {
// 動(dòng)畫結(jié)束時(shí)停止
if (!isStarted()) {
return;
}
// 遍歷粒子,并繪制在View上
for (ParticleModel[] particle : mParticles) {
for (ParticleModel p : particle) {
p.advance((Float) getAnimatedValue());
mPaint.setColor(p.color);
// 錯(cuò)誤的設(shè)置方式只是這樣設(shè)置,透明色會顯示為黑色
// mPaint.setAlpha((int) (255 * p.alpha));
// 正確的設(shè)置方式,這樣透明顏色就不是黑色了
mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));
canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
}
}
mContainer.invalidate();
}
@Override
public void start() {
super.start();
mContainer.invalidate();
}
}
ExplosionField.java
ExplosionField.java是真實(shí)執(zhí)行上面ExplosionAnimator。ExplosionField會創(chuàng)建一個(gè)View并依附在Activity的根View上。
/**
* 說明:每次爆炸時(shí),創(chuàng)建一個(gè)覆蓋全屏的View,這樣的話,不管要爆炸的View在任何位置都能顯示爆炸效果
* 作者:Jian
* 時(shí)間:2017/12/26.
*/
public class ExplosionField extends View {
private static final String TAG = "ExplosionField";
private static final Canvas mCanvas = new Canvas();
private ExplosionAnimator animator;
public ExplosionField(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
animator.draw(canvas);
}
/**
* 執(zhí)行爆破破碎動(dòng)畫
*/
public void explode(final View view, final AnimatorListenerAdapter listener) {
Rect rect = new Rect();
view.getGlobalVisibleRect(rect); //得到view相對于整個(gè)屏幕的坐標(biāo)
rect.offset(0, -UIUtils.statusBarHeignth()); //去掉狀態(tài)欄高度
animator = new ExplosionAnimator(this, createBitmapFromView(view), rect);
// 接口回調(diào)
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
if (listener != null) listener.onAnimationStart(animation);
// 延時(shí)添加到界面上
attach2Activity((Activity) getContext());
// 讓被爆炸的View消失(爆炸的View是新創(chuàng)建的View,原View本身不會發(fā)生任何變化)
view.animate().alpha(0f).setDuration(150).start();
}
@Override
public void onAnimationEnd(Animator animation) {
if (listener != null) listener.onAnimationEnd(animation);
// 從界面中移除
removeFromActivity((Activity) getContext());
// 讓被爆炸的View顯示(爆炸的View是新創(chuàng)建的View,原View本身不會發(fā)生任何變化)
view.animate().alpha(1f).setDuration(150).start();
}
@Override
public void onAnimationCancel(Animator animation) {
if (listener != null) listener.onAnimationCancel(animation);
}
@Override
public void onAnimationRepeat(Animator animation) {
if (listener != null) listener.onAnimationRepeat(animation);
}
});
animator.start();
}
private Bitmap createBitmapFromView(View view) {
// 為什么屏蔽以下代碼段?
// 如果ImageView直接得到位圖,那么當(dāng)它設(shè)置背景(backgroud)時(shí),不會讀取到背景顏色
// if (view instanceof ImageView) {
// Drawable drawable = ((ImageView)view).getDrawable();
// if (drawable != null && drawable instanceof BitmapDrawable) {
// return ((BitmapDrawable) drawable).getBitmap();
// }
// }
//view.clearFocus(); //不同焦點(diǎn)狀態(tài)顯示的可能不同——(azz:不同就不同有什么關(guān)系?)
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
if (bitmap != null) {
synchronized (mCanvas) {
mCanvas.setBitmap(bitmap);
view.draw(mCanvas);
// 清除引用
mCanvas.setBitmap(null);
}
}
return bitmap;
}
/**
* 將創(chuàng)建的ExplosionField添加到Activity上
*/
private void attach2Activity(Activity activity) {
ViewGroup rootView = activity.findViewById(Window.ID_ANDROID_CONTENT);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
rootView.addView(this, lp);
}
/**
* 將ExplosionField從Activity上移除
*/
private void removeFromActivity(Activity activity) {
ViewGroup rootView = activity.findViewById(Window.ID_ANDROID_CONTENT);
rootView.removeView(this);
}
}
動(dòng)畫執(zhí)行時(shí)為什么要?jiǎng)?chuàng)建一個(gè)新View(ExplosionField)
其實(shí)上面的動(dòng)畫類ExplosionAnimator已經(jīng)實(shí)現(xiàn)了核心功能,直接在原View上使用該動(dòng)畫應(yīng)該是沒問題的。為什么還要引入一個(gè)ExplosionField類呢?動(dòng)畫的執(zhí)行為什么不能直接在原本的View上執(zhí)行呢?偏偏要在一個(gè)看似多余的ExplosionField對象上執(zhí)行呢。
這里就得從Android下View繪制原理來解釋了:Android下的View都有一個(gè)Bound,在View進(jìn)行measure和layout的時(shí)候,已經(jīng)確定了View的大小和位置,如果要在這個(gè)View上進(jìn)行動(dòng)畫的話,就會出現(xiàn)動(dòng)畫只能在view大小范圍內(nèi)進(jìn)行展現(xiàn)。當(dāng)然了,也不是說在原來View上一定不能實(shí)現(xiàn)這一動(dòng)效,就是相當(dāng)復(fù)雜,要在動(dòng)畫執(zhí)行過程中,不斷改變原View的大小和View的屬性等信息,相當(dāng)復(fù)雜。
在性能還行的前提下,要優(yōu)先代碼的整潔度,盡量避免為了優(yōu)化的性能,而舍棄整潔清爽的代碼。一般來說,過度的優(yōu)化,并沒有給用戶帶來太多體驗(yàn)上的提升,反而給項(xiàng)目帶來了巨大的維護(hù)難度。
UIUtils.java
UIUtils是關(guān)于UI的工具類,沒啥可說的
public class UIUtils {
public static int dp2px(double dpi) {
return (int) (Resources.getSystem().getDisplayMetrics().density * dpi + 0.5f);
}
public static int statusBarHeignth() {
return dp2px(25);
}
}
結(jié)束
源碼:point_right: https://github.com/ReadyShowShow/explosion
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android自定義View實(shí)現(xiàn)比賽時(shí)間閃動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)比賽時(shí)間閃動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
Android判斷11位手機(jī)號碼的方法(正則表達(dá)式)
項(xiàng)目里頭需要做一個(gè)判斷用戶輸入的號碼是否是正確的手機(jī)號碼,正確的手機(jī)號碼應(yīng)該是11位的,這里我們需要用一個(gè)正則表達(dá)式來進(jìn)行判斷,下面我把寫法分享給大家2016-12-12
android實(shí)現(xiàn)切換日期左右無限滑動(dòng)效果
本篇內(nèi)容給大家分享了android開發(fā)時(shí)候?qū)崿F(xiàn)自定義的日期無限左右滑動(dòng)效果以及控件使用的技巧。2017-11-11
Android ListView構(gòu)建支持單選和多選的投票項(xiàng)目
如何在Android的ListView中構(gòu)建CheckBox和RadioButton列表?這篇文章主要為大家詳細(xì)介紹了Android ListView實(shí)現(xiàn)支持單選和多選的投票項(xiàng)目,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
Flutter?Widget之NavigationBar使用詳解
這篇文章主要為大家介紹了Flutter?Widget之NavigationBar使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

