乐趣区

Android 自定义 View 之 LeavesLoading

1. 前言
前天的浏览 GitHub 时发现一个模仿 Gif 的 Loading 特效的项目,感觉效果很不错,也比较有创意,如下:

GitHub 上好几个做这个效果的项目,但是很少有完全实现的,有的还有 Bug,于是花了 2 天实现了一下。
效果如下:

GitHub 项目在这里 LeavesLoading
2. 分析
实现要求:

叶子

随机产生
飘动轨迹为正弦函数,并且随机振幅
飘动时伴随自旋转,更符合物理规律
遇到进度条似乎是融入的

风扇

可旋转
Loading == 100% 时显示一个动画

细节

风扇和叶子自适应 View 大小
叶子在视觉上不能飘出 RountRect 边界

3. 核心实现
3.1 随机产生叶子
本质是事先产生一定数量叶子,这些叶子的漂动时的振幅、相位、旋转方向等等都是随机的,并且飘动是周期性地即叶子飘动到最左边时,又重新回到最右边。
Leaf 类:
private class Leaf{
float x,y;// 坐标
AmplitudeType type;// 叶子飘动振幅
int rotateAngle;// 旋转角度
RotateDir rotateDir;// 旋转方向
long startTime;// 起始时间
int n;// 初始相位
}
Leaf 生成方法:
Leaf generateLeaf(){
Leaf leaf = new Leaf();
// 随机振幅
int randomType = mRandom.nextInt(3);
switch (randomType){
case 0:
// 小振幅
leaf.type = AmplitudeType.LITTLE;
break;
case 1:
// 中等振幅
leaf.type = AmplitudeType.MIDDLE;
break;
default:
// 大振幅
leaf.type = AmplitudeType.BIG;
break;
}
// 随机旋转方向
int dir = mRandom.nextInt(2);
switch (dir){
case 0:
// 逆时针
leaf.rotateDir = RotateDir.ANTICLOCKWISE;
break;
default:
// 顺时针
leaf.rotateDir = RotateDir.CLOCKWISE;
break;
}
// 随机起始角度
leaf.rotateAngle = mRandom.nextInt(360);
leaf.n = mRandom.nextInt(20);
mAddTime += mRandom.nextInt((int)mLeafFloatTime);
leaf.startTime = System.currentTimeMillis() + mAddTime;
return leaf;
}
3.2 叶子飘动轨迹为正弦函数
确定 Leaf 在某个时刻的坐标 (x , y):
/**
* 获取叶子的(x,y)位置
* @param leaf 叶子
* @param currentTime 当前时间
*/
private void getLeafLocation(Leaf leaf,long currentTime){
long intervalTime = currentTime – leaf.startTime;// 飘动时长
if (intervalTime <= 0){
// 此 Leaf 还没到飘动时间
return;
}else if (intervalTime > mLeafFloatTime){
// Leaf 的飘动时间大于指定的飘动时间,即叶子飘动到了最左边,应回到最右边
leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime);
}
// 计算移动因子
float fraction = (float) intervalTime / mLeafFloatTime;
leaf.x = (1-fraction)*mProgressLen;
leaf.y = getLeafLocationY(leaf);

if (leaf.x <= mYellowOvalHeight / 4){
// 叶子飘到最左边,有可能会超出 RoundRect 边界,所以提前特殊处理
leaf.startTime = currentTime + new Random().nextInt((int)mLeafFloatTime);
leaf.x = mProgressLen;
leaf.y = getLeafLocationY(leaf);
}
}
要想让 Leaf 飘动轨迹为正弦函数,关键在于确定 Leaf 的 Y 轴坐标:
/**
* 获取叶子的 Y 轴坐标
* @param leaf 叶子
* @return 经过计算的叶子 Y 轴坐标
*/
private float getLeafLocationY(Leaf leaf){
float w = (float) (Math.PI * 2 / mProgressLen);// 角频率
float A;// 计算振幅值
switch (leaf.type){
case LITTLE:
A = mLeafLen/3;
break;
case MIDDLE:
A = mLeafLen*2/3;
break;
default:
A = mLeafLen;
break;
}
// (mHeight-mLeafLen)/2 是为了让 Leaf 的 Y 轴起始位置居中
return (float) (A * Math.sin(w * leaf.x + leaf.n)+(mHeight-mLeafLen)/2);
}
3.3 叶子飘动时自旋转
这里就涉及到了 Leaf 的绘制,其实 Gif 中的叶子和风扇都可以使用 Canves 直接绘制图案,但是这样就会有两个问题:

难画:想要画出满意图形,并且还要旋转、缩放、平移可要下一番功夫。
灵活性低:如果想换其他样式又得重新设计绘制过程。

因此这里采用 Canves.drawBitmap() 的方式绘制,直接使用已有的图片作为叶子和风扇,同时利用 Canves.drawBitmap() 的一个重载的方法可以很方便的实现旋转、缩放、平移:
void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) ;
就是通过这里的 Matrix 矩阵,它内部封装了 postScale()、postTranslate、postRotate() 等方法,可以帮助我们快速的对 Bitmap 进行旋转、缩放、平移还有其他操作。使用时要记得配合 Canves 的 save() 和 restore() 使用,否则达不到想要的效果。
对这方面不熟的朋友可以看看 HenCoder 的自定义 View 教学 1-4。
绘制 Leaf 的方法:
private void drawLeaves(Canvas canvas){
long currentTime = System.currentTimeMillis();
for (Leaf leaf : mLeafList) {
if (currentTime > leaf.startTime && leaf.startTime != 0){
// 获取 leaf 当前的坐标
getLeafLocation(leaf,currentTime);
canvas.save();
Matrix matrix = new Matrix();
// 缩放 自适应 View 的大小
float scaleX = (float) mLeafLen / mLeafBitmapWidth;
float scaleY = (float) mLeafLen / mLeafBitmapHeight;
matrix.postScale(scaleX,scaleY);
// 位移
float transX = leaf.x;
float transY = leaf.y;
matrix.postTranslate(transX,transY);
// 旋转
// 计算旋转因子
float rotateFraction = ((currentTime – leaf.startTime) % mLeafRotateTime)
/(float)mLeafRotateTime;
float rotate;
switch (leaf.rotateDir){
case CLOCKWISE:
// 顺时针
rotate = rotateFraction * 360 + leaf.rotateAngle;
break;
default:
// 逆时针
rotate = -rotateFraction * 360 + leaf.rotateAngle;
break;
}
// 旋转中心选择 Leaf 的中心坐标
matrix.postRotate(rotate,transX + mLeafLen / 2,transY + mLeafLen / 2);
canvas.drawBitmap(mLeafBitmap,matrix,mBitmapPaint);
canvas.restore();
}
}
3.4 Loading == 100% 出现动画
增加一个判断字段 isLoadingCompleted,在 onDraw() 中选择对应绘制策略。
isLoadingCompleted 在 setProgress() 中根据 progress 设置:
/**
* 设置进度(自动刷新)
* @param progress 0-100
*/
public void setProgress(int progress){
if (progress < 0){
mProgress = 0;
}else if (progress > 100){
mProgress = 100;
}else {
mProgress = progress;
}
if (progress == 100){
isLoadingCompleted = true;
}else {
isLoadingCompleted = false;
}
// 255 不透明
mCompletedFanPaint.setAlpha(255);
postInvalidate();
}
LeavesLoading.onDraw() 部分实现:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
……
if (isLoadingCompleted){
// 绘制加载完成特效
drawCompleted(canvas);
}else {
// 绘制扇叶
drawFan(canvas,mFanLen,mBitmapPaint);
}
// 刷新
postInvalidate();
}
drawCompleted() 实现:
private void drawCompleted(Canvas canvas) {
// 每次绘制风扇透明度递减 10
int alpha = mCompletedFanPaint.getAlpha() – 10;
if (alpha <= 0){
alpha = 0;
}
mCompletedFanPaint.setAlpha(alpha);
// 文字透明度刚好与风扇相反
mCompletedTextPaint.setAlpha(255-alpha);
// 计算透明因子
float fraction = alpha / 255f;
// 叶片大小 和 文字大小 也是相反变化的
float fanLen = fraction * mFanLen;
float textSize = (1 – fraction) * mCompletedTextSize;
mCompletedTextPaint.setTextSize(textSize);
// 测量文字占用空间
Rect bounds = new Rect();
mCompletedTextPaint.getTextBounds(
LOADING_COMPLETED,
0,
LOADING_COMPLETED.length(),
bounds);
// 与 drawLeaf() 相似,不再赘述
drawFan(canvas, (int) fanLen, mCompletedFanPaint);
// 画文字
canvas.drawText(
LOADING_COMPLETED,
0,
LOADING_COMPLETED.length(),
mFanCx-bounds.width()/2f,
mFanCy+bounds.height()/2f,
mCompletedTextPaint);
}
流程:计算风扇和文字透明度 -> 计算风扇和文字大小以及文字占用空间 -> 绘制,风扇逐渐变透明和变小,文字逐渐变清晰和变大,注释写得比较清楚就不赘述了。
4. 结束
文章中如有出现任何错误,欢迎大家到评论区留言指正。
如果觉得 LeavesLoading 对您有任何帮助,希望可以在 GitHub 得到您的 Star!
Thanks:

GAStudio 的这篇文章提供的核心代码参考

退出移动版