炫酷的loadingView

作者: NiuGuLu | 来源:发表于2016-10-03 18:09 被阅读4177次

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

    言归正传,前些日子看到一个很炫酷的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动效分析与实现!

    相关文章

      网友评论

      本文标题:炫酷的loadingView

      本文链接:https://www.haomeiwen.com/subject/auvtettx.html