最近比较忙,烦心的事情也不少。就迷上了一款游戏《守望先锋》,差点就没回来。
言归正传,前些日子看到一个很炫酷的loadingView,看到的时候感觉,这个感觉怎么说呢,用英语说就是amazing(太TM吊了)。
我也仅仅只是通过别人的博客,加上一点自己的理解写的这篇博客,目的是想要和大家分享,顺便记录一下。感觉实现一个这么炫酷的动画还是感觉挺有成就感动的(毕竟菜鸟一枚)。这里先放上原博主的链接,感谢这位大神。这边博客把实现过程已经写的很清晰的建议有些自定义view基础的人,先去看这里先放上原博主的博客,然后自己实现以下,我这里会对整个view的实现过程详细的讲一下。
此次时间有点仓促,没有对代码进行优化,同时也有部分原作者的代码。希望大家谅解,主要是给大家提供一个思路。
先看一下效果图:
怎么样,我没说错吧,第一眼看见就眼前一亮。下面我们就对整个过程进行详细的讲解。
拆分动画
- 和叶子一样颜色的进度条
- 右侧旋转的白色电风扇
- 漂浮的叶子(原博主说的很细致我这里直接引用原话)
1.叶子的随机产生;
2.叶子随着一条正余弦曲线移动;
3.叶子在移动的时候旋转,旋转方向随机,正时针或逆时针;
4.叶子遇到进度条,似乎是融合进入;
5.叶子不能超出最左边的弧角;
7.叶子飘出时的角度不是一致,走的曲线的振幅也有差别,否则太有规律性,缺乏美感; - 最后又一个结束动画,风扇消失,然后“100%”出现
整个动画就是这样子的,难点就是绘制叶子要满足以上的7点。
定义属性
private static final int DEFAULT_BG_OUTER = 0xfffde399; // 外部边框的背景颜色
private static final String DEFAULT_WHITE = "#fffefd";
private static final int DEFAULT_BG_INNER = 0xffffa800; //内部进度条的颜色
private static final String DEFAULT_BG_FAN = "#fcce5b"; // 风扇 扇叶的颜色
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 600;
//振幅的强度
private static final int LOW_AMPLITUDE = 0;
private static final int NORMAL_AMPLITUDE = 1;
private static final int HIGH_AMPLITUDE = 2;
private static final int DEFAULT_AMPLITUDE = 20;
// 叶子飘动一个周期所花的时间
private static final int LEAF_FLY_TIME = 2000;
private static final int LEAF_ROTATE_TIME = 2000;
private Resources mResources;
// 定义画笔
private Paint innerPaint;
private Paint outerPaint;
private Paint fanPaint;
private Paint fanBgPaint;
private Paint textPaint;
// view的大小 和 “100%”的高度
private int mWidth;
private int mHeight;
private float textHeight;
//外部圆半径 内部圆半径 风扇背景的半径
private float outerRadius;
private float innerRadius;
private float fanBgRadius;
//各种路径
private RectF outerCircle;
private RectF outerRectangle;
private RectF innerCircle;
private RectF innerRectangle;
private RectF fanWhiteRect;
//电风扇 扇叶路径
private Path mPath;
private Path nPath;
// 定义结束的属性动画
private ValueAnimator progressAnimator;
private ValueAnimator completedAnimator;
//进度值
private float maxProgress = 100;
private float currentProgress;
private float completedProgress;
//计算时间增量和progress增量
private long preTime ;
private long addTime;
private float addProgress;
private float preProgress;
//先填充半圆的进度 和 长方形的时间
private float firstStepTime;
private float secondStepTime;
//和叶片相关
private Bitmap mLeafBitmap;
private int mLeafWidth;
private int mLeafHeight;
private int mLeafFlyTime = LEAF_FLY_TIME;
private int mLeafRotateTime = LEAF_ROTATE_TIME;
private int mAddTime;
private float mAmplitudeDisparity = DEFAULT_AMPLITUDE;
//判断是否加载完毕 然后执行结束动画
private boolean isFinished;
//精度条的总长度
private float mProgressWidth;
private List leafInfos;
//对 外面的边框缓存
private WeakReference outBorderBitmapCache;
这里定义的属性比较多,但是还是都通熟易懂的。
OnDraw()
我们这先看一下onDraw方法吧,整个的绘制流程是都放生在这个方法里面。我们先梳理一下绘制的流程,具体画每个图形后面我会详细讲解。
protected void onDraw(Canvas canvas) {
//判断背景有没有缓存(这里的背景是指,黄色进度条外面的边框)
Bitmap outBorderBitmap = outBorderBitmapCache == null ? null : outBorderBitmapCache.get();
if (outBorderBitmap == null || outBorderBitmap.isRecycled()) {
outBorderBitmap = getBitmap();
outBorderBitmapCache = new WeakReference(outBorderBitmap);
}
//对画布保存主要是要用Xfermode对图像处理,主要是不想让叶子飞出边界
//如果不了解Xfermode的同学建议先去看一下,很有用的一个东西
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null, Canvas.MATRIX_SAVE_FLAG |
Canvas.CLIP_SAVE_FLAG |
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
Canvas.CLIP_TO_LAYER_SAVE_FLAG);
canvas.drawBitmap(outBorderBitmap, 0, 0, outerPaint);
// canvas.translate(mWidth / 10, mHeight / 2);
//画叶子
drawLeaf(canvas);
//恢复画布
canvas.restoreToCount(sc);
canvas.translate(mWidth / 10, mHeight / 2);
//画内部圆
drawInnerCircle(canvas);
//画风扇白色的背景
canvas.drawArc(fanWhiteRect, 90, 360, true, fanPaint);
//画风扇的黄色背景
canvas.save();
canvas.scale(0.9f, 0.9f, 8 * outerRadius, 0);
canvas.drawArc(fanWhiteRect, 90, 360, true, fanBgPaint);
canvas.restore();
//画扇叶
canvas.save();
drawFan(canvas, true);
canvas.restore();
//结束动画
//结束动画是指 电风扇的扇叶从扇叶变成100%字样
if (isFinished) {
showCompletedText(canvas);
} else {
//这里重新绘制 主要是为了画叶子
invalidate();
}
}
首先我们先说一下画背景(这里的背景指的是进度条外面的边框)
先看一下具体实现
public Bitmap getBitmap() {
//这里先产生一个一个画布,画布的大小就是view的大小
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight,Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.translate(mWidth / 10, mHeight / 2);
canvas.drawArc(outerCircle, 90, 180, true, outerPaint);
canvas.drawRect(outerRectangle, outerPaint);
return bitmap;
}
这里我们让背景作为bitmap返回主要是 要使用Xfermode方法。先移动坐标系到我们想要的位置,然后背景是由一个左半圆和一个矩形组成。至于为什么要使用Xfermode,这里先说明一下,我们期望叶子是不可以飘出背景外的。(就是说叶子飘出背景外的地方要变成透明的)。
画进度条
//先填充半圆
private void drawInnerCircle(Canvas canvas) {
firstStepProgress = innerRadius / (innerRadius + 7 * outerRadius);
if (currentProgress > firstStepProgress) {
canvas.drawArc(innerCircle, 90, 180, true, innerPaint);
drawInnerRectangle(canvas);
} else {
//这里就是绘制半圆的执行(方法是绘制圆弧)
canvas.drawArc(innerCircle, 180 - 90 * currentProgress / firstStepTime, 180 * currentProgress / firstStepTime, false, innerPaint);
}
}
//填充剩下的长方形
private void drawInnerRectangle(Canvas canvas) {
secondStepProgress = 1 - firstStepProgress;
//判断是否结束,结束了会执行结束动画
if (currentProgress >= 1) {
if (!isFinished) {
isFinished = true;
completedAnimator.start();
}
} else {
canvas.drawRect(-1, -innerRadius, 7 * outerRadius * (currentProgress - firstStepProgress) / secondStepProgress, innerRadius, innerPaint);
}
}
进度条和背景是一样的,都是先都前半圆和一个矩形组成的。先计算半圆所占进度,当currentProgress没有超过firstStepProgress时候,先绘制半圆部分,之后绘制矩形。
绘制风扇
参数分别是 canvas 画布,isNeedRotate 风扇是否旋转。
//画扇叶
private void drawFan(Canvas canvas, boolean isNeedRotate) {
canvas.save();
//加载的时候旋转风扇,负数是逆时针旋转,默认旋转5圈
if (isNeedRotate) {
canvas.rotate(-currentProgress * 360 * 5, 8 * outerRadius, 0);
}
//结束动画时候需要不断的缩小风扇,然后“100%”从小变大
if (completedProgress != 0) {
canvas.scale(1 - completedProgress, 1 - completedProgress, 8 * outerRadius, 0);
}
//旋转画扇叶,扇叶使用path绘制的
for (float i = 0; i <= 270; i = i + 90) {
canvas.rotate(i, 8 * outerRadius, 0);
canvas.drawPath(mPath, fanPaint);
}
//这个是风扇中间的小点
canvas.drawCircle(8 * outerRadius, 0, 5 * (1 - completedProgress), fanPaint);
canvas.restore();
}
绘制结束动画
结束动画 这里我们用的是属性动画提供的0-1的值实现的。这个过程主要是把进度条补齐以及风扇消失,然后“100%”字样显示。
//结束时动画 展示“100%”字样
private void showCompletedText(Canvas canvas) {
//补齐进度条
canvas.drawRect(-1, -innerRadius, (7 + completedProgress) * outerRadius, innerRadius, innerPaint);
canvas.drawArc(fanWhiteRect, 90, 360, true, fanPaint);
//绘制风扇的背景
canvas.save();
canvas.scale(0.9f, 0.9f, 8 * outerRadius, 0);
canvas.drawArc(fanWhiteRect, 90, 360, true, fanBgPaint);
canvas.restore();
if (completedProgress == 1) {
textPaint.setTextSize(60);
canvas.drawText("100%", 8 * outerRadius, textHeight, textPaint);
} else {
drawFan(canvas, completedProgress, false);
textPaint.setTextSize(60 * completedProgress);
canvas.drawText("100%", 8 * outerRadius, textHeight, textPaint);
}
}
绘制叶子
因为叶子是一直在飘荡的,这里利用系统的时间,来计算叶子的坐标。
private class Leaf {
// 在绘制部分的位置
float x, y;
// 控制叶子飘动的幅度
int type;
// 旋转角度
int rotateAngle;
// 旋转方向--0代表顺时针,1代表逆时针
int rotateDirection;
// 起始时间(ms)
long startTime;
}
/**
* 画叶子
*/
private void drawLeaf(Canvas canvas) {
long currentTime = System.currentTimeMillis();
canvas.save();
//这里进行了 一次画布平移
canvas.translate(mWidth / 10 - innerRadius, mHeight / 2 - outerRadius);
for (Leaf leaf : leafInfos) {
//如果系统当前的时间大于叶子开始绘制的时间,就去获取叶子的坐标
if (currentTime > leaf.startTime && leaf.startTime != 0) {
getLocation(leaf, currentTime);
// 通过时间关联旋转角度,则可以直接通过修改LEAF_ROTATE_TIME调节叶子旋转快慢
float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime)
/ (float) mLeafRotateTime;
int angle = (int) (rotateFraction * 360);
int rotate = leaf.rotateDirection == 0 ? angle + leaf.rotateAngle : -angle
+ leaf.rotateAngle;
//用矩阵进行坐标转换
Matrix matrix = new Matrix();
matrix.reset();
matrix.postTranslate(leaf.x, leaf.y);
matrix.postRotate(rotate, leaf.x + mLeafWidth / 2, leaf.y + mLeafHeight / 2);
//对画笔设置Xfermode
outerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
canvas.drawBitmap(mLeafBitmap, matrix, outerPaint);
outerPaint.setXfermode(null);
} else {
continue;
}
}
canvas.restore();
}
//获取叶子当前的位置
public void getLocation(Leaf leaf, long currentTime) {
//计算当前的时间和叶子绘制的时间的差值
long intervalTime = currentTime - leaf.startTime;
if (intervalTime < 0) {
//不对此片叶子进行绘制,还没到它出场的时间
return;
} else if (intervalTime > mLeafFlyTime) {
//重置叶子的出场时间
leaf.startTime = System.currentTimeMillis()
+ new Random().nextInt(mLeafFlyTime);
}
float fraction = (float) intervalTime / mLeafFlyTime;
leaf.x = getLeafX(fraction);
leaf.y = getLeafY(leaf);
}
//获取叶子x坐标
public float getLeafX(float fraction) {
return mProgressWidth * (1 - fraction);
}
//获取叶子y坐标,用到sin函数,多处用到random是为了让叶子显的更加自然
public float getLeafY(Leaf leaf) {
float w = (float) (2 * Math.PI / mProgressWidth);
float a = outerRadius / 2;
switch (leaf.type) {
case LOW_AMPLITUDE:
// 小振幅 = 中等振幅 - 振幅差
a = -mAmplitudeDisparity;
break;
case NORMAL_AMPLITUDE:
break;
case HIGH_AMPLITUDE:
// 小振幅 = 中等振幅 + 振幅差
a = +mAmplitudeDisparity;
break;
default:
break;
}
return (float) (a * Math.sin((w * leaf.x))) - mLeafHeight / 2 + outerRadius;
}
最后放上效果图
可能看着和原著有点.......,嘿嘿,原谅我没有进行优化,大家看看思路就可以了。代码地址
本文参考了一个绚丽的loading动效分析与实现!