Android点赞烟花效果实现

本文字数:2910字
预计阅读时间:15分钟
01
前言介绍
本文字数:2910字
预计阅读时间:15分钟
01
前言介绍
App中使用点赞特效来提高用户互动的做法普遍存在。下面选取一种烟花点赞特效,配置上节日素材小图标,可以很好的营造节日气氛,增强用户操作的兴趣。
02
效果截图
02
效果截图
03
实现思路
03
实现思路
在脑海里迅速过一下烟花效果的实现过程,也可以通过这些问题自己想象下怎么实现。
烟花的产生与消失,主要就是怎么来实现那种从定点开始的抛射效果? 烟花到达最高点后消失,如何产生爆炸后扩散的火花以及火花如何形成扩散的圆环? 如何控制内外层火花圆环的扩散,呈现出有序的层叠辐射效果,以及最后火花的消失?
基本上,想清楚了以上三步,你的烟花效果也就算是做出来了。很简单吧,下面我们看下实现过程。
04
实现过程
04
实现过程
根据整个烟花效果,将烟花分为两个主体,一是发射点发出的烟花粒子,二是烟花爆炸后形成的火花粒子组。将每个粒子持有一张对应的bitmap图片,在画布canvs上绘制图形,这样不就组成了烟花图案了嘛。
比如效果图中,烟花粒子为logo小图标,外圈火花粒子组为七彩小圆点,内圈火花粒子组为黄色小五角星。当然素材的选取与搭配,可以根据想法DIY自己设计哦。
粒子有了,自定义一个视图 ParticleDrawingView 承载烟花吧,根据预设的组数N一次点击可以同时打出多组烟花。那么,烟花图形又是怎么动气来的呢,通过属性动画ValueAnimator设置运行时间Duration,利用插值器在每个时间因子不断更新粒子的位置、速度、透明度、旋转角度等属性,通过view.postInvalidate() 渲染更新画布。
最后,将烟花视图mDrawingView添加在一个全屏父容器ViewGrouup中,就可以呈现出烟花满屏绽放的效果了。
1. 定义粒子对象
public class Particle {
Matrix mMatrix;
Paint mPaint;
Bitmap mImage; float mInitialX, mInitialY; //初始位置
float mSpeedX, mSpeedY; //发射粒子的速度(像素/毫秒)
float mRotation; //旋转角度值
float mScale = 1f; //缩放
int mAlpha = 255; //透明度
float mCurrentX, mCurrentY; //坐标位置
protected Particle(Bitmap bitmap) {
mMatrix = new Matrix();
mPaint = new Paint();
mImage = bitmap;
}
public boolean update(long miliseconds) {
// 更新粒子属性
updateLocation(this, miliseconds);
updateScale(this, miliseconds);
updateAlpha(this, miliseconds);
return true;
}
public void draw(Canvas c) {
mMatrix.reset();
mMatrix.postRotate(mRotation, mBitmapHalfWidth, mBitmapHalfHeight);
mMatrix.postScale(mScale, mScale, mBitmapHalfWidth, mBitmapHalfHeight);
mMatrix.postTranslate(mCurrentX, mCurrentY);
mPaint.setAlpha(mAlpha);
c.drawBitmap(mImage, mMatrix, mPaint);
}
}
2. 在画布上绘制图形
public class ParticleLikeFieldView extends View {
List<Particle> fireworkList; // 烟花粒子
List<List<Particle>> sparkOuterList; // 火花外圈粒子组
List<List<Particle>> sparkInnerList; // 火花内圈粒子组
boolean isFireworkDead = false; // 烟花是否已经爆炸 @Override
protected void onDraw(Canvas canvas) {
synchronized (this) {
if (!isFireworkDead) {
for (Particle p : fireworkList) {
p.draw(canvas);
}
} else {
for (List<Particle> outerList : sparkOuterList) {
for (Particle p : outerList) {
p.draw(canvas);
}
}
for (List<Particle> innerList : sparkInnerList) {
for (Particle p : innerList) {
p.draw(canvas);
}
}
}
}
}
}
// 点赞触发动画时在容器中添加烟花视图
private void addDrawingView() {
ParticleLikeFieldView mDrawingView = new ParticleLikeFieldView(context);
ViewGroup mParentView = (ViewGroup) activity.findViewById(android.R.id.content); //全屏容器
mParentView.addView(mDrawingView);
}// 整个动画结束后在容器中清除烟花视图
private void cleanupDrawingView() {
mParentView.removeView(mDrawingView);
mParentView.postInvalidate();
mDrawingView = null;
}
3. 创建动画
烟花视图有了,开始创建动画来控制视图一点一点动起来吧!
一起回顾下烟花动画过程:首先从发射位置发射烟花粒子,烟花粒子逐步放大,受重力因素做减速运动,运动至目标位置后快速消失并开始爆炸。然后是爆炸后火花的运动,火花匀速向外扩散成圆环,同时火花粒子逐步放大,运动至目标位置后快速消失。火花的扩展分外圈和内圈两组,每组8个粒子组成圆环向外扩散,其中外圈先于内圈执行,内环扩散完成整个烟花过程结束。
下面将动画过程拆分,逐个创建动画对象:
fireworkAnim(发射烟花),时长fireworkDuration 800毫秒
├── Scan:烟花小图标设置为2倍放大。
├── Alpha: 粒子在其运行最后100毫秒快速消失。sparkOuterAnim(火花外圈扩散),时长sparkDuration 300毫秒
├── Scan:火花圆环设置为3倍放大。
├── Alpha: 粒子在其运行最后100毫秒快速消失。
sparkInnerAnim(火花内圈扩散),时长sparkDuration 300毫秒
├── Scan:火花圆环设置为3倍放大。
├── Alpha: 粒子在其运行最后100毫秒快速消失。
nullOuterAnim(火花外圈停顿),时长100毫秒
下面以fireworkAnim举例:
ValueAnimator fireworkAnim = ValueAnimator.ofInt(0, fireworkDuration);
fireworkAnim.setDuration(fireworkDuration);
fireworkAnim.setInterpolator(new DecelerateInterpolator());
// 监听动画帧变化
fireworkAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 插值值?下面会有介绍
int miliseconds = (Integer) animation.getAnimatedValue();
// 更新粒子属性
particle.update(miliseconds);
// 更新视图显示
mDrawingView.postInvalidate();
}
});
// 监听动画生命周期
fireworkAnim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mDrawingView.isFireworkDead = true;
}
@Override
public void onAnimationCancel(Animator animation) {
cleanupDrawingView();
}
});
4. 组合动画
AnimatorSet set = new AnimatorSet();
set.play(fireworkAnim).before(sparkOuterAnim);
set.play(sparkOuterAnim).with(nullOuterAnim);
set.play(nullOuterAnim).before(sparkInnerAnim);
set.start();
5. 速率控制
动画的几个阶段我们搞清楚了,那么每个阶段的动画又是如何控制速率的变化呢?
比如,如何控制粒子匀速放大?如何控制粒子加速消失?又如何控制粒子减速移动?
试想,动画按照时间行走,而时间是匀速增长的,在没有给定速率转换公式的前提下,又该如何模拟速率变化呢?下面就借助于属性动画插值器 Interpolator,就可以轻松做到。
插值器
插值器 Interpolator,实现TimeInterpolator接口,核心函数为 getInterpolation方法,即根据当前动画运行的时间占比来计算当前动画的变化频率。
参数说明:input,范围0~1.0的值,指示当前动画的进度,0表示开始,1.0表示结束;返回值fraction为插值值。
public interface TimeInterpolator {
float getInterpolation(float input);
}
插值值
Android 内置了种类丰富的插值器,为了说明返回的插值值的取值规律,下面简单介绍几种。
例如,LinearInterpolator,没有对input值进行任何逻辑处理,直接返回,即input值 = fraction值。因为input值是匀速增加的,因此插值值也是匀速增加的,所以叫做匀速插值器。
public float getInterpolation(float input) {
return input;
}
例如,AccelerateDecelerateInterpolator,通过公式转换,插值值体现出的速率变化是先加速后减速。
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}
例如,CycleInterpolator 循环插值器
public float getInterpolation(float input) {
return (float)(Math.sin(2 * mCycles * Math.PI * input));
}
05
粒子属性计算
05
粒子属性计算
做了以上准备工作,下面看下粒子一系列属性是怎么计算的。
1. 确定发射位置
怎么精确计算发射点位置?可以是点赞按钮,也可以是一个点击位置。Canvas 2D坐标系,左上角为(0,0)点。先获取发射按钮在当前窗口内的绝对坐标,根据设置的发射重力范围和发射按钮宽高信息,可以计算出发射点的位置范围。这里我们按照发射点为发射按钮的正中心来计算发射位置。
// 获取发射按钮在当前窗口内的绝对坐标
int[] location = new int[2];
emitter.getLocationInWindow(location);// 发射按钮中心位置坐标
int emitterX = location[0] + emitter.getWidth() / 2;
int emitterY = location[1] + emitter.getHeight() / 2;
// 粒子初始位置
particle.mInitialX = emiterX - mBitmapHalfWidth;
particle.mInitialY = emiterY - mBitmapHalfHeight;
particle.mCurrentX = mInitialX;
particle.mCurrentY = mInitialY;
2. 初始速度、角速度
烟花粒子运动方向,由于一次点击发射多组烟花,根据传入的角度范围 minAngle、maxAngle,既要保证粒子能正常扩展到目标范围,还要尽可能均匀地分布在既定区域的任意方向。
// 烟花粒子发射角度
int angle = random.nextInt(maxAngle - minAngle) + minAngle;
// 火花粒子组发射角度
float angleOuter = 45 * (n - 1); //外圈粒子
float angleInner = 45 * (n- 1); //内圈粒子// 角度转换为弧度:180 -> 3.14
double angleInRads = Math.toRadians(angle);
// 分解为X和Y方向上的速度,其中speed为外部控制系数。
particle.mSpeedX = (float) (speed * Math.cos(angleInRads));
particle.mSpeedY = (float) (speed * Math.sin(angleInRads));
// 旋转角度值
particle.mRotation = angle + 90;
火花粒子组,每组8个,n从1开始。
3. 缩放
借助LinearInterpolator,动画过程中不断更新粒子的scale属性,达到烟花小图标2倍匀速放大的效果。根据预期效果,烟花小图标设置为2倍缩放,火花小圆点设置为3倍缩放。插值器使用 LinearInterpolator 匀速缩放。
float mInitialValue = 1.0; float mFinalValue = 2.0;
Interpolator mInterpolator = new LinearInterpolator();public boolean updateScale(Particle particle, long miliseconds, long startMilis, long endMilis) {
if (miliseconds < startMilis) {
particle.mScale = mInitialValue;
} else if (miliseconds > endMilis) {
particle.mScale = mFinalValue;
} else {
// input为当前动画的进度值,线性插值器插值值fraction=input
float input = (miliseconds - startMilis) * 1f / (endMilis - startMilis);
float fraction = mInterpolator.getInterpolation(input);
float newScale = mInitialValue + (mFinalValue - mInitialValue) * fraction;
particle.mScale = newScale;
}
}
4. 透明度
借助AccelerateInterpolator,在粒子运行最后100毫秒内不断更新粒子的alpha属性,达到烟花加速淡出直至消除的效果。
int mInitialValue = 255; int mFinalValue = 0;
Interpolator mInterpolator = new AccelerateInterpolator();public boolean updateAlpha(Particle particle, long miliseconds, long startMilis, long endMilis) {
if (miliseconds < startMilis) {
particle.mAlpha = mInitialValue;
} else if (miliseconds > endMilis) {
particle.mAlpha = mFinalValue;
} else {
// input为当前动画的进度值,加速插值器插值值fraction速率变化也是加速的
float input = (miliseconds - startMilis) * 1f / (endMilis - startMilis);
float fraction = mInterpolator.getInterpolation(input);
int newAlpha = (int) (mInitialValue + (mFinalValue - mInitialValue) * fraction);
particle.mAlpha = newAlpha;
}
}
5. 位置坐标
ValueAnimator animation = ValueAnimator.ofInt(startValue, endValue);
animation.setDuration(duration);
animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction(); // 范围是由0到1
int value = (Integer) animation.getAnimatedValue(); // 范围是由startValue到endValue
updateLocation(particle, value);
}
});
当设置startValue=0, endValue=duration时,监听回调中value的取值范围就和动画的运行时长范围一致了。所以,此时可以将value的取值作为动画运行的某个时间点的值 miliseconds。
public boolean updateLocation(Particle particle, long miliseconds) {
particle.mCurrentX = mInitialX + mSpeedX * miliseconds;
particle.mCurrentY = mInitialY + mSpeedY * miliseconds;
}
借助于动画所设置的减速插值器 DecelerateInterpolator,value取值的增速将逐渐减小,即这里miliseconds取值的增速逐渐减小。
虽然速度值mSpeedX、mSpeedY固定,但是计算出的位置轨迹可以是一个减速运动的效果。
06
总结
06
总结
使用Canvas绘制高帧率的粒子动效,提供了动画的一种实现方案,虽然轨迹的计算有些繁琐。当然,易用性肯定也是Canvas的优势了。并且属性动画提了种类丰富的插值器 Interpolator,除本文提到的还有弹跳插值器、回荡秋千插值器等等,另外还支持自定义插值器,这成为定制复杂动画运动轨迹的关键。实际需求中可以根据需要设计组装成不同的动画效果,粒子喷泉,屏幕气泡、镜头由远及近等等,快来试试吧。