先看效果图:
这是我在某软件上看到的加载动画,感觉挺不错,就自己研究了一下。下面给大家分享一下该动画的实现过程
一、三个圆环的绘制和运动分析
来看下面这张解析图:
假设每个圆环的初始位置如上图,那么我们可以设定每一个圆环的位置以及它离控件边界的距离(w/6)
为了方便,我们定义控件宽度getWidth()为w,那么左上,右上,正下方圆环的圆心坐标依次为:
(w/4, w/4),(w*3/4, w/4),(w/2, w*3/4)
我们再来看下面这个动画:
发现了吧,实际上每个圆环都在做直线运动,三个圆环组合在一起就呈现出了相互靠近和远离的效果。
所以我们的最终目的是让这三个圆环延三条线段做匀速运动,当某一个圆环移动到了线段的端点时,就要改变它的运动状态,让它延另一条线段移动。
我们作图解析:
梳理一下:
- 处在左上角的圆环会延着编号1的线段移动,右上角的圆环会延着编号2的线段移动,正下方的圆环会延着编号3的线段移动。
- 彼此到达所在线段端点时(保证到达线段端点耗费的时间都相同),改变自身的运动状态,延着下一条线段继续运动。
二、圆环绘制和运动的代码解释
我们先创建一个MovePoint类,保存每个圆环的圆心坐标,所在直线的斜率与截距和每次移动的步长:
public class MovePoint {
private float x;
private float y;
private float k,b; //直线函数式中的斜率与截距
private float moveStep; //移动步长
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
setY(x*k+b);
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public void setCalculate(float k,float b){
this.k=k;
this.b=b;
}
public float getK() {
return k;
}
public void setK(float k) {
this.k = k;
}
public float getB() {
return b;
}
public void setB(float b) {
this.b = b;
}
public float getMoveStep() {
return moveStep;
}
public void setMoveStep(float moveStep) {
this.moveStep = moveStep;
}
}
初始化各属性:
private Paint circlePaint;
private int circleRadius=0; //圆的半径
private MovePoint[] movePoints=new MovePoint[3]; //记录三个圆环的位置,状态,和移动时的函数公式
private float[] x; //三个圆环的x坐标
private float[] k,b; //三个圆环所在直线的斜率与截距
private int moveTime=70; //移动到线段端点需要的时间
public MyThreeCircleLoadView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
circlePaint=new Paint();
circlePaint.setColor(Color.RED);
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setAntiAlias(true);
}
/*
* 在左上角向右边移动; y=getWidth()/4
* 在右上角向左下移动; y=-2*x+getWidth()*7/4
* 在正下方向左上移动; y=2*x-getWidth()/4
* */
private void setMovePoints(){
x=new float[]{getWidth()/4.0f,getWidth()*3/4.0f,getWidth()/2.0f};
k=new float[]{0,-2,2};b=new float[]{getWidth()/4.0f,getWidth()*7/4.0f,-getWidth()/4.0f};
int len=movePoints.length;
for(int i=0;i<len;i++){
movePoints[i]=new MovePoint();
movePoints[i].setCalculate(k[i],b[i]);
movePoints[i].setX(x[i]);
}
for(int i=0;i<len;i++)
movePoints[i].setMoveStep(getMoveStep(movePoints[(i+1)%len].getX(),movePoints[i].getX(),moveTime));
}
//计算移动步长(速度):两点距离/移动花费的时间
private float getMoveStep(float x1,float x2,int time){
return (x1-x2)/time;
}
注意移动步长的计算,因为我这里每次改变的是每个圆环的x坐标,而不是实际的线段距离,所以在getMoveStep()中,只需要(x1-x2)获得线段端点间的水平距离。
我们要求每个圆环到达线段端点的时间相等,由匀速运动公式可得v=(x/t),我们给定相同的t,x又是已知的,就可以得到每个圆环在当前线段上的速度v。
每个点都应该用float变量,否则会产生速度计算上的误差
绘制圆环:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (MovePoint movePoint : movePoints) {
canvas.drawCircle(movePoint.getX(), movePoint.getY(), circleRadius, circlePaint);
changeCircleState(movePoint);
}
move();
}
//改变圆环移动的状态
private void changeCircleState(MovePoint movePoint){
//从左上角向右移并移动到右上方顶点时
if (movePoint.getX() >= x[1] && movePoint.getY() == movePoint.getX() * k[0] + b[0]) {
movePoint.setCalculate(k[1], b[1]);
movePoint.setMoveStep(getMoveStep(x[2], x[1], moveTime));
}
//右上向坐下移动并移动到正下方顶点时
else if (movePoint.getX() <= x[2] && movePoint.getY() == movePoint.getX() * k[1] + b[1]) {
movePoint.setCalculate(k[2], b[2]);
movePoint.setMoveStep(getMoveStep(x[0], x[2], moveTime));
}
//从正下方向左上移动并移动到左上方顶点时
else if (movePoint.getX() <= x[0]) {
movePoint.setCalculate(k[0], b[0]);
movePoint.setMoveStep(getMoveStep(x[1], x[0], moveTime));
}
}
changeCircleState():用于判断圆环是否运动到线段的端点。若运动到了端点,就改变该圆环的运动状态和附属的线段
移动圆环:
//移动圆环
private void move(){
for (MovePoint movePoint : movePoints) {
movePoint.setX(movePoint.getX() + movePoint.getMoveStep());
}
postInvalidateDelayed(5);
}
下面是自定义View的完整代码:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.bean.MovePoint;
import androidx.annotation.Nullable;
public class MyThreeCircleLoadView extends View {
private Paint circlePaint;
private int circleRadius=0; //圆的半径
private MovePoint[] movePoints=new MovePoint[3]; //记录三个圆环的位置,状态,和移动时的函数公式
private float[] x; //三个圆环的x坐标
private float[] k,b; //三个圆环所在直线的斜率与截距
private int moveTime=70; //移动到线段端点需要的时间
public MyThreeCircleLoadView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
circlePaint=new Paint();
circlePaint.setColor(Color.RED);
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setAntiAlias(true);
}
/*
* 在左上角向右边移动; y=getWidth()/4
* 在右上角向左下移动; y=-2*x+getWidth()*7/4
* 在正下方向左上移动; y=2*x-getWidth()/4
* */
private void setMovePoints(){
x=new float[]{getWidth()/4.0f,getWidth()*3/4.0f,getWidth()/2.0f};
k=new float[]{0,-2,2};b=new float[]{getWidth()/4.0f,getWidth()*7/4.0f,-getWidth()/4.0f};
int len=movePoints.length;
for(int i=0;i<len;i++){
movePoints[i]=new MovePoint();
movePoints[i].setCalculate(k[i],b[i]);
movePoints[i].setX(x[i]);
}
for(int i=0;i<len;i++)
movePoints[i].setMoveStep(getMoveStep(movePoints[(i+1)%len].getX(),movePoints[i].getX(),moveTime));
}
//计算移动步长(速度):两点距离/移动花费的时间
private float getMoveStep(float x1,float x2,int time){
return (x1-x2)/time;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
circleRadius=getWidth()/4-getWidth()/6;
circlePaint.setStrokeWidth(5+getWidth()/100);
setMovePoints();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (MovePoint movePoint : movePoints) {
canvas.drawCircle(movePoint.getX(), movePoint.getY(), circleRadius, circlePaint);
changeCircleState(movePoint);
}
move();
}
//改变圆环移动的状态
private void changeCircleState(MovePoint movePoint){
//从左上角向右移并移动到右上方顶点时
if (movePoint.getX() >= x[1] && movePoint.getY() == movePoint.getX() * k[0] + b[0]) {
movePoint.setCalculate(k[1], b[1]);
movePoint.setMoveStep(getMoveStep(x[2], x[1], moveTime));
}
//右上向坐下移动并移动到正下方顶点时
else if (movePoint.getX() <= x[2] && movePoint.getY() == movePoint.getX() * k[1] + b[1]) {
movePoint.setCalculate(k[2], b[2]);
movePoint.setMoveStep(getMoveStep(x[0], x[2], moveTime));
}
//从正下方向左上移动并移动到左上方顶点时
else if (movePoint.getX() <= x[0]) {
movePoint.setCalculate(k[0], b[0]);
movePoint.setMoveStep(getMoveStep(x[1], x[0], moveTime));
}
}
//移动圆环
private void move(){
for (MovePoint movePoint : movePoints) {
movePoint.setX(movePoint.getX() + movePoint.getMoveStep());
}
postInvalidateDelayed(5);
}
}
xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<com.myviewtext.MyThreeCircleLoadView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"/>
</RelativeLayout>