仿360手机助手下载按钮

408 阅读9分钟

本篇文章已授权微信公众号guolin_blog(郭霖)独家发布

最近在学习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();
}

好了介绍就到这里了,如果觉得行的话, 进入github的传送门点个star吧,谢谢!!!

关于我:

email: a1002326270@163.com

csdn: 仿360手机助手下载按钮

github: enter

更多你喜欢的文章

仿360手机助手下载按钮
仿苹果版小黄车(ofo)app主页菜单效果
设计一个银行app的最大额度控件
带你实现ViewGroup规定行数、item居中的流式布局
定制一个类似地址选择器的view
3D版翻页公告效果
一分钟搞定触手app主页酷炫滑动切换效果
快速利用RecyclerView的LayoutManager搭建流式布局
用贝塞尔曲线自己写的一个电量显示的控件
快速搞定一个自定义的日历