• 1. 青い夜の記憶 - 须藤ひとみ
  • 2. 念夏 - 马天宇
  • 3. 偏爱 - 张芸京
  • 4. エンド・タイトル-东京爱情故事
  • 5. 夜色 - 玉置浩二
  • 6. オセンチな歩美 - 大野克夫
  • 7. The_Godfather_Waltz/Speak_Softly_Medley - Jack_Jezzro
  • 8. 猫になりたい - スピッツ
  • 9. Love_Theme_from_Cinema_Paradiso - Jeff_Steinberg
  • 10. 時には昔の話を - 加藤登紀子
person

先贴出学习的博客地址:一个绚丽的loading动效分析与实现
anim.gif

这是一篇15年的老博客了,一直收藏在我的浏览器标签里面。但是一直没去看。貌似收藏了一大堆这样的博客。

最近准备去面试工作,又回去看了一下自定义View。看的是GcsSloop的自定义控件系列教程:Android自定义View教程目录,这个教程写得非常好,主要是通俗易懂。也是在这个系列教程中,作者推荐学习这个自定义View,点开一看,很熟悉啊,一翻书签才发现收藏了一年了-_-

ps:原作者的叶子和风扇是抠图的,这里改成了使用Path绘制。照搬了叶子的飞行曲线函数。

基础

如果基础不太熟悉,我的建议是看一看GcsSloop的自定义控件系列教程。良心推荐!

步骤

  1. 准备叶子和风扇的Path。
  2. 绘制背景的圆角矩形。
  3. 绘制风扇。
  4. 绘制进度条。
  5. 绘制叶子。

进度条

首先确定进度条的宽度:

深度截图_选择区域_20180917131838.png

如图所示,进度条的宽度为RO

已知view的宽度为mWidth,高度为mHeight

MR = mProgressPadding

QP = mHeight/2

QO = mHeight/2 - mProgressPadding

所以:

OP² = QP² - QO²

RO = mWidth - MR - PS - OP

代码表示:

/*计算op*/
double leftMargin = Math.sqrt(Math.pow(mHeight / 2, 2) - Math.pow(mHeight / 2 - mProgressPadding, 2));

mProgressBarWidth = (int) (mWidth - mProgressPadding - mHeight / 2 - leftMargin);

难点主要是在绘制进度条左边的半圆部分和飞行中的叶子。

左边的半圆绘制方法为:

drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint)

弧线的外围矩形是很好确定的,主要是找到弧度的开始角度。

我们先看图:
深度截图_选择区域_20180917122347.png

首先移动画布到交叉点,要绘制蓝色区域的圆弧,startAngle的大小为∠B,即为180度 - 角AsweepAngle的大小为2倍∠A。所以确定∠A大小即可。

cosA = c/b; c=b-d;

∠A=acos(c/b) (反cos)

此时求出的∠A为弧度制的,使用Math.toDegrees转为角度。

使用代码表达:

/*计算弧度夹角*/
float degrees = (float) Math.toDegrees(Math.acos((
    mSemicircleRadius - currentProgressWidth) * 1f / mSemicircleRadius));

canvas.drawArc(mSemiCircleRectF, 180 - degrees, 2 * degrees,
               false, mProgressPaint);

当进度宽度>= d+c的时候,我们需要再绘制上右边的矩形部分:

深度截图_选择区域_20180917124608.png

else if (currentProgressWidth >= mSemicircleRadius) {
    /*进度条大于半圆的时候,需要绘制半圆加矩形*/
    canvas.drawArc(mSemiCircleRectF, 90, 180, false, mProgressPaint);
    
    /*更新矩形的宽度*/
    mProgressRectF.right = currentProgressWidth - mSemicircleRadius;
    canvas.drawRect(mProgressRectF, mProgressPaint);
}

风扇

准备扇叶:

扇叶分为四个,但是只准备一个Path,旋转画布重复绘制Path就可以达到效果。

使用了Path.cubicTo贝塞尔曲线三阶级。为了美观,参数基本上是慢慢调整的,所以不用太在意参数。

/**
 * 初始化风扇叶的路径 只包含一个风扇叶的路径
 */
private void initFanLeafPath() {
    /*风扇叶距离中心的高度*/
    int fanLeafTop = mHeight / 2 - mFanCircleWidth - fanLeafOutMargin / 2;
    int fanLeafRectWidth = mHeight / 2 - mFanCircleWidth;

    mFanLeafPath = new Path();
    mFanLeafPath.moveTo(0, -fanLeafInMargin);
    mFanLeafPath.cubicTo(fanLeafRectWidth / 4f, -fanLeafRectWidth / 3f,
            fanLeafRectWidth / 2f, -fanLeafRectWidth + fanLeafOutMargin / 2,
            0, -fanLeafTop);
    mFanLeafPath.cubicTo(-fanLeafRectWidth / 2f, -fanLeafRectWidth + fanLeafOutMargin / 2,
            -fanLeafRectWidth / 4f, -fanLeafRectWidth / 3f,
            0, -fanLeafInMargin);

    mFanLeafPath.close();
}

移动画布到风扇中心:

/*移动到风扇中心*/
canvas.translate(mWidth - mHeight / 2, mHeight / 2);

绘制风扇叶缘的两个圆:

/*绘制外圆*/
canvas.drawCircle(0, 0, mHeight / 2, mFanPaint);
/*绘制内圆*/
canvas.drawCircle(0, 0, mHeight / 2 - mFanCircleWidth, mFanFillPaint);

绘制风扇叶子:

for (int i = 0; i < 4; i++) {
    canvas.drawPath(mFanLeafPath, mFanPaint);
    canvas.rotate(90);
}

但是风扇是旋转的,所以还要更新一下风扇旋转角度:

canvas.rotate(-fanRotateAngel);
for (int i = 0; i < 4; i++) {
    canvas.drawPath(mFanLeafPath, mFanPaint);
    canvas.rotate(90);
}

fanRotateAngel += (margin * mFanRotateDirection);
if (fanRotateAngel == 360) {
    fanRotateAngel = 0;
}

风扇是一直在转的,所以在绘制结束之后,再调用一下invalidate()重绘,这样就到达一直旋转的效果了。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    drawBackground(canvas);
    drawLeaf(canvas);
    drawProgress(canvas);

    if (mIsFinish) {
        drawFan(canvas, true);
    } else {
        drawFan(canvas, false);
        invalidate();
    }
}

叶子

准备叶子:

叶子的Path和风扇的差不多,也是慢慢调整参数的,不使用图片的情况下,暂时还没有找到比较好的方法。老是感觉这个叶子像小蝌蚪 (゜-゜)つロ

/**
 * 初始化叶子的路径
 */
private void initLeafPath() {
    mLeafPath = new Path();
    mLeafPath.moveTo(-1 / 20f * mLeafWidth, 4 / 10f * mLeafWidth);
    mLeafPath.lineTo(1 / 40f * mLeafWidth, 4 / 10f * mLeafWidth);
    mLeafPath.lineTo(1 / 20f * mLeafWidth, 2 / 10f * mLeafWidth);
    mLeafPath.cubicTo(
            1 / 3f * mLeafWidth, 0,
            1 / 4f * mLeafWidth, -2 / 5f * mLeafWidth,
            0, -1 / 2f * mLeafWidth);

    mLeafPath.cubicTo(
            -1 / 4f * mLeafWidth, -2 / 5f * mLeafWidth,
            -1 / 3f * mLeafWidth, 0,
            -1 / 20f * mLeafWidth, 2 / 10f * mLeafWidth);

    mLeafPath.close();
}

由于叶子是一直在飞行的(不管进度是否更新),所以参照原博主的方法,使用当前时间来控制叶子的坐标。

基本思想:

  1. 一开始就初始化几片叶子,并且给每片叶子设置一个出场时间,出场时间到了才从风扇中心飞出去。
  2. 飞行轨迹是一个曲线函数,这个曲线函数可以让叶子的轨迹显得更自然。
  3. 飞到终点之后,再把叶子的坐标设置为起点,重置出场时间。等待下一波出场。
  4. 叶子在飞行的过程中要旋转,所以还要给叶子设置一个旋转角度。

所以叶子有坐标,有出场时间,有旋转角度:

static class Leaf {
    int x;
    int y;
    int angle;
    long startTime;
    int direction;/*旋转方向 0 逆时针 1顺时针*/
    StartType type;
}

初始化的时候就new一堆叶子出来,一定范围内,随机设置他们的参数。

private void initLeafArray() {
    if (mLeafPathArray == null) {
        mLeafPathArray = new ArrayList<>();
    } else {
        mLeafPathArray.clear();
    }

    for (int i = 0; i < mLeafCount; i++) {
        Leaf leaf = new Leaf();

        leaf.angle = mRandom.nextInt(360);
        leaf.direction = mRandom.nextInt(2);
        int randomType = mRandom.nextInt(3);

        /*随时类型- 随机振幅*/
        StartType type = StartType.MIDDLE;
        switch (randomType) {
            case 0:
                break;
            case 1:
                type = StartType.LITTLE;
                break;
            case 2:
                type = StartType.BIG;
                break;
            default:
                break;
        }
        leaf.type = type;

        leaf.startTime = System.currentTimeMillis() + mRandom.nextInt(mLeafOnceCycleTime);

        mLeafPathArray.add(leaf);
    }
}

绘制叶子:

private void drawLeaf(Canvas canvas) {
        canvas.save();
        /*移动画布中心到图中的P点,P点为风扇中心即叶子的出发点*/
        canvas.translate(mWidth - mSemicircleRadius, mHeight / 2);
    
        for (Leaf leaf : mLeafPathArray) {

            canvas.save();
            /*更新叶子的坐标*/
            setLeafLocation(leaf);
            canvas.translate(leaf.x, leaf.y);
            /*旋转叶子的角度*/
            canvas.rotate(leaf.angle);
            
            canvas.drawPath(mLeafPath, mProgressPaint);
            canvas.restore();
        }
        canvas.restore();
    }

更新叶子坐标的方法:

private void setLeafLocation(Leaf leaf) {
    /*根据叶子的旋转方向,修改旋转度数*/
    leaf.angle += ((leaf.direction == 0) ? 5 : -5);

    long currentTimeMillis = System.currentTimeMillis();
    /*计算当前时间和叶子出场时间的差值*/
    long timeDiff = currentTimeMillis - leaf.startTime;

    /*1. 未到出场时间*/
    if (timeDiff < 0) {
        return;
    }

    /*2. 到达终点*/
    if (timeDiff > mLeafOnceCycleTime) {
        leaf.x = 0;
        leaf.y = 0;
        /*重置坐标到原点,并且把开始时间加上一个周期,再加一个随机值避免每个周期出场时间都一样*/
        leaf.startTime += mLeafOnceCycleTime + mRandom.nextInt(1000);
        return;
    }

    /*3. 在飞行途中*/
    /*按照时间比例,计算x*/
    leaf.x = -(int) ((mWidth - mProgressPadding - mLeafWidth / 2 - mSemicircleRadius)
                     * timeDiff * 1f / mLeafOnceCycleTime);

    leaf.y = getLocationY(leaf) - mHeight / 4;
}

计算Y值的函数,为了使叶子飞行更自然,给叶子设置了不同的振幅。

/*通过叶子信息获取当前叶子的Y值*/
    private int getLocationY(Leaf leaf) {
        
        float w = (float) ((float) 2 * Math.PI / mProgressBarWidth);
        float a = mMiddleAmplitude;
        
        switch (leaf.type) {
            case LITTLE:
                /*小振幅 = 中等振幅 - 振幅差*/
                a = mMiddleAmplitude - mAmplitudeDisparity;
                break;
            case MIDDLE:
                a = mMiddleAmplitude;
                break;
            case BIG:
                /*小振幅 = 中等振幅 + 振幅差*/
                a = mMiddleAmplitude + mAmplitudeDisparity;
                break;
            default:
                break;
        }
        return (int) (a * Math.sin(w * leaf.x)) + mSemicircleRadius * 3 / 4;
    }

结束动画

效果表现为 风扇叶缩小的同时,100%文字放大。

我这里的方法是,扇叶缩小到0.5的时候,开始显示并放大100%文字,扇叶缩小到0.3以下的时候,不再绘制扇叶。其实只要自己看着舒服就行。

扇叶的缩小,只要在绘制扇叶前缩小画布即可,其他代码一样。

/*在执行缩放动画*/
if (scale) {
    canvas.save(); /*@1*/
    /*缩放画布*/
    canvas.scale(mFanLeafScaleValue, mFanLeafScaleValue);
}

文字的放大是修改了Paint的字体大小,风扇缩小的同时放大字体。

/**
  * 字体居中绘制处理
  */
private void initTextBaseLine() {
    Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
    float top = fontMetrics.top;/*为基线到字体上边框的距离*/
    float bottom = fontMetrics.bottom;/*为基线到字体下边框的距离*/

    /*基线中间点的y轴计算公式*/
    mTextBaseLineY = (int) (mSemiCircleRectF.centerY() - top / 2 - bottom / 2);
}

private void draw100Text(Canvas canvas) {
    /*设置字体的大小为: (1-风扇的缩放比例)*/
    mTextPaint.setTextSize(mTextMaxSize * (1 - mFanLeafScaleValue));
    initTextBaseLine();
    canvas.drawText("100%", mSemiCircleRectF.centerX(), mTextBaseLineY, mTextPaint);
}

 尾声

😀代码基本上都写上了中文注释,有任何问题和建议欢迎评论!

😭项目地址:Github android_leaf_loading

新评论