Android仿豆瓣笑脸进度加载
最近看到豆瓣的笑脸loading很有意思,看一张效果图:
下面分析一下如何实现这样的效果:
1、默认状态是一张笑脸的状态(一个嘴巴,两个眼睛,默认状态)
2、开始旋转,嘴巴追上眼睛(合并状态)
3、追上以后自转一周(自转状态)
4、然后逐渐释放眼睛(分离状态)
5、回到初始笑脸状态(默认状态)
一、默认状态
首先需要确定好嘴巴和眼睛的初始位置,我这里的初始化嘴巴是一个半圆,在横轴下方。眼睛分别与横轴夹角60度,如下图:
这两部分可以使用pathMeasure,我这里使用最简单的两个api:canvas.drawArc()和canvas.drawPoint()。
1、画嘴巴
//画起始笑脸
canvas.drawArc(-radius, -radius, radius, radius, startAngle, swipeAngle, false,facePaint);
这里的startAngle初始值为0,swiperAngle为180,半径radius为40。
2、画眼睛
/**
* 初始化眼睛坐标
*/
private void initEyes() {
//默认两个眼睛坐标位置 角度转弧度
leftEyeX = (float) (-radius * Math.cos(eyeStartAngle * Math.PI / 180));
leftEyeY = (float) (-radius * Math.sin(eyeStartAngle * Math.PI / 180));
rightEyeX = (float) (radius * Math.cos(eyeStartAngle * Math.PI / 180));
rightEyeY = (float) (-radius * Math.sin(eyeStartAngle * Math.PI / 180));
}
注意:需要将角度转弧度
(2)开始画眼睛
//画起始眼睛
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
二、合并状态
1、嘴巴的旋转
faceLoadingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);
faceLoadingAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
faceLoadingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
faceValue = (float) animation.getAnimatedValue();
invalidate();
}
});
//动画延迟500ms启动
faceLoadingAnimator.setStartDelay(200);
faceLoadingAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//恢复起始状态
currentStatus = smileStatus;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
动画执行时间1s,记录动画当前执行进度值,存放在faceValue中。当动画执行结束的时候,需要将状态恢复到默认状态,调用invalidate的时候,进入onDraw()方法,开始重新绘制嘴巴。
//记录时刻的旋转角度
startAngle = faceValue * 360;
//追上右边眼睛
if (startAngle >= 120 + startAngle / 2) {
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
//开始自转一圈
mHandler.sendEmptyMessage(2);
//此时记录自转一圈起始的角度
circleStartAngle = 120 + startAngle / 2;
} else {
//追眼睛的过程
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
}
2、眼睛的旋转
//画左边眼睛 ,旋转的角度设置为笑脸旋转角度的一半,这样笑脸才能追上眼睛
leftEyeX = (float) (-radius * Math.cos((60 + startAngle / 2) * Math.PI / 180));
leftEyeY = (float) (-radius * Math.sin((60 + startAngle / 2) * Math.PI / 180));
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
//画右边眼睛 ,旋转的角度设置为笑脸旋转角度的一半,这样笑脸才能追上眼睛
rightEyeX = (float) (radius * Math.cos((60 - startAngle / 2) * Math.PI / 180));
rightEyeY = (float) (-radius * Math.sin((60 - startAngle / 2) * Math.PI / 180));
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
三、自转状态
1、开启动画
circleAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);
circleAnimator.setInterpolator(new LinearInterpolator());
circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
circleValue = (float) animation.getAnimatedValue();
invalidate();
}
});
circleAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mHandler.sendEmptyMessage(3);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
2、重新绘制
canvas.drawArc(-radius, -radius, radius, radius,
circleStartAngle + circleValue * 360,
swipeAngle, false, facePaint);
四、分离状态
startAngle = faceValue * 360;
//判断当前笑脸的起点是否已经走过260度 (吐出眼睛的角度,角度可以任意设置)
if (startAngle >= splitAngle) {
//画左边眼睛 ,旋转的角度设置为笑脸旋转角度的2倍,这样眼睛才能快于笑脸旋转速度
leftEyeX = (float) (-radius * Math.cos((eyeStartAngle + startAngle * 2) * Math.PI / 180));
leftEyeY = (float) (-radius * Math.sin((eyeStartAngle + startAngle * 2) * Math.PI / 180));
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
//画右边眼睛 ,旋转的角度设置为笑脸旋转角度的2倍,这样眼睛才能快于笑脸旋转速度
rightEyeX = (float) (radius * Math.cos((eyeStartAngle - startAngle * 2) * Math.PI / 180));
rightEyeY = (float) (-radius * Math.sin((eyeStartAngle - startAngle * 2) * Math.PI / 180));
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
}
//画笑脸
canvas.drawArc(-radius, -radius, radius, radius, startAngle, swipeAngle,
false, facePaint);
最后附上完整代码
public class FaceView2 extends View {
//圆弧半径
private int radius = 40;
//圆弧画笔宽度
private float paintWidth = 15;
//笑脸状态(一个脸,两个眼睛)
private final int smileStatus = 0;
//加载状态 合并眼睛,旋转
private final int loadingStatus = 1;
//合并完成 转一圈
private final int circleStatus = 2;
//转圈完成 吐出眼睛
private final int splitStatus = 3;
//当前状态
private int currentStatus = smileStatus;
//笑脸画笔
private Paint facePaint;
//眼睛画笔
private Paint eyePaint;
//笑脸开始角度
private float startAngle;
//笑脸弧度
private float swipeAngle;
//左侧眼睛起点x轴坐标
private float leftEyeX = 0;
//左侧眼睛起点y轴坐标
private float leftEyeY = 0;
//右侧眼睛起点x轴坐标
private float rightEyeX;
//右侧眼睛起点y轴坐标
private float rightEyeY;
//一开始默认状态笑脸转圈动画
private ValueAnimator faceLoadingAnimator;
//吞并完成后,自转一圈动画
private ValueAnimator circleAnimator;
//faceLoadingAnimator动画进度值
private float faceValue;
//circleAnimator动画进度值
private float circleValue;
//记录开始自转一圈的起始角度
private float circleStartAngle;
//吐出眼睛的角度
private float splitAngle;
private float initStartAngle;
//眼睛起始角度
private float eyeStartAngle = 60;
public FaceView2(Context context) {
this(context, null);
}
public FaceView2(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FaceView2(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
//自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FaceView2,
defStyleAttr, 0);
initStartAngle = typedArray.getFloat(R.styleable.FaceView2_startAngle, 0);
swipeAngle = typedArray.getFloat(R.styleable.FaceView2_swipeAngle, 180);
splitAngle = typedArray.getFloat(R.styleable.FaceView2_splitAngle, 260);
typedArray.recycle();
startAngle = initStartAngle;
eyeStartAngle += startAngle;
initEyes();
initPaint();
//开始默认动画
initAnimator();
}
/**
* 初始化画笔
*/
private void initPaint() {
//初始化画笔
facePaint = new Paint();
facePaint.setStrokeWidth(paintWidth);
facePaint.setColor(Color.RED);
facePaint.setAntiAlias(true);
facePaint.setStyle(Paint.Style.STROKE);
facePaint.setStrokeCap(Paint.Cap.ROUND);
eyePaint = new Paint();
eyePaint.setStrokeWidth(paintWidth);
eyePaint.setColor(Color.RED);
eyePaint.setAntiAlias(true);
eyePaint.setStyle(Paint.Style.STROKE);
eyePaint.setStrokeCap(Paint.Cap.ROUND);
}
/**
* 初始化眼睛坐标
*/
private void initEyes() {
//默认两个眼睛坐标位置 角度转弧度
leftEyeX = (float) (-radius * Math.cos(eyeStartAngle * Math.PI / 180));
leftEyeY = (float) (-radius * Math.sin(eyeStartAngle * Math.PI / 180));
rightEyeX = (float) (radius * Math.cos(eyeStartAngle * Math.PI / 180));
rightEyeY = (float) (-radius * Math.sin(eyeStartAngle * Math.PI / 180));
}
private Handler mHandler = new Handler(new Handler.Callback() {
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 1:
//启动一开始笑脸转圈动画,并且开始合并眼睛
currentStatus = loadingStatus;
faceLoadingAnimator.start();
break;
case 2:
//暂停眼睛和笑脸动画
currentStatus = circleStatus;
faceLoadingAnimator.pause();
//启动笑脸自转一圈动画
circleAnimator.start();
break;
case 3:
//恢复笑脸转圈动画,并且开始分离眼睛
currentStatus = splitStatus;
circleAnimator.cancel();
faceLoadingAnimator.resume();
invalidate();
break;
}
return false;
}
});
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画布移到中间
canvas.translate(getWidth() / 2, getHeight() / 2);
switch (currentStatus) {
//起始状态
case smileStatus:
//起始角度为0
startAngle = initStartAngle;
//画起始笑脸
canvas.drawArc(-radius, -radius, radius, radius, startAngle, swipeAngle, false,
facePaint);
//重置起始眼睛坐标
initEyes();
//画起始眼睛
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
//更改状态,进行笑脸合并眼睛
mHandler.sendEmptyMessage(1);
break;
//合并状态
case loadingStatus:
//记录时刻的旋转角度
startAngle = faceValue * 360;
//追上右边眼睛
if (startAngle >= 120 + startAngle / 2) {
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
//开始自转一圈
mHandler.sendEmptyMessage(2);
//此时记录自转一圈起始的角度
circleStartAngle = 120 + startAngle / 2;
} else {
//追眼睛的过程
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
}
//画左边眼睛 ,旋转的角度设置为笑脸旋转角度的一半,这样笑脸才能追上眼睛
leftEyeX = (float) (-radius * Math.cos((60 + startAngle / 2) * Math.PI / 180));
leftEyeY = (float) (-radius * Math.sin((60 + startAngle / 2) * Math.PI / 180));
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
//画右边眼睛 ,旋转的角度设置为笑脸旋转角度的一半,这样笑脸才能追上眼睛
rightEyeX = (float) (radius * Math.cos((60 - startAngle / 2) * Math.PI / 180));
rightEyeY = (float) (-radius * Math.sin((60 - startAngle / 2) * Math.PI / 180));
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
break;
//自转一圈状态 circleValue * 360 为旋转角度
case circleStatus:
canvas.drawArc(-radius, -radius, radius, radius,
circleStartAngle + circleValue * 360,
swipeAngle, false, facePaint);
break;
//笑脸眼睛分离状态
case splitStatus:
startAngle = faceValue * 360;
//判断当前笑脸的起点是否已经走过260度 (吐出眼睛的角度,角度可以任意设置)
if (startAngle >= splitAngle) {
//画左边眼睛 ,旋转的角度设置为笑脸旋转角度的2倍,这样眼睛才能快于笑脸旋转速度
leftEyeX = (float) (-radius * Math.cos((eyeStartAngle + startAngle * 2) * Math.PI / 180));
leftEyeY = (float) (-radius * Math.sin((eyeStartAngle + startAngle * 2) * Math.PI / 180));
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
//画右边眼睛 ,旋转的角度设置为笑脸旋转角度的2倍,这样眼睛才能快于笑脸旋转速度
rightEyeX = (float) (radius * Math.cos((eyeStartAngle - startAngle * 2) * Math.PI / 180));
rightEyeY = (float) (-radius * Math.sin((eyeStartAngle - startAngle * 2) * Math.PI / 180));
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
}
//画笑脸
canvas.drawArc(-radius, -radius, radius, radius, startAngle, swipeAngle,
false, facePaint);
break;
}
}
/**
* 初始化动画
*/
private void initAnimator() {
faceLoadingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);
faceLoadingAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
faceLoadingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
faceValue = (float) animation.getAnimatedValue();
invalidate();
}
});
//动画延迟500ms启动
faceLoadingAnimator.setStartDelay(200);
faceLoadingAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//恢复起始状态
currentStatus = smileStatus;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
circleAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);
circleAnimator.setInterpolator(new LinearInterpolator());
circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
circleValue = (float) animation.getAnimatedValue();
invalidate();
}
});
circleAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mHandler.sendEmptyMessage(3);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
}
自定义属性
<declare-styleable name="FaceView2">
<attr name="startAngle" format="dimension" />
<attr name="swipeAngle" format="dimension" />
<attr name="splitAngle" format="dimension" />
</declare-styleable>
布局文件中使用
<com.example.viewdemo.FaceView2
android:layout_width="match_parent"
android:layout_height="match_parent"/>
完整代码都在上面啦.
到这里就结束啦.
评论