android 自定义loading,Android 自定义 View 之 LeavesLoading

1.前言

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

6512d92532be0308bbcd3ab34df3b193.gif

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

效果如下:

1460000018087468

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() 使用,否则达不到想要的效果。

绘制 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 的这篇文章提供的核心代码参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值