篇章目标介绍

之前看到网易云,酷我音乐都发布过用于播放器页面粒子动效的效果,于是打算自己也动手做一个,产品目标是对标酷我手机app的动效设计,实现过程完全基于自身的推测理解予以实现。计划通过两次完全完成这个项目,第一篇文章重点介绍粒子动效实现的核心问题和完成效果的主要代码介绍;计划在第二篇文章针对粒子动效的资源占用进行优化和完善UI展示效果。本文是第一篇文章

粒子动效核心问题

粒子动效的是需要模拟太阳光扩散的效果,要体现粒子运动的规律和源源不断的粒子发散的效果。要达成目标,重点需要认识以下4个问题

问题1:移动方向

如下图示意,粒子的运动方向需要按照法线方向向外扩散,且扩散过程中,粒子的透明度alpha呈现扩散趋势。需要在粒子信息中记录粒子半径,圆形x坐标,圆形y坐标,透明度alpha,运动速度

问题2:粒子分布

粒子需要较为均匀的从光源外圈发射,但是粒子分布又不宜绝对均匀分布,要体现一定随机性。因此基本思路是等分圆弧角度,在划定弧度范围内释放一组粒子,粒子位置在弧度范围内随机。如果希望粒子密度增大,只需增加一组粒子的个数值即可。这样即可兼顾均匀分布性和随机性两个特征要求

问题3:移动速度

为了保证同一时间发射的粒子扩散范围具有一定相关性,因为速度随机值差异不宜太大。速度的计算容易理解,但是执行过程中移动过程中速度的概念容易被误解,比如误将x向移动量等效为速度,这种错误等效会导致粒子扩散过程无法保持粒子星云效果,速度应当是x向和y向变化量的合成值。计算如下:

问题4:粒子补充

粒子补充是模拟太阳光发射需要考虑的一个现象,即光源是源源不断的在发射光子,而非待粒子回收到一定程度才补充。这个是容易走错的误区。

其他诸如粒子超出边界回收属于常规手段,在此不重点介绍

实现效果

效果图1

效果图2

效果图3

主要源码介绍

源码包括数据对象和绘制逻辑两部分构成。

数据实体

首先是粒子和气泡的数据对象,因为其本质都是画圆,因此定义了一个抽象接口用于粒子和气泡实现。

/*** 用于生产圆形的工厂接口类,气泡/粒子均实现此接口*/
public interface CircleFactory<T extends CircleFactory> {//设置圆形x坐标T setX(int x);//设置圆形y坐标T setY(int y);//构建形状T build();//改变形状信息void change();//从集合中移除形状void remove();
}

下边是粒子的数据实体,主要包括粒子的信息构成,粒子信息更新(坐标和透明度更新),粒子超出边界移除

    /*** 粒子信息由半径,x坐标,y坐标,alpha透明度,velocity移动速度构成* 半径范围控制在1~2以内* alpha呈现减少趋势*/private class Particle implements CircleFactory<Particle>{private int r;private int x;private int y;private int alpha;private int velocity;//斜率private float slope;//方向,x向增加为1,x向减小为-1private int direction;//粒子寿命,velocity降为0或者超出粒子云外径视为寿命终结private int age;@Overridepublic Particle setX(int x) {this.x = x;return this;}@Overridepublic Particle setY(int y) {this.y = y;return this;}public int getR() {return r;}public int getX() {return x;}public int getY() {return y;}public int getAlpha() {return alpha;}public float getSlope() {return slope;}public int getDirection() {return direction;}public int getVelocity() {return velocity;}public int getAge() {return age;}public void setR(int r) {this.r = r;}public void setAlpha(int alpha) {this.alpha = alpha;}public void setVelocity(int velocity) {this.velocity = velocity;}public void setSlope(float slope) {this.slope = slope;}public void setDirection(int direction) {this.direction = direction;}public void setAge(int age) {this.age = age;}@Overridepublic Particle build() {r = mRandomInt.nextInt(1) + 1;alpha = 255;velocity = mRandomInt.nextInt(3) + 3;slope = 1.0000f*(y - height/2) / (x - width/2);direction = (x >= width/2 ? 1 : -1);return this;}/*** 改变该粒子的x,y坐标*/@Overridepublic void change() {float sqrt = (float) (1.0000f / Math.sqrt(1 + Math.pow(slope, 2)));x += direction*velocity *sqrt;y += direction*velocity*slope *sqrt;alpha -=30;ageEnd();}/*** 粒子寿命终结时移除*/public void ageEnd(){boolean exceedingBoarder = (Math.pow(x - width/2, 2) + Math.pow(y - height/2, 2) - Math.pow(mOutSideStarRadius, 2) >= 0.001f);boolean moveState = (velocity > 0);boolean visible = (alpha > 0);if(exceedingBoarder | !moveState | !visible){remove();}}@Overridepublic void remove(){mParticleList.remove(this);}}

气泡数据实体因为与粒子基本相同,只是信息更新和移除有所差异,其思路是继承粒子实体,重写更新和移除方法

    /*** 气泡的整体元素复用粒子,差异点在于透明度偏低,半径较大,调整速度较慢*/private class Bubble extends Particle{@Overridepublic Bubble build() {setR(mRandomInt.nextInt(10) + 5);setAlpha(100);setVelocity(mRandomInt.nextInt(2) + 2);setSlope(1.0000f*(getY() - height/2) / (getX() - width/2));setDirection(getX() >= width/2 ? 1 : -1);return this;}@Overridepublic void change() {float sqrt = (float) (1.0000f / Math.sqrt(1 + Math.pow(getSlope(), 2)));setX((int)(getX() + getDirection() * getVelocity() * sqrt));setY((int)(getY() + getDirection() * getVelocity() * getSlope() * sqrt));setAlpha(getAlpha() - 10);ageEnd();}@Overridepublic void remove() {mBubbleList.remove(this);}}

主要逻辑

主要逻辑即是围绕上述的4个核心问题展开
首先是测量,估算计划投放的粒子组数量。然后初始化粒子群和气泡群对象

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);width = getMeasuredWidth();height = getMeasuredHeight();//计算流星最外轨道半径mOutSideStarRadius = Math.min(width, height) / 2 * 9 / 10;//计算中心原图的半径mInsideImageRadius = mOutSideStarRadius * 2 / 3;//计算适合的粒子群个数mAlbumCircleRadius = (mInsideImageRadius + 20) < mOutSideStarRadius ? (mInsideImageRadius + 20) : (mInsideImageRadius + (int)mCirclePaint.getStrokeWidth()/2);//平均间隔10个像素的圆环分配一个粒子组PARTICLE_GROUP_SIZE = (int)(mAlbumCircleRadius * 2 *Math.PI / 10);//初始化粒子群initParticleGroup(PARTICLE_GROUP_SIZE);//初始化气泡群initBubbleGroup(PARTICLE_GROUP_SIZE / 60);}

粒子群的初始化,位置更新的逻辑如下

    //添加粒子群private void initParticleGroup(int size){//每一个起始的粒子占据的角度范围float angleInterval = 1.0f * 360 / size;//粒子所处角度float angle;for(int i = 0; i < size; i++){for(int j = 0; j < PARTICLE_SIZE; j++){angle = (i * angleInterval + mRandomInt.nextInt(10)*angleInterval / 10)*1.0f;int x = (int)(Math.cos(2 * Math.PI * angle / 360) * mAlbumCircleRadius) + width/2;int y = (int)(Math.sin(2 * Math.PI * angle / 360) *mAlbumCircleRadius) +height/2;Particle particle = new Particle().setX(x).setY(y).build();mParticleList.add(particle);}}}/*** 绘制粒子群* @param canvas 画布*/private void drawParticleGroup(Canvas canvas){for(int i = 0; i < mParticleList.size(); i++){Particle particle = mParticleList.get(i);mParticlePaint.setAlpha(particle.getAlpha());canvas.drawCircle(particle.getX(), particle.getY(), particle.getR(), mParticlePaint);}}/*** 改变粒子群信息*/private void changeParticleGroup(){for(int i = 0; i < mParticleList.size(); i++){mParticleList.get(i).change();}//调整中心图片的旋转角度,设置进行逆时针旋转mRotateAngle -= 2;mRotateAngle = (mRotateAngle + 360) % 360;invalidate();}

气泡群的初始化/更新的逻辑如下

    /*** 添加气泡群*/private void initBubbleGroup(int size){//每一个起始的气泡占据的角度范围float angleInterval = 1.0f * 360 / size;//气泡所处角度float angle;for(int i = 0; i < size; i++){angle = (i * angleInterval + mRandomInt.nextInt(10)*angleInterval / 10)*1.0f;int x = (int)(Math.cos(2 * Math.PI * angle / 360) * mAlbumCircleRadius) + width/2;int y = (int)(Math.sin(2 * Math.PI * angle / 360) *mAlbumCircleRadius) +height/2;Particle particle = new Bubble().setX(x).setY(y).build();mBubbleList.add(particle);}}/*** 改变气泡群信息*/private void changeBubbleGroup(){for(int i = 0; i < mBubbleList.size(); i++){mBubbleList.get(i).change();}}private void drawBubbleGroup(Canvas canvas){for(int i = 0; i < mBubbleList.size(); i++){Particle particle = mBubbleList.get(i);mBubblePaint.setAlpha(particle.getAlpha());canvas.drawCircle(particle.getX(), particle.getY(), particle.getR(), mBubblePaint);}}

最后一步是绘制的业务实现,需要完成5个工作,(1)绘制中间的圆形专辑图并且实现逆时针旋转;(2)绘制专辑外侧的光晕圆形;(3)绘制粒子群;(4)绘制气泡群;(5)定义刷新View(粒子和气泡变更状态,专辑图旋转)。其中生成圆形专辑图上一篇流星动效的文章已经介绍,本次不再提供。

    @Overrideprotected void onDraw(Canvas canvas) {//设置画布透明canvas.drawARGB(0,0,0,0);//绘制中间的圆形图片Drawable drawable = getDrawable();if(null == drawable){return;}//将ImageView的原图裁剪成圆形图片Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();Bitmap roundBitmap = RoundBitmapUtil.createRoundBitmap(bitmap, mInsideImageRadius);//通过Matrix设置圆形Bitmap旋转mMatrix.reset();mMatrix.setRotate(mRotateAngle);//获取旋转后的BitmapBitmap rotateBitmap = Bitmap.createBitmap(roundBitmap, 0, 0, 2*mInsideImageRadius, 2*mInsideImageRadius, mMatrix, false);//在画布上绘制旋转后的Bitmap,注意基于Matrix旋转后的Bitmap与原图的大小并不相等,故计算中心位置时应以转换后的Bitmap进行计算canvas.drawBitmap(rotateBitmap, width / 2 - rotateBitmap.getWidth()/2 , height / 2 - rotateBitmap.getHeight()/2, null);//提取主题色int colorTheme = BitmapColorUtil.extractColor(bitmap);mCirclePaint.setColor(colorTheme);mParticlePaint.setColor(colorTheme);mBubblePaint.setColor(colorTheme);//绘制专辑图外围的圆环canvas.drawCircle(width/2, height/2, mAlbumCircleRadius, mCirclePaint);//绘制粒子群drawParticleGroup(canvas);//绘制气泡群drawBubbleGroup(canvas);//33ms后更新粒子位置和气泡位置postDelayed(new Runnable() {@Overridepublic void run() {changeParticleGroup();changeBubbleGroup();//补充粒子initParticleGroup(PARTICLE_GROUP_SIZE);//补充气泡initBubbleGroup(PARTICLE_GROUP_SIZE / 60);}}, 33);//回收过程中BitmaproundBitmap.recycle();}

学习心得

粒子动效的自定义View核心不在于绘制,而是要理解并处理提到的4个重点课题。由于作者水平有限,可能存在一些纰漏,欢迎交流。

Android自定义View高级动效---粒子动效实现|音乐播放器粒子动效|实现酷我网易云粒子动效相关推荐

  1. Android复习13【广播:思维导图、音乐播放器】

    音乐播放器Android代码下载:https://wws.lanzous.com/ifqzihaxvij 思维导图 https://share.weiyun.com/1vVLYnlb 音乐播放器

  2. Android自定义View高级动效之---安卓流星雨动效|Android流星雨专辑封面

    篇章目标要点 最近看到酷我音乐App出了一则<穹顶流星>动效,看完之后决定自己尝试一下实现,本文将围绕通过自定义View实现流星雨效果,可以看到流星雨环绕专辑图的高级动效.通过完成这项开发 ...

  3. android画布设置最外层,Android自定义View高级(三)-Canvas之画布操作

    一.Canvas简介 Canvas我们可以称之为画布,能够在上面绘制各种东西,是Android平台2D图形绘制的基础. 二.Canvas的常用操作 操作类型 相关API 备注 绘制颜色 drawCol ...

  4. 手机android版本2.3.6可以安装哪个版本的音乐播放器,喜马拉雅fm老版本2.3.6下载...

    喜马拉雅fm2.3.6旧版本是一款能够通过声音就可以让你知晓天下大事的app,喜马拉雅能够将世界电台一网打尽,让你能够听到最新的新闻.最好听的美文.最优美的音乐.你还可以在软件上与电台主播进行互动,说 ...

  5. android 自定义特效,Android自定义View之高仿QQ健康

    我们都知道自定义View一般有三种直接继承View.继承原有的控件对控件的进行修改.重新拼装组合,最后一种主要针对于ViewGroup.具体的怎么做不是本文的所涉及的内容(本文是基于第一种方式实现的) ...

  6. 精通Android自定义View(十九)自定义圆形炫彩加载转圈效果

    1 效果 2 源码 public class JiondongView extends View {private Paint mBackgroundPaint;private float mScal ...

  7. Android之简单本地音乐播放器

    平台:Android studio APK:http://fir.im/apps/56ea5187e75e2d69af000042 本地的音乐播放器,主要功能就是可以播放音乐,能够读取本地的音乐,并显 ...

  8. Android课设:简易音乐播放器

    实验主题 本次课程设计计划实现一个低配版的仿网易云音乐的音乐播放器,主要实现功能如下: 打开APP需先进行注册 已有账号可进行登录 登录后跳转至音乐界面,本地歌曲列表读取本地音乐文件并显示 点击本地音 ...

  9. android音乐播放器实现,Android实现简单音乐播放器(MediaPlayer)

    Android实现简单音乐播放器(MediaPlayer),供大家参考,具体内容如下 开发工具:Andorid Studio 1.3 运行环境:Android 4.4 KitKat 工程内容 实现一个 ...

最新文章

  1. Python之路----迭代器与生成器
  2. POJ-3268-最短路(dijkstra算法)
  3. [html] W3C--span is a nested element.
  4. excel冻结窗口_excel成绩表怎么固定表头或者某一行?
  5. python之字符编码(四)
  6. 我遇到了Hibernate异常
  7. 看故事学Redis:再不懂,我怀疑你是假个开发
  8. c语言录屏软件wps,WPS制作录屏视频
  9. Spring中AOP及ReflectiveMethodInvocation逻辑简析
  10. mysql5.0基础语句_mysql基础语句
  11. python mp4提取音频_Python从视频文件中提取wav
  12. 万字攻略,详解腾讯面试(T1-T9)核心技术点,面试题整理
  13. mybatis:Error preparing statement. Cause: java.lang.NullPointerException
  14. 大厂前端面试都问些什么问题?入职爱奇艺年薪48万,面试经验总结
  15. 利用南十字星座测量经纬度的方法
  16. 分析比较国内几大OTA(Online Travel Agency)的优劣势
  17. 科技公司的域名大战!
  18. 计算机硬件和工作原理,计算机硬件及基本工作原理ppt课件.ppt
  19. 学校关于扩建计算机房的请示,申请装配机房的请示(模版)
  20. equals和==和hashcode的恩怨情仇

热门文章

  1. 扫码登录背后的实现原理
  2. 可知:南京满天飞的“毛絮”竟是虫子!
  3. android视频播放器!毕业一年萌新的Android大厂面经,面试心得体会
  4. 柯尼卡美能达AccurioPress C2070系列全新彩色生产型数字印刷系统重磅登场
  5. 精通 NumPy 数值分析:1~5
  6. 读书笔记-高标管事 低调管人
  7. Web 3D VS Native 3D是未来元宇宙
  8. 显著性检测论文(一)
  9. 英语知识系列:英语单词的可数名词与不可数名词
  10. 基于强化学习的多智能体框架在路由和调度问题中的应用