本篇文章已授权微信公众号guolin_blog(郭霖)独家发布
最近在学习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();
}
好了介绍就到这里了,如果觉得行的话,
进入github的传送门点个star吧,谢谢!!!
关于我:
email:a1002326270@163.com
csdn:仿360手机助手下载按钮
github:enter