炫酷的loadingView

最近比较忙,烦心的事情也不少。就迷上了一款游戏《守望先锋》,差点就没回来。

言归正传,前些日子看到一个很炫酷的loadingView,看到的时候感觉,这个感觉怎么说呢,用英语说就是amazing(太TM吊了)。
我也仅仅只是通过别人的博客,加上一点自己的理解写的这篇博客,目的是想要和大家分享,顺便记录一下。感觉实现一个这么炫酷的动画还是感觉挺有成就感动的(毕竟菜鸟一枚)。这里先放上原博主的链接,感谢这位大神。这边博客把实现过程已经写的很清晰的建议有些自定义view基础的人,先去看这里先放上原博主的博客,然后自己实现以下,我这里会对整个view的实现过程详细的讲一下。

此次时间有点仓促,没有对代码进行优化,同时也有部分原作者的代码。希望大家谅解,主要是给大家提供一个思路。

先看一下效果图:


炫酷的loadingView_第1张图片

怎么样,我没说错吧,第一眼看见就眼前一亮。下面我们就对整个过程进行详细的讲解。

拆分动画

  • 和叶子一样颜色的进度条
  • 右侧旋转的白色电风扇
  • 漂浮的叶子(原博主说的很细致我这里直接引用原话)
    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;
    }



最后放上效果图

可能看着和原著有点.......,嘿嘿,原谅我没有进行优化,大家看看思路就可以了。代码地址

炫酷的loadingView_第2张图片

本文参考了一个绚丽的loading动效分析与实现!

你可能感兴趣的:(炫酷的loadingView)