Android實(shí)現(xiàn)笑臉進(jìn)度加載動(dòng)畫
最近看到豆瓣的笑臉loading很有意思,看一張效果圖:

下面分析一下如何實(shí)現(xiàn)這樣的效果:
1、默認(rèn)狀態(tài)是一張笑臉的狀態(tài)(一個(gè)嘴巴,兩個(gè)眼睛,默認(rèn)狀態(tài))
2、開始旋轉(zhuǎn),嘴巴追上眼睛(合并狀態(tài))
3、追上以后自轉(zhuǎn)一周(自轉(zhuǎn)狀態(tài))
4、然后逐漸釋放眼睛(分離狀態(tài))
5、回到初始笑臉狀態(tài)(默認(rèn)狀態(tài))
一、默認(rèn)狀態(tài)
首先需要確定好嘴巴和眼睛的初始位置,我這里的初始化嘴巴是一個(gè)半圓,在橫軸下方。眼睛分別與橫軸夾角60度,如下圖:

這兩部分可以使用pathMeasure,我這里使用最簡(jiǎn)單的兩個(gè)api:canvas.drawArc()和canvas.drawPoint()。
1、畫嘴巴
//畫起始笑臉 canvas.drawArc(-radius, -radius, radius, radius, startAngle, swipeAngle, false,facePaint);
這里的startAngle初始值為0,swiperAngle為180,半徑radius為40。
2、畫眼睛
(1)初始化眼睛坐標(biāo)
/**
* 初始化眼睛坐標(biāo)
*/
private void initEyes() {
//默認(rèn)兩個(gè)眼睛坐標(biāo)位置 角度轉(zhuǎn)弧度
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));
}
注意:需要將角度轉(zhuǎn)弧度
(2)開始畫眼睛
//畫起始眼睛 canvas.drawPoint(leftEyeX, leftEyeY, eyePaint); canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
二、合并狀態(tài)
這個(gè)狀態(tài)可以分為兩部分
- 嘴巴的旋轉(zhuǎn)
- 眼睛的旋轉(zhuǎn)
1、嘴巴的旋轉(zhuǎn)
開啟動(dòng)畫
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();
}
});
//動(dòng)畫延遲500ms啟動(dòng)
faceLoadingAnimator.setStartDelay(200);
faceLoadingAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//恢復(fù)起始狀態(tài)
currentStatus = smileStatus;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
動(dòng)畫執(zhí)行時(shí)間1s,記錄動(dòng)畫當(dāng)前執(zhí)行進(jìn)度值,存放在faceValue中。當(dāng)動(dòng)畫執(zhí)行結(jié)束的時(shí)候,需要將狀態(tài)恢復(fù)到默認(rèn)狀態(tài),調(diào)用invalidate的時(shí)候,進(jìn)入onDraw()方法,開始重新繪制嘴巴。
//記錄時(shí)刻的旋轉(zhuǎn)角度
startAngle = faceValue * 360;
//追上右邊眼睛
if (startAngle >= 120 + startAngle / 2) {
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
//開始自轉(zhuǎn)一圈
mHandler.sendEmptyMessage(2);
//此時(shí)記錄自轉(zhuǎn)一圈起始的角度
circleStartAngle = 120 + startAngle / 2;
} else {
//追眼睛的過程
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
}
這里的每次旋轉(zhuǎn)角度為startAngle。當(dāng)完全追趕上右側(cè)眼睛的時(shí)候,開始執(zhí)行自轉(zhuǎn)一周,并停止當(dāng)前動(dòng)畫。
2、眼睛的旋轉(zhuǎn)
眼睛的開始旋轉(zhuǎn)速度明顯是慢于嘴巴的旋轉(zhuǎn)速度,所以每次的旋轉(zhuǎn)速度可以設(shè)置為嘴巴的一半
//畫左邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的一半,這樣笑臉才能追上眼睛 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); //畫右邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的一半,這樣笑臉才能追上眼睛 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);
三、自轉(zhuǎn)狀態(tài)
1、開啟動(dòng)畫
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);
四、分離狀態(tài)
主要的注意點(diǎn)就是眼睛的旋轉(zhuǎn)角度設(shè)置為嘴巴旋轉(zhuǎn)角度的2倍,這樣才會(huì)達(dá)到眼睛超過嘴巴的效果,主要的旋轉(zhuǎn)代碼如下:
startAngle = faceValue * 360;
//判斷當(dāng)前笑臉的起點(diǎn)是否已經(jīng)走過260度 (吐出眼睛的角度,角度可以任意設(shè)置)
if (startAngle >= splitAngle) {
//畫左邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的2倍,這樣眼睛才能快于笑臉旋轉(zhuǎn)速度
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);
//畫右邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的2倍,這樣眼睛才能快于笑臉旋轉(zhuǎn)速度
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;
//笑臉狀態(tài)(一個(gè)臉,兩個(gè)眼睛)
private final int smileStatus = 0;
//加載狀態(tài) 合并眼睛,旋轉(zhuǎn)
private final int loadingStatus = 1;
//合并完成 轉(zhuǎn)一圈
private final int circleStatus = 2;
//轉(zhuǎn)圈完成 吐出眼睛
private final int splitStatus = 3;
//當(dāng)前狀態(tài)
private int currentStatus = smileStatus;
//笑臉畫筆
private Paint facePaint;
//眼睛畫筆
private Paint eyePaint;
//笑臉開始角度
private float startAngle;
//笑臉弧度
private float swipeAngle;
//左側(cè)眼睛起點(diǎn)x軸坐標(biāo)
private float leftEyeX = 0;
//左側(cè)眼睛起點(diǎn)y軸坐標(biāo)
private float leftEyeY = 0;
//右側(cè)眼睛起點(diǎn)x軸坐標(biāo)
private float rightEyeX;
//右側(cè)眼睛起點(diǎn)y軸坐標(biāo)
private float rightEyeY;
//一開始默認(rèn)狀態(tài)笑臉轉(zhuǎn)圈動(dòng)畫
private ValueAnimator faceLoadingAnimator;
//吞并完成后,自轉(zhuǎn)一圈動(dòng)畫
private ValueAnimator circleAnimator;
//faceLoadingAnimator動(dòng)畫進(jìn)度值
private float faceValue;
//circleAnimator動(dòng)畫進(jìn)度值
private float circleValue;
//記錄開始自轉(zhuǎn)一圈的起始角度
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();
//開始默認(rèn)動(dòng)畫
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);
}
/**
* 初始化眼睛坐標(biāo)
*/
private void initEyes() {
//默認(rèn)兩個(gè)眼睛坐標(biāo)位置 角度轉(zhuǎn)弧度
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:
//啟動(dòng)一開始笑臉轉(zhuǎn)圈動(dòng)畫,并且開始合并眼睛
currentStatus = loadingStatus;
faceLoadingAnimator.start();
break;
case 2:
//暫停眼睛和笑臉動(dòng)畫
currentStatus = circleStatus;
faceLoadingAnimator.pause();
//啟動(dòng)笑臉自轉(zhuǎn)一圈動(dòng)畫
circleAnimator.start();
break;
case 3:
//恢復(fù)笑臉轉(zhuǎn)圈動(dòng)畫,并且開始分離眼睛
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) {
//起始狀態(tài)
case smileStatus:
//起始角度為0
startAngle = initStartAngle;
//畫起始笑臉
canvas.drawArc(-radius, -radius, radius, radius, startAngle, swipeAngle, false,
facePaint);
//重置起始眼睛坐標(biāo)
initEyes();
//畫起始眼睛
canvas.drawPoint(leftEyeX, leftEyeY, eyePaint);
canvas.drawPoint(rightEyeX, rightEyeY, eyePaint);
//更改狀態(tài),進(jìn)行笑臉合并眼睛
mHandler.sendEmptyMessage(1);
break;
//合并狀態(tài)
case loadingStatus:
//記錄時(shí)刻的旋轉(zhuǎn)角度
startAngle = faceValue * 360;
//追上右邊眼睛
if (startAngle >= 120 + startAngle / 2) {
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
//開始自轉(zhuǎn)一圈
mHandler.sendEmptyMessage(2);
//此時(shí)記錄自轉(zhuǎn)一圈起始的角度
circleStartAngle = 120 + startAngle / 2;
} else {
//追眼睛的過程
canvas.drawArc(-radius, -radius, radius, radius, startAngle,
swipeAngle, false, facePaint);
}
//畫左邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的一半,這樣笑臉才能追上眼睛
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);
//畫右邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的一半,這樣笑臉才能追上眼睛
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;
//自轉(zhuǎn)一圈狀態(tài) circleValue * 360 為旋轉(zhuǎn)角度
case circleStatus:
canvas.drawArc(-radius, -radius, radius, radius,
circleStartAngle + circleValue * 360,
swipeAngle, false, facePaint);
break;
//笑臉眼睛分離狀態(tài)
case splitStatus:
startAngle = faceValue * 360;
//判斷當(dāng)前笑臉的起點(diǎn)是否已經(jīng)走過260度 (吐出眼睛的角度,角度可以任意設(shè)置)
if (startAngle >= splitAngle) {
//畫左邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的2倍,這樣眼睛才能快于笑臉旋轉(zhuǎn)速度
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);
//畫右邊眼睛 ,旋轉(zhuǎn)的角度設(shè)置為笑臉旋轉(zhuǎn)角度的2倍,這樣眼睛才能快于笑臉旋轉(zhuǎn)速度
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;
}
}
/**
* 初始化動(dòng)畫
*/
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();
}
});
//動(dòng)畫延遲500ms啟動(dòng)
faceLoadingAnimator.setStartDelay(200);
faceLoadingAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//恢復(fù)起始狀態(tài)
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"/>
完整代碼都在上面啦.
到這里就結(jié)束啦.
以上就是Android實(shí)現(xiàn)笑臉進(jìn)度加載動(dòng)畫的詳細(xì)內(nèi)容,更多關(guān)于Android 笑臉進(jìn)度加載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Android自定義加載圈動(dòng)畫效果
- Android實(shí)現(xiàn)仿微軟系統(tǒng)加載動(dòng)畫效果
- android實(shí)現(xiàn)加載動(dòng)畫對(duì)話框
- Android 自定義加載動(dòng)畫Dialog彈窗效果的示例代碼
- android自定義波浪加載動(dòng)畫的實(shí)現(xiàn)代碼
- Android仿視頻加載旋轉(zhuǎn)小球動(dòng)畫效果的實(shí)例代碼
- Android 使用 Path 實(shí)現(xiàn)搜索動(dòng)態(tài)加載動(dòng)畫效果
- Android使用View Animation實(shí)現(xiàn)動(dòng)畫加載界面
- Android使用lottie加載json動(dòng)畫的示例代碼
- Android實(shí)現(xiàn)仿iOS菊花加載圈動(dòng)畫效果
相關(guān)文章
android幾種不同對(duì)話框的實(shí)現(xiàn)方式
這篇文章介紹了android幾種不同對(duì)話框的實(shí)現(xiàn),主要包括:1、顯示提示消息的對(duì)話框.2、簡(jiǎn)單列表項(xiàng)對(duì)話框。3、單選列表項(xiàng)對(duì)話框。4、多選列表對(duì)話框。5、自定義列表項(xiàng)對(duì)話框。6、自定義View的對(duì)話框,需要的朋友可以參考下2015-08-08
Android App開發(fā)中創(chuàng)建Fragment組件的教程
這篇文章主要介紹了Android App開發(fā)中創(chuàng)建Fragment的教程,Fragment是用以更靈活地構(gòu)建多屏幕界面的可UI組件,需要的朋友可以參考下2016-05-05
Android?App跳轉(zhuǎn)微信小程序踩坑實(shí)戰(zhàn)
現(xiàn)在市面上很多的應(yīng)用都可以實(shí)現(xiàn)相互跳轉(zhuǎn),下面這篇文章主要給大家介紹了關(guān)于Android?App跳轉(zhuǎn)微信小程序踩坑的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05
Android view更改背景資源與padding消失的問題解決辦法
這篇文章主要介紹了Android view更改背景資源與padding消失的問題解決辦法的相關(guān)資料,需要的朋友可以參考下2017-04-04
Android實(shí)現(xiàn)局部圖片滑動(dòng)指引效果示例
現(xiàn)在滑動(dòng)效果用的比較多,尤其是在手機(jī)端上面,本文介紹了Android實(shí)現(xiàn)局部圖片滑動(dòng)指引效果示例,現(xiàn)在就分享給大家,也給大家做個(gè)參考。2016-10-10
android實(shí)現(xiàn)簡(jiǎn)單左滑刪除控件
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)一個(gè)簡(jiǎn)單左滑刪除控件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
快速解決fragment中onActivityResult不調(diào)用的問題
下面小編就為大家?guī)硪黄焖俳鉀Qfragment中onActivityResult不調(diào)用的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04
Android 獲取設(shè)備屏幕大小的幾種方法總結(jié)
這篇文章主要介紹了Android 獲取設(shè)備屏幕大小的幾種方法總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-05-05
Android彈出dialog后無法捕捉back鍵的解決方法
這篇文章主要為大家詳細(xì)介紹了Android彈出dialog后無法捕捉back鍵的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09

