你的浏览器禁用了JavaScript, 请开启后刷新浏览器获得更好的体验!
首页
热门
推荐
精选
登录
|
注册
仿360手机助手下载按钮
立即下载
用AI写一个
该例子支持:好用才打赏哦
现在下载学习
发布时间:2018-04-14
10人
|
浏览:2756次
|
收藏
|
分享
技术:canvas(rotate、translate)、属性动画
运行环境:android studio+android sdk25
概述
通过canvas的旋转、平移实现的仿360手机助手下载按钮
详细
最近在学习android的高级view的绘制,再结合值动画的数据上的改变,自己撸了个360手机助手的下载按钮。先看下原版的360手机助手的下载按钮是长啥样子吧:  ### 一、运行效果: 再来看看自己demo吧,你们尽情的吐槽吧,哈哈:  里面的细节问题还会不断地更改的,gif的动态图是有些快的,这是因为简书要求gif的大小了,这个也冒得办法啊 。所以想看真是效果的筒子们,可以去看demo哈。  **细心的朋友可能发现loading状态下左边几个运动圆的最高点和最低点都越界了,这是因为在规定正弦函数的最高点时没考虑圆的半径的长度,因此近两天做了点修改了,效果图如下:**  ### 二、实现细节分析步骤图: 咱们的整个过程可以分为这么几个状态,在这里我用枚举类进行了归纳: ``` public enum Status { Normal, Start, Pre, Expand, Load, Complete; } ``` **Normal(还没进行开始的状态,也就是我们的默认状态,也就是我们还没执行onTouch的时候了):**  **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**状态结束都就是进入到**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**状态结束后,就是**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(); } }; ``` 可以看出此时改变的全局变量有两个:**currentLength**和**translateX**,想必大家知道**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**状态结束后就是正式进入到下载状态了,这里的枚举我定义是**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`的外面了。效果图如下:  最后一个状态就是**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起来吧。 属性也没怎么整理,就抽取出了一些比较常用的几个了:  **代码使用:** ``` /** * 进度改变的方法 * @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(); } ``` ### 三、项目文件目录截图: > 项目结构 
本实例支付的费用只是购买源码的费用,如有疑问欢迎在文末留言交流,如需作者在线代码指导、定制等,在作者开启付费服务后,可以点击“购买服务”进行实时联系,请知悉,谢谢
感谢
1
手机上随时阅读、收藏该文章 ?请扫下方二维码
相似例子推荐
评论
作者
随风者
购买服务
购买服务
服务描述:
demo不懂的地方可以讲给你听
服务价格:
¥10
我要联系
6
例子数量
107
帮助
22
感谢
评分详细
可运行:
4.5
分
代码质量:
4.5
分
文章描述详细:
4.5
分
代码注释:
4.5
分
综合:
4.5
分
作者例子
苹果版小黄车(ofo)app主页菜单效果
一分钟搞定触手app主页酷炫滑动切换效果
3D版翻页公告效果
定制一个类似地址选择器的view
仿360手机助手下载按钮
Mvp快速搭建商城购物车模块