最近决定好好整理一下,包括简书文章、自己平时关注研究和实现的代码。之前做过很多东西,都记录的比较散,要么文档不全,要么看过的源码没有留下痕迹,结果继续看和新的一样,那是忘得真快啊,花点时间复习一下,总结也是对知识的一种尊重,就从之前看过的一个开源爆炸特效开始吧。以后每天坚持知识总结并将零散的技术文档搬到简书。加油。
开源爆炸效果github地址
正题
一个视图要实现爆炸,关注以下几点。
1:爆炸图层。
2:爆炸区域确定,以视图中心区域为起始点向外发散
3:将视图View实现分割取色。
4:爆炸碎片特性初始化。
5:动画实现,这个最复杂,也是核心。使用随机+概率。
爆炸图层
自定义视图ExplosionField作为动画图层,加入到对应资源节点com.android.internal.R.id.content的FrameLayout视图。
public static ExplosionField attach2Window(Activity activity) {
ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
ExplosionField explosionField = new ExplosionField(activity);
rootView.addView(explosionField, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return explosionField;
}
ExplosionField是MATCH_PARENT,与setContentView的视图同一父视图FrameLayout,它在上层。
setContentView加入一个待爆炸的视图,+监听。
爆炸区域
点击视图,爆炸效果是从视图中心区域向外发散。动画起点在爆炸视图的中心区域。
public void explode(final View view) {
Rect r = new Rect();
view.getGlobalVisibleRect(r);
int[] location = new int[2];
getLocationOnScreen(location);
r.offset(-location[0], -location[1]);
r.inset(-mExpandInset[0], -mExpandInset[1]);//Rect(287, 685 - 793, 1191)扩大一定区域
int startDelay = 100;
view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start();//View缩放动画
explode(Utils.createBitmapFromView(view), r, startDelay, ExplosionAnimator.DEFAULT_DURATION);
}
View是待爆炸视图,getGlobalVisibleRect方法获取该视图以最顶层父视图DecorView为原点的坐标区域。这点注意,并不是以他父视图左上位坐标原点。
我获取到的该区域是Rect(375, 1015 - 705, 1345)。
getLocationOnScreen获取以屏幕左上为原点,爆炸图层的坐标,它以MATCH_PARENT加入。
我获取到该坐标是(0,242),242是系统标题栏和状态栏高度。
视图区域Rect#offset,坐标原点转换,前Rect坐标原点是DecorView视图,转换成装载setContentView的父视图。
我获取到的该区域是Rect(375, 773 - 705, 1103)。
爆炸视图区域.jpg在与setContentView同样大小的覆盖图层ExplosionField中确定了下层爆炸视图的位置坐标。整个ExplosionField是爆炸区域,这么做的目的是找到爆炸视图位置从而确定爆炸中心点的坐标。
黄色是ExplosionField,绿色是最终确定的区域Rect。
下层待爆炸视图设置一个缩小动画,在150毫秒很短的时间内缩放消失。
public void explode(Bitmap bitmap, Rect bound, long startDelay, long duration) {
final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound);//创建爆炸动画
explosion.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mExplosions.remove(animation);//结束后删除动画
}
});
explosion.setStartDelay(startDelay);
explosion.setDuration(duration);
mExplosions.add(explosion);
explosion.start();
}
几乎在同时,爆炸图层启动一个爆炸动画ExplosionAnimator 。爆炸图层支持下层多个视图同时爆炸。
Bitmap分割采样取色
1:从View中获取Bitmap。
2:Bitmap取色。
//从View获取Bitmap
public static Bitmap createBitmapFromView(View view) {
if (view instanceof ImageView) {
Drawable drawable = ((ImageView) view).getDrawable();
if (drawable != null && drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
}
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;
}
源码中放置的是ImageView,从BitmapDrawable获取。否则,利用Canvas获取。
我设置了一个ImageView背景蓝色,src是一个圆形G图片,边角透明,并未取到View边角蓝色部分,而是整个图片资源src的Bitmap像素。
创建动画时,将Bitmap分成255份。一个二维数组,存放每份爆炸碎片的特性。利用Bitmap#getPixel获取坐标x,y处的颜色值。当然,分的碎片越多,颜色值搜集的就越全。
public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) {
mPaint = new Paint();
mBound = new Rect(bound);
int partLen = 15;//定义装255个碎片的数组
mParticles = new Particle[partLen * partLen];
Random random = new Random(System.currentTimeMillis());
int w = bitmap.getWidth() / (partLen + 2);//宽高分17份
int h = bitmap.getHeight() / (partLen + 2);
//0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,分块越多,取的视图颜色值越全面
for (int i = 0; i < partLen; i++) {
for (int j = 0; j < partLen; j++) {
mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random);//根据分块,截取所有块的右下角的点的Bitmap颜色,
}
}
mContainer = container;
setFloatValues(0f, END_VALUE);//动画范围值
setInterpolator(DEFAULT_INTERPOLATOR);//加速差值器
setDuration(DEFAULT_DURATION);
}
Particle定义了每个碎片的特性,包括颜色,大小,运动轨迹等。
这部分的重点在横纵向遍历Bitmap的像素点区域,实现Bitmap色值采样,取每个区域的右下角色值。
爆炸碎片属性与动画逻辑实现
//产生碎片
private Particle generateParticle(int color, Random random) {
Particle particle = new Particle();
particle.color = color;// 颜色设置,每个碎片一种颜色
particle.radius = V;
//基础半径计算
if (random.nextFloat() < 0.2f) {
particle.baseRadius = V + ((X - V) * random.nextFloat());
} else {
particle.baseRadius = W + ((V - W) * random.nextFloat());
}
//nextFloat是通过随机数进行爆炸球x和y轴移动轨迹比例的控制
float nextFloat = random.nextFloat();
//(0.2f到0.36f)height,20%
//(0.2f到0.36f)height+(0f到0.72f)height,80%
particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f);
particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat());
//针对nextFloat<0.2时,(-0.9到0.9)height,20%
//针对0.2<nextFloat<0.8时,(-0.45到0.45)height,60%
//针对0.8<nextFloat<时,(-0.27到0.27)height,20%
particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f;
float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f;
particle.bottom = f;
//计算mag,neg
particle.mag = 4.0f * particle.top / particle.bottom;
particle.neg = (-particle.mag) / particle.bottom;
//计算爆炸球初始圆心坐标,不考虑透明时爆炸球开始出现的圆心点,所有球都集中在一小块区域中
f = mBound.centerX()+ (Y * (random.nextFloat() - 0.5f));//XY的中心点+随机值,不要都挤成一坨
particle.baseCx = f;
particle.cx = f;
f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f));
particle.baseCy = f;
particle.cy = f;
//计算life,overflow
particle.life = END_VALUE / 10 * random.nextFloat();//(0到0.14)
particle.overflow = 0.4f * random.nextFloat();//(0到0.4f)
particle.alpha = 1f;
return particle;
}
每个碎片产生一个Particle
属性如下
color,颜色
radius,圆形碎片半径,动画中改变。
baseRadius,基础半径,根据随机数概率,20%的碎片一个随机数计算方法,80%的是另一个计算方法,动画时静态。
mBound,上面计算的最终绿色区域
top,20%波动范围在(0.2f到0.36f)的区域高度height,80%的波动范围在(0.2f到0.36f)height+(0f到0.72f)height。
bottom,20%的碎片在(-0.9到0.9)height,60%的在(-0.45到0.45)height,还有20%的在(-0.27到0.27)height。它代表碎片动画时在x轴的运动圆心位置,概率+随机数。可以看出,在爆炸向外扩散时,60%的在中间位置分布。
neg、mag和top,一起用于计算动画时碎片在y轴的运动圆心位置。
cy,cx,动画过程中碎片的圆心位置,动态。
alpha,碎片透明度。
baseCx,baseCy,碎片基础圆心位置。静态,初始位置圆心。
overflow,life,根据动画进度,控制动画运行中开始阶段和结束阶段的状态变化。
动画运行时,重绘爆炸图层ExplosionField。触发动画draw方法。
public boolean draw(Canvas canvas) {
if (!isStarted()) {
return false;
}
//Log.d("factor_anim", "frac:" + frac + "," + particle.cx + "," + particle.cy + "," + particle.radius);
//为每一个碎片画一个圆
for (Particle particle : mParticles) {
float frac = (float) getAnimatedValue();
particle.advance(frac);
if (particle.alpha > 0f) {
mPaint.setColor(particle.color);//每一个碎片都只有一种颜色。
mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha));
canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint);
}
}
mContainer.invalidate();
return true;
}
Particle#advance根据当前进度,设置当前碎片属性,位置,半径,透明度。设置完成后,drawCircle绘制圆形碎片。
mContainer是爆炸图层,只要动画未结束,一连串的绘制将展现爆炸效果。
//根据动画运行比例,修改内容包括,透明度,圆心位置,圆心半径
public void advance(float factor) {
float f = 0f;
float normalization = factor / END_VALUE;
//运行比例在(0到0.14)和(0.6到1)时,设置透明
if (normalization < life || normalization > 1f - overflow) {//life(0到0.14)与overflow(0到0.4f) ,
alpha = 0f;//life是运行比例的十分之一x随机数,overflow是0.4x随机数,
return;
}
//这个阶段不设置透明,取出首尾,再计算比例
normalization = (normalization - life) / (1f - life - overflow);
float f2 = normalization * END_VALUE;//(0-1.4)
//在重计算的比例大于0.7时,后面的0.3比例开始让他逐渐变透明。使透明度有一个过度的阶段。
if (normalization >= 0.7f) {
f = (normalization - 0.7f) / 0.3f;
}
alpha = 1f - f;
f = bottom * f2;//bottom不变,运动到底部位置x(0-1.4),动画结束时,x轴移动最大位移将达1.4倍
//基础baseXx不变,圆心点x坐标,
//增加的权重值范围是(0-底部位置的1.4倍)
cx = baseCx + f;
cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;
radius = V + (baseRadius - V) * f2;
}
1,frac是运行比例,根据运行比例,normalization(0-1),根据life和overflow控制运行比例, 每个爆炸球的life和overflow不同,life是随机数0到0.14,overflow是0到0.4,因此,在动画运行在(0到0.14)和(0.6到1),所有的球陆续出现与消失。达到的效果是在一个时间段完成,但不同时。
2,在中间的时间段,球体可见,去除首尾,重置运行比例,目的是从可见的时机开始按照比例计算位置和半径。
3,后期30%的运行比例,透明度逐渐改变,避免瞬间消失的感觉效果。
4,圆心位置x坐标计算,每个碎片bottom是个定值,前面介绍过,所有碎片分布情况不同,有概率。f2是运行比例计算的权值(0到1.4),因此,右侧的碎片最远到达0.9heightx1.4的位置。
5,所有碎片初始位置原点在带爆炸视图的中心区域内。
6,cy是y轴方向的位置,该算法与top,bottom,neg,mag和运行比例相关,比较复杂。最终效果是碎片们先从初始位置圆点在y轴上向上移动,返回,向下移动,超过初始圆点,继而消失。
7,动画中,根据运行比例和基础半径,计算radius。
任重而道远
网友评论