zoukankan      html  css  js  c++  java
  • 仿360手机助手下载按钮

    代码地址如下:
    http://www.demodashi.com/demo/12833.html

    最近在学习android的高级view的绘制,再结合值动画的数据上的改变,自己撸了个360手机助手的下载按钮。先看下原版的360手机助手的下载按钮是长啥样子吧:

    360下载按钮效果图.gif

    一、运行效果:

    再来看看自己demo吧,你们尽情的吐槽吧,哈哈:
    360downSimple.gif

    里面的细节问题还会不断地更改的,gif的动态图是有些快的,这是因为简书要求gif的大小了,这个也冒得办法啊 。所以想看真是效果的筒子们,可以去看demo哈。

    完善后的效果图.gif

    细心的朋友可能发现loading状态下左边几个运动圆的最高点和最低点都越界了,这是因为在规定正弦函数的最高点时没考虑圆的半径的长度,因此近两天做了点修改了,效果图如下:

    修改loading状态下的运动点最高点和最低点.gif

    二、实现细节分析步骤图:

    咱们的整个过程可以分为这么几个状态,在这里我用枚举类进行了归纳:

     public enum Status {
            Normal, Start, Pre, Expand, Load, Complete;
     }
    

    Normal(还没进行开始的状态,也就是我们的默认状态,也就是我们还没执行onTouch的时候了):

    normal状态.png

    Start(点击onTouch改变为该状态):

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = MotionEventCompat.getActionMasked(event);
        //抬起的时候去改变status
        if (action == MotionEvent.ACTION_UP) {
            status = Status.Start;
            startAnimation(collectAnimator);
        }
        return true;
    }
    

    那咱们再来看看collectAnimator做了些什么呢:

    collectAnimator = new Animation() {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            currentLength = (int) (width - width * interpolatedTime);
            if (currentLength <= height) {
                currentLength = height;
                clearAnimation();
                status = Status.Pre;
                angleAnimator.start();
            }
            invalidate();
        }
    };
    collectAnimator.setInterpolator(new LinearInterpolator());
    collectAnimator.setDuration(collectSpeed);
    

    其实核心的就是在这个过程中改变了全局变量currentLength而已,此时我们回到onDraw里面吧,看看在Start状态下currentLength都做了些什么:

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (status == Status.Normal || status == Status.Start) {
            float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
            canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            float allHeight = fontMetrics.descent - fontMetrics.ascent;
            if (status == Status.Normal) {
                canvas.drawText("下载", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint);
            }
        } else if (status == Status.Pre) {
            canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
            canvas.save();
            canvas.rotate(angle, (float) (width * 1.0 / 2), (float) (height * 1.0 / 2));
            canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
            canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
            canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
            canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
            canvas.restore();
        } else if (status == Status.Expand) {
            float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
            canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
    
            canvas.save();
            canvas.translate(translateX, 0);
            canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
            canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
            canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
            canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
            canvas.restore();
        } else if (status == Status.Load || status == Status.Complete) {
    
            float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
            bgPaint.setColor(progressColor);
            canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
            if (progress != 100) {
                //画中间的几个loading的点的情况哈
                if (fourMovePoint[0].isDraw)
                    canvas.drawCircle(fourMovePoint[0].moveX, fourMovePoint[0].moveY, fourMovePoint[0].radius, textPaint);
                if (fourMovePoint[1].isDraw)
                    canvas.drawCircle(fourMovePoint[1].moveX, fourMovePoint[1].moveY, fourMovePoint[1].radius, textPaint);
                if (fourMovePoint[2].isDraw)
                    canvas.drawCircle(fourMovePoint[2].moveX, fourMovePoint[2].moveY, fourMovePoint[2].radius, textPaint);
                if (fourMovePoint[3].isDraw)
                    canvas.drawCircle(fourMovePoint[3].moveX, fourMovePoint[3].moveY, fourMovePoint[3].radius, textPaint);
            }
    
            float progressRight = (float) (progress * width * 1.0 / 100);
            //在最上面画进度
            bgPaint.setColor(bgColor);
    
            canvas.save();
            canvas.clipRect(0, 0, progressRight, height);
            canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
            canvas.restore();
    
            if (progress != 100) {
                bgPaint.setColor(bgColor);
                canvas.drawCircle((float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
                canvas.save();
                canvas.rotate(loadAngle, (float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2));
             	  canvas.drawCircle(width - height + 25, getCircleY(width - height + 25), 5, textPaint);
                canvas.drawCircle(width - height + 40, getCircleY(width - height + 40), 7, textPaint);
                canvas.drawCircle(width - height + 60, getCircleY(width - height + 60), 9, textPaint);
                canvas.drawCircle(width - height + 90, getCircleY(width - height + 90), 11, textPaint);
                canvas.restore();
            }
            //中间的进度文字
            Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
            float allHeight = fontMetrics.descent - fontMetrics.ascent;
            canvas.drawText(progress + "%", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint);
        }
    }
    

    为了便于我们分析每一个状态,我们就看下每个状态下的绘制动作吧:

    if (status == Status.Normal || status == Status.Start) {
        float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
        canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float allHeight = fontMetrics.descent - fontMetrics.ascent;
        if (status == Status.Normal) {
            canvas.drawText("下载", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint);
        }
    }
    

    大家看到变量currentLength了没,其实这里就是去改变背景的right坐标,正好上面动画里面也是从width减小的一个值,那么此时的动画大家脑海里能想象得出来了吧:
    start效果图.gif

    Start状态结束都就是进入到Pre状态了:
    上面collectAnimator动画结束后启动的动画是:angleAnimator了,
    我们再去看看该动画都做了些啥:

    angleAnimator = ValueAnimator.ofFloat(0, 1);
    angleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            angle += 10;
            invalidate();
        }
    });
    

    改变的还是全局的变量angle,再来看看该变量在onDraw方法里面都做了些啥吧:

    else if (status == Status.Pre) {
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
        canvas.save();
        canvas.rotate(angle, (float) (width * 1.0 / 2), (float) (height * 1.0 / 2));
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.restore();
    } 
    

    画了几个圆,然后通过上面的angle变量来旋转canvas,而且几个圆的圆心都与view的中心点有关,因此大家从示例图中应该看出来了:
    pre效果图.gif

    pre状态结束后,就是Expand状态了,大家可以看pre状态下动画结束的代码:

    angleAnimator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
    
        }
    
        @Override
        public void onAnimationEnd(Animator animation) {
            status = Status.Expand;
            angleAnimator.cancel();
            startAnimation(tranlateAnimation);
        }
    
        @Override
        public void onAnimationCancel(Animator animation) {
    
        }
    
        @Override
        public void onAnimationRepeat(Animator animation) {
    
        }
    });
    

    可以看出下一个动画tranlateAnimation了,还是一样定位到该动画的代码吧,看看都做了些啥:

    tranlateAnimation = new Animation() {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            currentLength = (int) (height + (width - height) * interpolatedTime);
            translateX = (float) ((width * 1.0 / 2 - height * 1.0 / 2) * interpolatedTime);
            invalidate();
        }
    };
    

    可以看出此时改变的全局变量有两个:currentLengthtranslateX,想必大家知道currentLength是什么作用了吧,下面就来看看onDraw吧:

    else if (status == Status.Expand) {
        float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
        canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
    
        canvas.save();
        canvas.translate(translateX, 0);
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.restore();
    }
    

    一个是改变背景的right坐标,再个就是canvas.translate几个中心点的圆了:
    expand效果图.gif

    expand状态结束后就是正式进入到下载状态了,这里的枚举我定义是Load,
    看下expand结束的动画代码吧:

    tranlateAnimation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
        }
    
        @Override
        public void onAnimationEnd(Animation animation) {
            clearAnimation();
            status = Status.Load;
            clearAnimation();
            loadRotateAnimation.start();
            movePointAnimation.start();
        }
    
        @Override
        public void onAnimationRepeat(Animation animation) {
    
        }
    });
    

    大家可以看到该处有两个动画的启动了(loadRotateAnimation.start()movePointAnimation.start()),说明此处有两个动画在同时执行罢了,先来看loadRotateAnimation动画里面都做了些啥吧:

    loadRotateAnimation = ValueAnimator.ofFloat(0, 1);
    loadRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            loadAngle += rightLoadingSpeed;
            if (loadAngle > 360) {
                loadAngle = loadAngle - 360;
            }
            invalidate();
        }
    });
    loadRotateAnimation.setDuration(Integer.MAX_VALUE);
    

    还是一个角度改变的动画啊,那就看看loadAngle是改变谁的动画吧,还是照常我们进入到onDraw方法吧:

    if (progress != 100) {
        bgPaint.setColor(bgColor);
        canvas.drawCircle((float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
        canvas.save();
        canvas.rotate(loadAngle, (float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2));
        canvas.drawCircle(width - height + 25, getCircleY(width - height + 25), 5, textPaint);
        canvas.drawCircle(width - height + 40, getCircleY(width - height + 40), 7, textPaint);
        canvas.drawCircle(width - height + 60, getCircleY(width - height + 60), 9, textPaint);
        canvas.drawCircle(width - height + 90, getCircleY(width - height + 90), 11, textPaint);
        canvas.restore();
    }
    

    还是一个圆的旋转啊,其实这几个点是有规律去绘制的,他们几个圆心应该是内圆的弧度上的,并且半径是依次增大的。这里调了getCircleY()方法,该方法就是算圆弧上几个点的y坐标。

    /**
     * 根据x坐标算出圆的y坐标
     *
     * @param cx:点的圆心x坐标
     * @return
     */
    private float getCircleY(float cx) {
        float cy = (float) (height * 1.0 / 2 - Math.sqrt((height * 1.0 / 2 - dp2px(7)) * (height * 1.0 / 2 - dp2px(7)) - ((width - height * 1.0 / 2) - cx) * ((width - height * 1.0 / 2) - cx)));
        return cy;
    }
    

    这里看似方法很复杂,其实就是初中定义圆的方程式:(x-cx)2+(y-cy)2=r^2

    下面再来看看movePointAnimation动画都做了些啥吧:

    fourMovePoint[0] = new MovePoint(dp2px(4), (float) ((width - height / 2) * 0.88), 0);
    fourMovePoint[1] = new MovePoint(dp2px(3), (float) ((width - height / 2) * 0.85), 0);
    fourMovePoint[2] = new MovePoint(dp2px(2), (float) ((width - height / 2) * 0.80), 0);
    fourMovePoint[3] = new MovePoint(dp2px(5), (float) ((width - height / 2) * 0.75), 0);
    
    movePointAnimation = ValueAnimator.ofFloat(0, 1);
    movePointAnimation.setRepeatCount(ValueAnimator.INFINITE);
    movePointAnimation.setInterpolator(new LinearInterpolator());
    movePointAnimation.setDuration(leftLoadingSpeed);
    movePointAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float value = animation.getAnimatedFraction();
            fourMovePoint[0].moveX = fourMovePoint[0].startX - fourMovePoint[0].startX * value;
            if (fourMovePoint[0].moveX <= height / 2) {
                fourMovePoint[0].isDraw = false;
            }
            fourMovePoint[1].moveX = fourMovePoint[1].startX - fourMovePoint[0].startX * value;
            if (fourMovePoint[1].moveX <= height / 2) {
                fourMovePoint[1].isDraw = false;
            }
            fourMovePoint[2].moveX = fourMovePoint[2].startX - fourMovePoint[0].startX * value;
            if (fourMovePoint[2].moveX <= height / 2) {
                fourMovePoint[2].isDraw = false;
            }
            fourMovePoint[3].moveX = fourMovePoint[3].startX - fourMovePoint[0].startX * value;
            if (fourMovePoint[3].moveX <= height / 2) {
                fourMovePoint[3].isDraw = false;
            }
            fourMovePoint[0].moveY = drawMovePoints(fourMovePoint[0].moveX);
            fourMovePoint[1].moveY = drawMovePoints(fourMovePoint[1].moveX);
            fourMovePoint[2].moveY = drawMovePoints(fourMovePoint[2].moveX);
            fourMovePoint[3].moveY = drawMovePoints(fourMovePoint[3].moveX);
            Log.d("TAG", "fourMovePoint[0].moveX:" + fourMovePoint[0].moveX + ",fourMovePoint[0].moveY:" + fourMovePoint[0].moveY);
        }
    });
    
    movePointAnimation.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            fourMovePoint[3].isDraw = true;
            fourMovePoint[2].isDraw = true;
            fourMovePoint[1].isDraw = true;
            fourMovePoint[0].isDraw = true;
        }
    
        @Override
        public void onAnimationEnd(Animator animation) {
    
        }
    
        @Override
        public void onAnimationCancel(Animator animation) {
    
        }
    
        @Override
        public void onAnimationRepeat(Animator animation) {
            fourMovePoint[3].isDraw = true;
            fourMovePoint[2].isDraw = true;
            fourMovePoint[1].isDraw = true;
            fourMovePoint[0].isDraw = true;
        }
    });
    

    这里首先定义了四个MovePoint,分别定义了他们的半径,圆心,然后在该动画里面不断地改变四个point的圆心,其实这里核心就是如何求出四个点运行的轨迹了,把轨迹弄出来一切就都呈现出来了,可以看看该动画的onAnimationUpdate方法里面调用的drawMovePoints方法:

    /**
     * 这里是在load情况下获取几个点运动的轨迹数学函数
     *
     * @param moveX
     * @return
     */
    private float drawMovePoints(float moveX) {
        float moveY = (float) (height / 2 + (height / 2 - fourMovePoint[3].radius) * Math.sin(4 * Math.PI * moveX / (width - height) + height / 2));
        return moveY;
    }
    

    这里就是一个数学里面经常用的正弦函数了,求出周期、x轴上的偏移量、y轴上的便宜量、顶点,还有一个注意点,该处求顶点的时候,需要减去这几个圆中的最大半径,之前我就是没注意到这点,最后出来的轨迹就是一个圆会跑到view的外面了。效果图如下:
    load效果图.gif

    最后一个状态就是Complete了,也就是当前的进度到了100,可见代码:

     /**
         * 进度改变的方法
         *
         * @param progress(当前进度)
         */
    public void setProgress(int progress) {
        if (status != Status.Load) {
            throw new RuntimeException("your status is not loading");
        }
    
        if (this.progress == progress) {
            return;
        }
        this.progress = progress;
        if (onProgressUpdateListener != null) {
            onProgressUpdateListener.onChange(this.progress);
        }
        invalidate();
        if (progress == 100) {
            status = Status.Complete;
            this.stop = false;
            clearAnimation();
            loadRotateAnimation.cancel();
            movePointAnimation.cancel();
        }
    }
    

    这里要做的就是改变状态,停止一切动画了,到此代码的讲解就到这里了,赶快start起来吧。

    属性也没怎么整理,就抽取出了一些比较常用的几个了:

    屏幕快照 2017-04-01 14.21.37.png

    代码使用:

     /**
     * 进度改变的方法
     * @param progress
     */
    public void setProgress(int progress) {
        if (status != Status.Load) {
            throw new RuntimeException("your status is not loading");
        }
    
        if (this.progress == progress) {
            return;
        }
        this.progress = progress;
        if (onProgressUpdateListener != null) {
            onProgressUpdateListener.onChange(this.progress);
        }
        invalidate();
        if (progress == 100) {
            status = Status.Complete;
            this.stop = false;
            clearAnimation();
            loadRotateAnimation.cancel();
            movePointAnimation.cancel();
        }
    }
    
    /**
     * 暂停或继续的方法
     *
     * @param stop(true:表示暂停,false:继续)
     */
    public void setStop(boolean stop) {
        if (this.stop == stop) {
            return;
        }
        this.stop = stop;
        if (stop) {
            loadRotateAnimation.cancel();
            movePointAnimation.cancel();
        } else {
            loadRotateAnimation.start();
            movePointAnimation.start();
        }
    }
    
    /**
     *设置状态的方法
     * @param status(Down360Loading.Status.Normal:直接取消的操作)
     */
    public void setStatus(Status status) {
        if (this.status == status) {
            return;
        }
        this.status = status;
        if (this.status == Status.Normal) {
            progress = 0;
            this.stop = false;
            clearAnimation();
            loadRotateAnimation.cancel();
            movePointAnimation.cancel();
        }
        invalidate();
    }
    

    三、项目文件目录截图:

    项目结构

    仿360手机助手下载按钮

    代码地址如下:
    http://www.demodashi.com/demo/12833.html

    注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权

  • 相关阅读:
    表达式执行工具方法
    Mysql表创建外键报错
    JVM打印加载类的详情信息
    Shell脚本查询进程存活信息
    旋转数组的最小数字
    斐波那契数列(水题)
    用两个栈实现队列
    变态跳台阶
    跳台阶
    9*9乘法表(5种输出格式)
  • 原文地址:https://www.cnblogs.com/demodashi/p/9437153.html
Copyright © 2011-2022 走看看