篇章目标要点
最近看到酷我音乐App出了一则《穹顶流星》动效,看完之后决定自己尝试一下实现,本文将围绕通过自定义View实现流星雨效果,可以看到流星雨环绕专辑图的高级动效。通过完成这项开发,能够更深的理解自定义View。
实现效果
先上图看下效果,中间设置的是外部传入的图片,四周是通过自定义View实现的流星效果
核心设计思路
1.裁剪ImageView原图获得圆形图片
这部分的基本思路是先将原图缩放至与目标大小合适的尺寸,然后进行裁剪。 其关键步骤是需要设置相交取背景,这部分有较多人员以及整理了相关资料,详情如下。其中SRC表示背景图,DST表示前景图。
2.旋转圆形图片
在onDraw中处理图片都是基于Bitmap类型对象进行操作的,而Java提供了Matrix用法处理Bitmap旋转,在这里虽然我们需要的是处理后的圆形效果的图片,但是图片的画布并非是圆形,而是矩形,只是其背景是透明色而已,因此旋转过程中,会旋转最合适的大小来容纳原图,故处理之后的图片的宽高是变化的,如下图所示,旋转过程中会产生补充区。
3.绘制流星
一个静态的流星的基本构成包含流星本体是一个圆形,流星的运动轨迹是弧线两个部分构成
这个只是静态的流星,如果要实现动态流星的效果,则需要通过以下3个参数保证
序号 | 参数 | 参数要求 |
---|
1 | 圆弧终点角 | 圆弧的终点角 = 起点角 + 扫过角度,其值应当呈现顺时针方向变化,才能呈现旋转效果 | 2 | 圆弧扫过角度 | 要实现流星拖尾和消亡两个阶段,则扫过角度应当线性增加或减少,减少至0后回收 | 3 | 圆弧颜色 | 圆弧颜色应当有渐变效果,能够体现拖尾的光减弱效果 |
关键代码说明
1.首先创建一个内部类实现流星对象 ,其内部定义了起点角度/扫过角度/圆弧半径/流星头部的圆心横坐标/流星头部的纵坐标 等几个主要的要素信息
/**
* 流星对象,主要由起点角度/扫过角度/圆弧半径/流星头部的圆心横坐标/流星头部的纵坐标 5个要素构成
*/
private class FallingStar{
//流星规则的渐变色
private final int[] SWEEP_COLORS = new int[]{Color.TRANSPARENT, 0xFFE0E0E0};
private final float[] POSITIONS = new float[]{0.2f, 0.8f};
private int startAngle;
private int sweepAngle;
private int radius;
//流星头部圆形的横纵坐标
private int starCenterX;
private int starCenterY;
private SweepGradient shader;
private RectF rect;
//true表示流星轨迹,处于增长期;反正为衰减期
private boolean rise;
//流星旋转速度
private int velocity;
public int getStartAngle() {
return startAngle;
}
public FallingStar setStartAngle(int startAngle) {
this.startAngle = startAngle;
return this;
}
public int getSweepAngle() {
return sweepAngle;
}
public FallingStar setSweepAngle(int sweepAngle) {
this.sweepAngle = sweepAngle;
return this;
}
public int getRadius() {
return radius;
}
public FallingStar setRadius(int radius) {
this.radius = radius;
return this;
}
public int getStarCenterX() {
return starCenterX;
}
public int getStarCenterY() {
return starCenterY;
}
public SweepGradient getShader() {
return shader;
}
public RectF getRect() {
return rect;
}
public FallingStar build(){
//计算流星头部的圆心坐标
double endAngle = 2 *Math.PI*(getStartAngle()+getSweepAngle()) / 360 ;
starCenterX = (int)(radius*Math.cos(endAngle)) + width/2;
starCenterY = (int)(radius*Math.sin(endAngle)) + height/2;
shader = new SweepGradient(width/2, height/2, SWEEP_COLORS, POSITIONS);
rect = new RectF(width/2 - getRadius(), height/2 - getRadius(), width/2 + getRadius(), height/2 + getRadius());
rise = true;
velocity = mRandomInt.nextInt(2) + 2;
return this;
}
/**
* 调整流星起点角度和扫过角度,起点角度的算法是逆时针匀速调整,扫过角度的算法是阈值及以上匀速减少至0,阈值以下匀速增加至阈值
*/
public void changeAngle(){
startAngle +=velocity;
startAngle %= 360;
if(rise){
sweepAngle ++;
if(sweepAngle > MAX_SWEEP_ANGLE){
rise = false;
sweepAngle = MAX_SWEEP_ANGLE;
}
}else{
sweepAngle --;
if(sweepAngle <= 0){
rise = true;
sweepAngle = 0;
mFallingStarList.remove(this);
}
}
//相应的调整圆头的位置
double endAngle = 2 *Math.PI*(getStartAngle()+getSweepAngle()) / 360 ;
starCenterX = (int)(getRadius()*Math.cos(endAngle)) + width/2;
starCenterY = (int)(getRadius()*Math.sin(endAngle)) + height/2;
}
}
2.然后熟悉一下单个流星的绘制,主要就是绘制一个圆形和一个弧线
/**
* 绘制单个流星包括绘制星星头部的圆形和绘制流星轨迹的渐变线
* @param canvas
* @param fallingStar
*/
private void drawFallingStar(Canvas canvas, FallingStar fallingStar){
//绘制流星头部圆形
canvas.drawCircle(fallingStar.getStarCenterX(), fallingStar.getStarCenterY(), 2 , starPaint);
//绘制轨迹线圆弧,设置弧线为渐变色
fallingLinePaint.setShader(fallingStar.getShader());
canvas.drawArc(fallingStar.getRect(), fallingStar.getStartAngle(), fallingStar.getSweepAngle(), false, fallingLinePaint);
}
3.绘制流星群组和补充流星的过程
/**
* 绘制流星群组
*/
private void drawFallingStarGroup(Canvas canvas){
//剩余流星不及原来总数50%时补充流星
if(mFallingStarList.size() <= 0){
initFallingStarAngle();
}else if(mFallingStarList.size() <= (FALLING_STAR_GROUP_SIZE/2)){
addFallingStar();
}
//绘制流星群组
for(int i = 0; i < mFallingStarList.size(); i++){
drawFallingStar(canvas, mFallingStarList.get(i));
}
}
/**
* 调整流星群组的起点角度和扫过角度,起点角度的算法是逆时针匀速调整,扫过角度的算法是阈值及以上匀速减少至0,阈值以下匀速增加至阈值
*/
private void changeFallingStarAngle(){
for(int i = 0; i < mFallingStarList.size(); i++){
mFallingStarList.get(i).changeAngle();
}
//调整中心图片的旋转角度,设置进行逆时针旋转
mRotateAngle -= 2;
mRotateAngle = (mRotateAngle + 360) % 360;
invalidate();
}
补充流星的详细代码
/**
* 在流星数量减少并处于衰竭状况下,补充流星个数,以确保整体的可观性
*/
private void addFallingStar(){
int additionSize = FALLING_STAR_GROUP_SIZE - mFallingStarList.size();
int starPathNo = 1;
int beginRandomAngle = mRandomInt.nextInt(360);
//计算原有的轨道总层数,将待补充的流星分配至原各层级轨道
int starPathCount = FALLING_STAR_GROUP_SIZE / (360 / MAX_SWEEP_ANGLE);
if((FALLING_STAR_GROUP_SIZE % (360 / MAX_SWEEP_ANGLE)) > 0){
starPathCount++;
}
//每一层待分配的流星个数
int starCountPerPath = additionSize / starPathCount;
for(int i = 0; i < additionSize; i++){
starPathNo = i / starCountPerPath + 1;
FallingStar star = new FallingStar().setRadius(mInsideImageRadius + starPathNo * 10)
.setStartAngle(i % (360 / MAX_SWEEP_ANGLE) * MAX_SWEEP_ANGLE + (starPathNo - 1) * 360 / PATH_COUNT_MAX + beginRandomAngle).setSweepAngle(mRandomInt.nextInt(MAX_SWEEP_ANGLE/6) + 5).build();
mFallingStarList.add(star);
}
}
4.裁剪获得圆形图片的过程,首先需要对原图进行适当的缩放,然后创建画图准备绘制输出后的图片,绘制圆形与缩放后的图片相交,取相交的背景即获得了圆形的图片
private Bitmap createRoundBitmap(Bitmap inBitmap){
Bitmap tempBitmap;
//判断是否需要进行缩放
if(inBitmap.getWidth() == (2 * mInsideImageRadius) && inBitmap.getHeight() == inBitmap.getWidth()){
tempBitmap = inBitmap;
}else {
tempBitmap = Bitmap.createScaledBitmap(inBitmap, 2*mInsideImageRadius, 2*mInsideImageRadius, false);
}
//创建待输出图片的画布
Bitmap result = Bitmap.createBitmap(tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
//设置画布透明
canvas.drawColor(0x00FFFFFF);
Paint paint = new Paint();
paint.setAntiAlias(true);
//绘制要裁剪的圆形
canvas.drawCircle(tempBitmap.getWidth()/2, tempBitmap.getHeight()/2, mInsideImageRadius, paint);
//设置相交模式
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
Rect rect = new Rect(0, 0, tempBitmap.getWidth(), tempBitmap.getHeight());
canvas.drawBitmap(tempBitmap, rect, rect, paint);
canvas.setBitmap(null);
tempBitmap.recycle();
return result;
}
5.对圆形图片设置旋转和初始化流星群组
@Override
protected 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 = createRoundBitmap(bitmap);
//通过Matrix设置圆形Bitmap旋转
mMatrix.reset();
mMatrix.setRotate(mRotateAngle);
//获取旋转后的Bitmap
Bitmap 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);
//绘制流星
drawFallingStarGroup(canvas);
//33ms后更新流星位置
postDelayed(new Runnable() {
@Override
public void run() {
changeFallingStarAngle();
}
}, 33);
//回收过程中Bitmap
roundBitmap.recycle();
}
6.初始化流星群组 这步骤的主要目标是使流星群组相对均匀的分布在轨道上,间距始终,避免产生混乱感
@Override
protected 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;
//计算可设置的流星雨最大个数
PATH_COUNT_MAX = (mOutSideStarRadius - mInsideImageRadius) / PATH_INTERVAL_FALLING_STAR + 1;
FALLING_STAR_GROUP_SIZE = PATH_COUNT_MAX * 360 / MAX_SWEEP_ANGLE;
//初始化流星群组
initFallingStarAngle();
}
/**
* 初始化流星群组的角度和半径参数
*/
private void initFallingStarAngle(){
mFallingStarList.clear();
int starPathNo = 1;
int beginRandomAngle = mRandomInt.nextInt(360);
for(int i = 0; i < FALLING_STAR_GROUP_SIZE; i++){
starPathNo = i / (360 / MAX_SWEEP_ANGLE) + 1;
FallingStar star = new FallingStar().setRadius(mInsideImageRadius + starPathNo * PATH_INTERVAL_FALLING_STAR)
.setStartAngle(i % (360 / MAX_SWEEP_ANGLE) * MAX_SWEEP_ANGLE + (starPathNo - 1) * 360 / PATH_COUNT_MAX + beginRandomAngle).setSweepAngle(mRandomInt.nextInt(MAX_SWEEP_ANGLE/6) + 5).build();
mFallingStarList.add(star);
}
}
学习心得
以上是初步实现的流星雨动效过程,并且实现了流星雨环绕圆形专辑图封面旋转。过程效果还存在部分改进空间,后续会进一步完善。如需要源码,请提供邮箱留言
|