Android仿豆瓣笑脸进度加载
龙旋
共 16069字,需浏览 33分钟
· 2021-05-01
最近看到豆瓣的笑脸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"/>
完整代码都在上面啦.
到这里就结束啦.
评论
豆瓣9.7,这部Java神作第3版重磅上市!
文末赠书Java 程序员们开年就有重磅好消息,《Effective Java 中文版(原书第 3 版)》要上市啦!该书的第1版出版于 2001 年,当时就在业界流传开来,受到广泛赞誉。时至今日,已热销近20年,本书第 3 版已是 Java 程序员的必读神书,被誉为“Java 四大名著之一”,甚至连
编码之外
0
豆瓣9分线代教材免费了!斯坦福伯克利都在用,新版PDF直接下载
西风 发自 凹非寺 量子位 | 公众号 QbitAI豆瓣评分9.2、斯坦福都在用的线性代数教材,全新第四版免费来袭!没错,就是那本被认为“直击线性代数理论核心”的Linear Algebra Done Right。此书之前已发售三版,风靡30多个国家的200多所高校,这其中就包括斯坦福、UCB等顶尖
机器学习算法与Python实战
0
豆瓣9.7,这部Java神作第3版重磅上市!
Java 程序员们开年就有重磅好消息,《Effective Java 中文版(原书第 3 版)》要上市啦!该书的第1版出版于 2001 年,当时就在业界流传开来,受到广泛赞誉。时至今日,已热销近20年,本书第 3 版已是 Java 程序员的必读神书,被誉为“Java 四大名著之一”,甚至连 Java
菜鸟学Python
0
数据集 | 使用1000w条豆瓣影评训练Word2Vec
本文内容介绍豆瓣影评数据集构造语料训练Word2Vec模型获取数据&cntext&Word2Vec模型文件一、豆瓣影评数据集1.1 数据集介绍数据集: douba-movie-1000w数据源: 豆瓣电影 记录数: &n
大邓和他的Python
10
性能优化——图片压缩、加载和格式选择
大厂技术 高级前端 Node进阶点击上方 程序员成长指北,关注公众号回复1,加入高级Node交流群前言相信大家都听说过 "258 原则(https://blog.csdn.net/weixin_42139375/article/details/8
程序员成长指北
10
豆瓣8.6,《码农翻身2》再续新篇
你好呀,我是码哥 。今天 给大家带来一本好书,生动幽默的讲解技术点 。 以下的“我”均代表《码农翻身2》的作者,刘欣老师。 2018年,我的第一本书《码农翻身》出版。 2021年,第二本书《半小时漫画计算机》出版。 20...
码哥字节
0