代码地址以下:
http://www.demodashi.com/demo/12833.htmlhtml
最近在学习android的高级view的绘制,再结合值动画的数据上的改变,本身撸了个360手机助手的下载按钮。先看下原版的360手机助手的下载按钮是长啥样子吧:android
再来看看本身demo吧,大家尽情的吐槽吧,哈哈:
canvas
里面的细节问题还会不断地更改的,gif的动态图是有些快的,这是由于简书要求gif的大小了,这个也冒得办法啊 。因此想看真是效果的筒子们,能够去看demo哈。api
细心的朋友可能发现loading状态下左边几个运动圆的最高点和最低点都越界了,这是由于在规定正弦函数的最高点时没考虑圆的半径的长度,所以近两天作了点修改了,效果图以下:app
我们的整个过程能够分为这么几个状态,在这里我用枚举类进行了概括:ide
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都作了些什么:ui
@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(); }
项目结构
仿360手机助手下载按钮
代码地址以下:
http://www.demodashi.com/demo/12833.html
注:本文著做权归做者,由demo大师代发,拒绝转载,转载须要做者受权