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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {faceValue = (float) animation.getAnimatedValue();invalidate();}});//动画延迟500ms启动faceLoadingAnimator.setStartDelay(200);faceLoadingAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {//恢复起始状态currentStatus = smileStatus;}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {circleValue = (float) animation.getAnimatedValue();invalidate();}});circleAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {mHandler.sendEmptyMessage(3);}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic 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)@Overridepublic 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)@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//画布移到中间canvas.translate(getWidth() / 2, getHeight() / 2);switch (currentStatus) {//起始状态case smileStatus://起始角度为0startAngle = 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {faceValue = (float) animation.getAnimatedValue();invalidate();}});//动画延迟500ms启动faceLoadingAnimator.setStartDelay(200);faceLoadingAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {//恢复起始状态currentStatus = smileStatus;}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});circleAnimator = ValueAnimator.ofFloat(0, 1).setDuration(1000);circleAnimator.setInterpolator(new LinearInterpolator());circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {circleValue = (float) animation.getAnimatedValue();invalidate();}});circleAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {mHandler.sendEmptyMessage(3);}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic 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.FaceView2android:layout_width="match_parent"android:layout_height="match_parent"/>
完整代码都在上面啦.
到这里就结束啦.
评论
