效果如下
1.动画分析:
根据复杂动画分解成简单的动画
上面的两个动画可以分解成最外层扩展动画以及里面六边形动画
最外层动画分解:
一个六边形从中心出发到外边,然后到另一个角,然后收回。里面的动画可以遮挡外边的六边形,最里面的六边形路径动画被里面六边形黑色背景遮挡。
中心六边形动画:
可以分成4层,最下层为六边形半透明背景,第二层为风扇或者粒子,第三层为六边形扫描动画,最上层为百分比
2.动画实现
电量
方案1:使用图片 计算文字位置,通过绘制Bitmap实现 优点:可以实现奇特文字效果
方案2:绘制文字 计算文字位置,绘制文字 优点:1.修改方便。2.占用内存小。
选用方案二
String levelStr = String.valueOf(mBatteryLevel);
Rect levelRect = new Rect();
mBatteryPaint.setTextSize(mBatteryTextSize);
mBatteryPaint.getTextBounds(levelStr, 0, levelStr.length(), levelRect);
Rect percentRect = new Rect();
mBatteryPaint.setTextSize(mBatteryTextSize/2);
mBatteryPaint.getTextBounds(mPercentStr, 0, mPercentStr.length(), percentRect);
float space = mBatteryTextSize/5;
float drawX = mCenter.x-(levelRect.width()+percentRect.width()+space)/2f;
float drawY = mCenter.y+levelRect.height()/2f;
mBatteryPaint.setTextSize(mBatteryTextSize);
canvas.drawText(levelStr,drawX, drawY, mBatteryPaint);
mBatteryPaint.setTextSize(mBatteryTextSize/2);
canvas.drawText(mPercentStr,drawX+levelRect.width()+space, drawY, mBatteryPaint);
六边形背景
方案1:使用图片 计算六边形位置,通过绘制Bitmap实现 优点:只需要计算图片位置,实现快
方案2:绘制六边形路径 计算六边形的顶点,根据顶点绘制实心六边形 优点:1.修改方便。2.占用内存小。
六边形顶点位置计算如下
六边形背景顶点路径代码如下:
final PointF regularHexagonBg = new PointF(mCenter.x+mRadius*mBackgroundRatio, mCenter.y);
for(int index=0;index<POLYGON_COUNT;index++){
if(index==0){
mRegularHexagonBgPath.reset();
mRegularHexagonBgPath.moveTo(startBgPoint.x, startBgPoint.y);
}
PointF point = Util.rotatePoint(regularHexagonBg, mCenter, START_ANGLE+ANGLE*index);
mRegularHexagonBgPath.lineTo(point.x, point.y);
if(index==POLYGON_COUNT-1){
mRegularHexagonBgPath.lineTo(startBgPoint.x, startBgPoint.y);
mRegularHexagonBgPath.close();
}
}
六边形扫描动画
方案1:使用图片 提供扫描背景图片和蒙版图片,旋转背景图片+蒙版实现 优点:只需要计算图片位置和旋转角度,实现快 方案2:绘制六边形路径 使用SweepGradient+CornerPathEffect的画笔绘制路径 优点:1.修改方便。2.占用内存小。
定义SweepGradient+CornerPathEffect的画笔
private final int[] mSweepColors = new int[]{
Color.argb(255, 22, 72, 126),
Color.argb(255, 26, 109, 189),
Color.argb(255, 25, 157, 238),
Color.argb(255, 9, 223, 235),
Color.argb(255, 23, 223, 205),
Color.argb(255, 31, 223, 175),
Color.argb(255, 40, 223, 145),
};
mRegularHexagonGradient = new SweepGradient(mCenter.x, mCenter.y, mSweepColors, null);
mRegularHexagonPaint.setShader(mRegularHexagonGradient);
mRegularHexagonPaint.setPathEffect(new CornerPathEffect(mRadius/10));
根据进度旋转SweepGradient然后绘制路径
mRegularHexagonMatrix.reset();
mRegularHexagonMatrix.setRotate(mScanProgress*360, mCenter.x, mCenter.y);
mRegularHexagonGradient.setLocalMatrix(mRegularHexagonMatrix);
canvas.drawPath(mRegularHexagonPath, mRegularHexagonPaint);
风扇动画
方案1:使用图片 提供风扇图片,绘制旋转的风扇图片实现 优点:只需要计算图片位置和旋转角度,实现复杂度小
方案2:绘制风扇叶路径 使用LinearGradient的画笔绘制风扇叶路径 优点:1.修改方便。2.占用内存小。
LinearGradient的画笔定义
mInitAngle = mRandom.nextInt(360);
mRegularHexagon = new PointF(mCenter.x+mRadius, mCenter.y);
mGradient = new LinearGradient(mCenter.x, mCenter.y,
mCenter.x, mCenter.y-mRegularHexagonMinRadius,
new int[]{Color.argb(150, 0, 255, 185), Color.argb(0, 255, 255, 255)}
, new float[]{0, 0.85f}, Shader.TileMode.CLAMP);
mAirFanPaint.setDither(true);
mAirFanPaint.setAntiAlias(true);
mAirFanPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mAirFanPaint.setShader(mGradient);
计算扇叶路径与外边六边形路径的交集并绘制
final float[] src = new float[]{
mCenter.x, mCenter.y+mRadius, mCenter.x, mCenter.y, mCenter.x+mRadius,
mCenter.y+mRadius, mCenter.x+mRadius, mCenter.y
};
final float[] dst = new float[]{
mCenter.x, mCenter.y+mRadius/2, mCenter.x, mCenter.y, mCenter.x+mRadius,
mCenter.y+mRadius, mCenter.x+mRadius, mCenter.y
};
for(int index = 0;index<FAN_COUNT; index++){
final float angle = mAngle+index*360/FAN_COUNT;
mMatrix.reset();
mMatrix.setPolyToPoly(src, 0, dst, 0, 4);
mMatrix.setRotate(angle, mCenter.x, mCenter.y);
mGradient.setLocalMatrix(mMatrix);
final double startAngle = Math.PI*2*angle/360F;
PointF startPoint = Util.rotatePoint(mRegularHexagon, mCenter, startAngle);
PointF endPoint = Util.rotatePoint(startPoint, mCenter, mRotateAngle);
mAirFanPath.reset();
mAirFanPath.moveTo(mCenter.x, mCenter.y);
mAirFanPath.lineTo(startPoint.x, startPoint.y);
mAirFanPath.addArc(mRectF, angle, mFanRotateAngle);
PointF rationPoint= Util.ratioPoint(mCenter, endPoint, 0.65f);
mAirFanPath.lineTo(rationPoint.x, rationPoint.y);
mAirFanPath.lineTo(mCenter.x, mCenter.y);
mAirFanPath.close();
mAirFanPath.op(mRegularHexagonPath, Path.Op.INTERSECT);
canvas.drawPath(mAirFanPath, mAirFanPaint);
}
canvas.drawCircle(mCenter.x, mCenter.y, 10, mAirFanPaint);
粒子动画
方案1:使用图片 通过代码绘制一张粒子图片,根据粒子位置与角度转换Matrix绘制图片 优点:速度快一点点 方案2:绘制粒子路径 通过PathMeasure切片路径,绘制不同粗细路径实现粒子效果 优点:占用内存小一点
随机生成粒子的位置与路径,以及它的生命周期,生命起始时间
private void reset(boolean init, long currentTime){
final int halfRadius = (int) (mRadius/2f);
final int randomInt = mRandom.nextInt(halfRadius*10)/10;
final int mAngle = mRandom.nextInt(3600)/10;
PointF point = new PointF(mCenter.x+randomInt+halfRadius-(mRadius-mRegularHexagonMinRadius)/2, mCenter.y);
mStartPointF = Util.rotatePoint(point, mCenter, mAngle);
point = new PointF(mCenter.x+(randomInt+halfRadius)/5, mCenter.y);
mEndPointF = Util.rotatePoint(point, mCenter, mAngle);
mPath = new Path();
mPath.moveTo(mStartPointF.x, mStartPointF.y);
mPath.lineTo(mEndPointF.x , mEndPointF.y);
lifeTime = (long) (mRandom.nextInt(500)*mParticleLifeTime/2000f+mParticleLifeTime);
mPathMeasure = new PathMeasure(mPath, false);
mPathLenth = mPathMeasure.getLength();
mParticleProgress = (init?mRandom.nextInt(10000):0)/10000f;
mLiveStartTime = init? (currentTime-(long)(lifeTime*mParticleProgress)):currentTime;
}
先生成一张粒子图片Bitmap
private Bitmap getParticleBitmap(int width, int height){
Bitmap particleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
if(width > height){
float radius = height/3f;
int startColor = PARTICLE_COLOR;
int endColor = startColor & 0x44ffffff;
Paint paint = new Paint(Paint.FAKE_BOLD_TEXT_FLAG);
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setMaskFilter(new BlurMaskFilter(radius*0.7f, BlurMaskFilter.Blur.NORMAL));
paint.setDither(true);
paint.setShader(new LinearGradient(width, 0, 0, 0,
new int[]{startColor, endColor} , null, Shader.TileMode.CLAMP));
PointF leftPoint = new PointF(0, height/2f);
PointF rightPoint = new PointF(width-height/2, height/2f);
double dAngle = Math.acos(radius/(width-height));
Path path = new Path();
path.moveTo(leftPoint.x, leftPoint.y);
PointF nextPoint = Util.rotatePoint(new PointF(rightPoint.x-radius, rightPoint.y), rightPoint,dAngle);
path.lineTo(nextPoint.x, nextPoint.y);
float startAngle = (float) (dAngle*180/Math.PI)+180;
float sweepAngle = (360-startAngle)*2;
RectF oval = new RectF(rightPoint.x-radius, rightPoint.y-radius, rightPoint.x+radius, rightPoint.y+radius);
path.addArc(oval, startAngle, sweepAngle);
path.lineTo(leftPoint.x, leftPoint.y);
path.close();
Canvas canvas = new Canvas(particleBitmap);
canvas.drawPath(path, paint);
}
return particleBitmap;
}
方案二中使用路径绘制方式关键代码
final float length = endLeng-startLeng;
final int segmentSize = (int) Math.ceil(length / DEFAULT_SEGMENT_LENGTH);
float startD = startLeng;
float stopD;
for(int index=0; index<segmentSize; index++){
float itemRogress = ((float)index)/segmentSize;
stopD = startLeng+(index+1)*DEFAULT_SEGMENT_LENGTH;
if(stopD > endLeng){
stopD = endLeng;
}
mDstPath.reset();
boolean segment = mPathMeasure.getSegment(startD, stopD, mDstPath, true);
if(segment){
mParticlePaint.setStrokeWidth((1-itemRogress)*(particleSize-0.1f)+0.1f);
mParticlePaint.setAlpha((int) ((isInit?itemRogress:(1-itemRogress))*(particleAlpha-20)+20));
canvas.drawPath(mDstPath, mParticlePaint);
}
}
外部线条移动动画
方案1:使用图片 通过帧动画 优点:实现快
方案2:计算路径 通过PathMeasure切片路径,以及通过Matrix缩放Canvas实现 优点:内存小很多
该动画实际为正方体框被两个平行面切出的边框 实现方式: 光线行走的路径如右图。当内部的光线路径扩展后会挡住外部的光线路径。
根据此思路,1.计算路径。2.在绘制时候根据内部的扩展计算可见区域。3.计算光线应该的位置与刚才计算可见区域的交集路径。4.绘制路径
路径代码
private void initPath(PointF regularHexagon, double angle){
mPoint = Util.rotatePoint(regularHexagon, mCenter, angle);
double gap = Math.asin(mLineWidth*0.492f/mRadius);
mLeftPoint = Util.rotatePoint(regularHexagon, mCenter, angle-ANGLE+(USE_CLIP_PATH?0:gap));
mRightPoint = Util.rotatePoint(regularHexagon, mCenter, angle+ANGLE-(USE_CLIP_PATH?0:gap));
mLeftPath.reset();
mLeftPath.moveTo(mCenter.x, mCenter.y);
PointF lp = Util.rotatePoint(mPoint, mCenter, -(USE_CLIP_PATH?0:gap));
mLeftPath.lineTo(lp.x, lp.y);
mLeftPath.lineTo(mLeftPoint.x, mLeftPoint.y);
mLeftPath.lineTo(mCenter.x, mCenter.y);
mLeftPath.close();
mLeftPathMeasure = new PathMeasure(mLeftPath, false);
mLeftPathLeng = mLeftPathMeasure.getLength();
mRightPath.reset();
mRightPath.moveTo(mCenter.x, mCenter.y);
PointF rp = Util.rotatePoint(mPoint, mCenter, (USE_CLIP_PATH?0:gap));
mRightPath.lineTo(rp.x, rp.y);
mRightPath.lineTo(mRightPoint.x, mRightPoint.y);
mRightPath.lineTo(mCenter.x, mCenter.y);
mRightPath.close();
mRightPathMeasure = new PathMeasure(mRightPath, false);
mRightPathLeng = mRightPathMeasure.getLength();
}
计算应该绘制的路径
public void addToPath(Path path) {
float startProgress = mMeteorProgress;
float endProgress = startProgress + mSpaceRatio;
final float littleScale = getLittleScale();
final float start = littleScale/(mCanvasScale*3);
final float end = (mCanvasScale*3-littleScale)/(mCanvasScale*3);
if(startProgress < start){
startProgress = start;
}
if(endProgress > end ){
endProgress = end;
}
if(endProgress > startProgress){
float startLeng = startProgress*mLeftPathLeng;
float endLeng = endProgress*mLeftPathLeng;
mLeftPathMeasure.getSegment(startLeng, endLeng, path, true);
startLeng = startProgress*mRightPathLeng;
endLeng = endProgress*mRightPathLeng;
mRightPathMeasure.getSegment(startLeng, endLeng, path, true);
}
}
public void onDraw(Canvas canvas) {
int saveCount = canvas.save();
Matrix m = new Matrix();
m.setScale(mCanvasScale, mCanvasScale,mCenter.x, mCenter.y);
canvas.setMatrix(m);
if(USE_CLIP_PATH){
canvas.clipPath(mFullPath);
}
mDrawPath.reset();
for(RadiancePath radiance :mRadiances ){
radiance.addToPath(mDrawPath);
}
canvas.drawPath(mDrawPath, mRadiancePaint);
canvas.restoreToCount(saveCount);
}
所有的动画都实现完了,然后调整下代码就打工完成。