前段时间在某效果网站看到开源项目【ExplosionField】非常喜欢,于是自己跟着源码学习着去做了做。跟源码效果有一点区别,我都是尽力读懂源码然后用自己的理解写出来,源码有些看不懂的地方,我也就没有用到,因为自己的代码要保证自己都能看懂。
最后效果如下:
(本文适合有一年Android开发经验者学习)
本文可以学到:
1.开源项目ExplosionField的实现思路
2.图示效果的实现过程
3.属性动画的用法
1.新建一个 Bean Particle
,表示一个粒子对象;新建一个 View ExplosionField
作为画布用来显示破碎的粒子;新建一个属性动画(ValueAnimator) ExplosionAnimator
用来改变不同时刻的粒子状态;
2.通过View
生成图片Bitmap
,把生成的图片分解成若干个粒子,让每个粒子记录特定的位置,所有的粒子组合能看出是原图。
3.加上动画效果,使得点击View
后,粒子能有所变化。
4.构思算法,形成不一样的效果。
5.匹配不同分辨率的设备。
6.重构。
1.1 新建Particle
对象,用来描述粒子,包括属性有颜色、透明度、圆心坐标、半径。
public class Particle {
float cx; //center x of circle
float cy; //center y of circle
float radius;
int color;
float alpha;
}
1.2 新建ExplosionField
对象,继承自View
,用于做粒子集的画布,需要重写onDraw()
方法
public class ExplosionField extends View{
public ExplosionField(Context context) {
super(context);
init();
}
public ExplosionField(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
//初始化
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制粒子
}
}
1.3 新建ExplosionAnimator
,继承自ValueAnimator
,用来执行自定义动画。ValueAnimator
简单来说就是在一段时间内通过不断改变值(一般是改变某个属性的值)来达到动画效果。更多可以参考《Android属性动画完全解析(上),初识属性动画的基本用法》来学习。
而我们现在是准备在一段时间内(大概1.5秒)让ValueAnimator
里的值从0.0f
变化到1.0f
,然后根据系统生成的递增随机值(范围在0.0f~1.0f
)改变Particle
里的属性值。
public class ExplosionAnimator extends ValueAnimator{
public static final int DEFAULT_DURATION = 1500;
public ExplosionAnimator() {
setFloatValues(0.0f, 1.0f);
setDuration(DEFAULT_DURATION);
}
}
这样,在1.5秒内,通过ExplosionAnimator
的方法getAnimatedValue()
就能够不断得到递增的范围在0.0f~1.0f
之间的值。
首先通过view
的宽高创建出一个同样大小的空白图,用Bitmap
的静态方法createBitmap()
创建,最后一个参数表示图片质量。
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
然后通过画布Canvas
,先把空白图设置到画布里,再让view
把自己画在画布上,空白图也变成了view
的翻版了。
mCanvas.setBitmap(bitmap);
view.draw(mCanvas);
//此处bitmap已是同view显示一样的图
完整代码:
//ExplosionField.java
public class ExplosionField extends View{
private static final Canvas mCanvas = new Canvas();
private Bitmap createBitmapFromView(View view) {
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;
}
}
PS:在原项目ExplosionField
中还有一个判断,如果view
是ImageView
的对象,那么直接获得ImageView
依附的BitmapDrawable
图。
if (view instanceof ImageView) {
Drawable drawable = ((ImageView)view).getDrawable();
if (drawable != null && drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
}
我为什么去掉了呢?是因为如果ImageView设置了背景(background
)的话,这样直接获取的BitmapDrawable
是src
的引用,并不包括背景色。所以统一用画布绘制的方法生成快照。
好了,先拿一个TextView
做示范,看看复制的效果:
前面我们已经生成了快照图片,现在我们需要把快照分解成若干个粒子,这些粒子的组合能看出来是原图的影子,然后再让粒子动起来形成后面的动画。
那怎么做呢?ExplosionField项目是分解成15 * 15个粒子,我这里有点不一样我就直接按照我的思路讲解了。
首先定义一个二维数组Particle[][]
(一维的也行啦,原项目就是定义一维的),用来存放所有粒子,因为图片大小不同,粒子个数也不会相同,所以我们把粒子的宽高固定,在Particle
类中新加一个静态常量属性
public static final int PART_WH = 8; //默认小球宽高
然后根据view
的宽高,算出横竖粒子的个数
//ExplosionAnimator.java - generateParticles(Bitmap bitmap, Rect bound)
int w = bound.width();
int h = bound.height();
int partW_Count = w / Particle.PART_WH; //横向个数
int partH_Count = h / Particle.PART_WH; //竖向个数
Particle[][] particles = new Particle[partH_Count][partW_Count];
其中bound
是Rect
类型,通过view.getGlobalVisibleRect()
方法能得到view
相对于整个屏幕的坐标
Rect bound = new Rect(); view.getGlobalVisibleRect(rect);
然后把二维粒子数组对应图片的位置,设置为相应的颜色属性和坐标。
通过bitmap.getPixel(x, y)
可以获得(x, y)
坐标的bitmap
的颜色值
//ExplosionAnimator.java - generateParticles(Bitmap bitmap, Rect bound)
Point point = null;
for (int row = 0; row < partH_Count; row ++) { //行
for (int column = 0; column < partW_Count; column ++) { //列
//取得当前粒子所在位置的颜色
int color = bitmap.getPixel(column * partW_Count, row * partH_Count);
point = new Point(column, row); //x是列,y是行
particles[row][column] = Particle.generateParticle(color, bound, point);
}
}
在Particle
类中定义静态方法generateParticle()
用来生成新的Particle
对象
//Particle.java
public static Particle generateParticle(int color, Rect bound, Point point) {
int row = point.y; //行是高
int column = point.x; //列是宽
Particle particle = new Particle();
particle.mBound = bound;
particle.color = color;
particle.alpha = 1f;
particle.radius = PART_WH;
particle.cx = bound.left + PART_WH * column;
particle.cy = bound.top + PART_WH * row;
return particle;
}
这里把半径设置为宽长,而不是宽的一半,是因为叠加显示效果会更好看一点。
为了能够显示出来,我们新建一个draw()
方法,用从ExplosionField
传来的canvas
来绘制所有粒子
//ExplosionAnimator.java
public void draw(Canvas canvas) {
for (Particle[] particle : mParticles) {
for (Particle p : particle) {
canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
}
}
}
//ExplosionField.java
private ArrayList<ExplosionAnimator> explosionAnimators;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (ExplosionAnimator animator : explosionAnimators) {
animator.draw(canvas);
}
}
因为画布可能同时绘制几个动画,所以用一个List
保存动画集。
现在大概的效果是这样:
前面说过,在ExplosionAnimator
中通过方法getAnimatedValue()
就能够不断得到递增的范围在0.0f~1.0f
之间的值(记做factor
)。
我们先在Particle
写好得到变化因素后,属性要发生的改变。cx
左右移动都可以,cy
向下移动且距离和view
高度有关(不同高度图片,每次下降距离不同),radius
变小,alpha
变得越来越透明。只要符合这几点,算法随便写就可以了。
//Particle.java
public 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());
}
记住传进来的factor
是从0.0f
到1.0f
不断递增的。
然后改造draw()
方法,每次绘制都让粒子“前进一步”调用一次advance()
方法,然后根据新属性重新绘制
//ExplosionAnimator.java
public void draw(Canvas canvas) {
if(!isStarted()) { //动画结束时停止
return;
}
for (Particle[] particle : mParticles) {
for (Particle p : particle) {
p.advance((Float) getAnimatedValue());
mPaint.setColor(p.color);
mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha)); //这样透明颜色就不是黑色了
canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
}
}
mContainer.invalidate();
}
最后一句的mContainer
其实就是ExplosionField
,调用它的invalidate()
方法,就是调用ExplosionField
的onDraw()
方法。而ExplosionField
的onDraw()
里又调用了ExplosionAnimator
的draw()
方法。这样循环就出现了动画效果。
结束的条件就是第一句if(!isStarted())
如果动画停止了,就断了绘制循环。
PS:这里值得一提的有setAlpha()
方法,之前我用的是
mPaint.setColor(p.color);
mPaint.setAlpha((int) (255 * p.alpha));
这样有个问题就是当颜色为透明时,显示的是黑色。
而改为了方法:
mPaint.setColor(p.color);
mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));
透明颜色就为透明色了。
现在动画过程已经写完,就差开始的导火线了,我们在动画开始的时候启动这根导火线,重写start()
方法:
//ExplosionAnimator.java
@Override
public void start() {
super.start();
mContainer.invalidate();
}
那在哪使动画开始呢,即在哪调用explosionAnimator.start()
呢?
在ExplosionField
中建立一个“爆炸”方法,只要调用这个方法,传入view
,最后执行animator.start()
,view
就会执行爆炸效果。
public void explode(final View view) {
Rect rect = new Rect();
view.getGlobalVisibleRect(rect); //得到view相对于整个屏幕的坐标
rect.offset(0, -Utils.dp2px(25)); //去掉状态栏高度
final ExplosionAnimator animator = new ExplosionAnimator(this, createBitmapFromView(view), rect);
explosionAnimators.add(animator);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
view.animate().alpha(0f).setDuration(150).start();
}
@Override
public void onAnimationEnd(Animator animation) {
view.animate().alpha(1f).setDuration(150).start();
//动画结束时从动画集中移除
explosionAnimators.remove(animation);
animation = null;
}
});
animator.start();
}
现在的效果:
现在动画效果什么的都做好了,要如何使用呢?
现在的思路是在Activity的最上层盖一层透明的ExplosionField
视图,用来显示粒子动画。
//ExplosionField.java
/** * 给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);
}
其实Activity
的根视图并不是我们设置的xml,它上面还有一层,通过findViewById(Window.ID_ANDROID_CONTENT)
能够得到,然后我们再把ExplosionField
全屏加载在Activity
的最上层,这样显示动画效果就不会被遮盖。
然后我们可以在初始化的时候加上这个方法:
public class ExplosionField extends View{
public ExplosionField(Context context) {
super(context);
init();
}
public ExplosionField(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
...
attach2Activity((Activity) getContext());
}
...
}
在看Activity
的onCreate()
方法就非常简单了:
//MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_az);
ExplosionField explosionField = new ExplosionField(this);
explosionField.addListener(findViewById(R.id.root));
}
最后一句调用了addListener()
方法,就是把需要实现点击破碎效果的view
加上监听器,看代码:
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 View.OnClickListener() {
@Override
public void onClick(View v) {
ExplosionField.this.explode(v);
}
};
}
return onClickListener;
}
只要传入ViewGroup
,会自动递归查找Child View
,并给Child View
加上点击监听器,一旦点击就调用爆破方法执行动画。
最终效果大图:
更多详细代码可 fork 源码查看!
源码地址:https://github.com/Xieyupeng520/AZExplosion
如果你喜欢这个效果,请给我Github上一个Star鼓励一下哈O(∩_∩)O谢谢!