前段时间在某效果网站看到开源项目【ExplosionField】非常喜欢,于是自己跟着源码学习着去做了做。跟源码效果有一点区别,我都是尽力读懂源码然后用自己的理解写出来,源码有些看不懂的地方,我也就没有用到,因为自己的代码要保证自己都能看懂。
最后效果如下:
(本文适合有一年Android开发经验者学习)
本文可以学到:
1.开源项目ExplosionField的实现思路
2.图示效果的实现过程
3.属性动画的用法
1.新建一个 Bean Particle,表示一个粒子对象;新建一个 View ExplosionField作为画布用来显示破碎的粒子;新建一个属性动画(ValueAnimator) ExplosionAnimator用来改变不同时刻的粒子状态;
2.通过View生成图片Bitmap,把生成的图片分解成若干个粒子,让每个粒子记录特定的位置,所有的粒子组合能看出是原图。
3.加上动画效果,使得点击View后,粒子能有所变化。
4.构思算法,形成不一样的效果。
5.匹配不同分辨率的设备。
6.重构。
可以先看看项目结构,非常简单:
v v
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谢谢!
原文出处: http://blog.csdn.net/XieYupeng520/article/details/49951835