炫酷的loadingView

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

言归正传,前些日子看到一个很炫酷的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<Leaf> leafInfos;

    //对 外面的边框缓存
    private WeakReference<Bitmap> 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<Bitmap>(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动效分析与实现!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,894评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,067评论 4 62
  • 转自 http://blog.csdn.net/shimiso** 前言:**介于许多人对项目经理这个职位的陌生和...
    酒红色的小猫一阅读 504评论 0 4
  • 猫大人的一个“好消息”:我被选上班级图书管理员啦! 此处鼓掌三百下! 嘿嘿,老子也是图书管理员哦~博览群书之后写出...
    瞅瞅君与猫大人阅读 980评论 0 2
  • 自古以来,中国的谦卑文化教育我们说:谦虚使人进步,骄傲使人落后。让我们一直活在一种压抑的情绪里。明明心里很开心,却...
    吕桂平阅读 1,179评论 0 0