自定义View实现圆环环绕的加载动画

23 篇文章 0 订阅

先看效果图:

这是我在某软件上看到的加载动画,感觉挺不错,就自己研究了一下。下面给大家分享一下该动画的实现过程

一、三个圆环的绘制和运动分析

来看下面这张解析图:

假设每个圆环的初始位置如上图,那么我们可以设定每一个圆环的位置以及它离控件边界的距离(w/6)

为了方便,我们定义控件宽度getWidth()为w,那么左上,右上,正下方圆环的圆心坐标依次为:

(w/4, w/4),(w*3/4, w/4),(w/2, w*3/4)

我们再来看下面这个动画:

发现了吧,实际上每个圆环都在做直线运动,三个圆环组合在一起就呈现出了相互靠近和远离的效果。

所以我们的最终目的是让这三个圆环延三条线段做匀速运动,当某一个圆环移动到了线段的端点时,就要改变它的运动状态,让它延另一条线段移动。

我们作图解析:

梳理一下:

  1. 处在左上角的圆环会延着编号1的线段移动,右上角的圆环会延着编号2的线段移动,正下方的圆环会延着编号3的线段移动。
  2. 彼此到达所在线段端点时(保证到达线段端点耗费的时间都相同),改变自身的运动状态,延着下一条线段继续运动。

二、圆环绘制和运动的代码解释

我们先创建一个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>

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值