红橙Darren视频笔记 旋转加载界面

效果
在这里插入图片描述

知识点

三角函数 动画结合画笔使用 状态机模式
关于数学中的度再逼逼两句
我们数学里面的度一半是是弧度的意思。即在一个圆中,角度代表当前角度对应的弧长AB是圆半径r的几倍。

我们知道圆周计算公式C=2πr,因此360°的弧长对应弧度2π,因此我们平常所说的30° 60° 90°对应弧度是π/6 π/3 π/2。视频里面讲到的时候我还一脸懵,以前的知识忘得差不多了。。。

关于状态机的使用 我在自定义View中有详细说明,如果不明白可以自己搜索一下,是个比较简单的设计模式

实现思路分析

在这里插入图片描述
黑色的十字交叉出为View的中心 蓝色为小圆运行轨迹 其半径就是代码里面的大圆半径 红色小圆是各个小圆的一个代表,其位置由初始角度+当前旋转角度+大圆半径共同决定,其半径为代码里面的小圆半径
1 旋转动画(属性动画)
用画笔以屏幕中心为基准 画几个圆,圆的初始位置由初始角度决定。可以将圆的对应弧度2π分割为几等份,初始位置的x坐标=(中心位置x坐标+sin(初始角度)) 初始位置的y坐标=(中心位置y坐标+cos(初始角度))
想象一下 初始角度是0 sin(0) = 0 cos(0)=1 那么该圆在屏幕中间靠上的位置
最后利用ValueAnimator更新圆的位置
2 聚合动画
刚开始是往外面扩散使用差值器完成
聚合动画使用画笔结合属性动画实现 同样是利用ValueAnimator更新各个小圆的位置
3 扩散动画
利用ValueAnimator更新透明圆的半径,从0变化到对角线的一半,不停绘制空心圆

代码

自定义View

/**
 * Created by hjcai on 2021/1/14.
 * RotateLoadingView 存在三种动画状态 MergeAnimationStatus ExtendAnimationStatus RotateAnimationStatus
 * 三种动画状态的控制状态如下
 * <p>
 * 创建RotateLoadingView之后 RotateLoadingView自动进入RotateAnimationStatus状态 该状态要由外部打破 否则持续执行
 * 当外部加载完毕 调用loadComplete方法,RotateLoadingView取消RotateAnimationStatus状态  进入MergeAnimationStatus状态
 * MergeAnimationStatus动画执行完毕进入ExtendAnimationStatus状态 ExtendAnimationStatus执行完毕 将当前RotateLoadingView隐藏
 */
class RotateLoadingView extends View {
    // 动画时长
    private static final int ANIMATION_DURATION = 1500;
    // 当前动画状态
    AnimateStatus mCurrentAnimateStatus;
    // 绘制各种圆 背景的画笔
    Paint mPaint;
    // 是否已经初始化
    boolean mInitialized = false;
    // 小圆的几个颜色
    int[] mColors;
    // 每个小圆对应的角度(将2π分割为n份 n代表颜色的数目) 用于表示各个小圆初始的位置
    float mPerAngle;
    // 旋转经过的角度 结合每个圆对应的角度来表示旋转时各个小圆的位置
    float mRotatedAngle = 0;
    // view的宽高
    int mViewHeight, mViewWidth;
    // 大圆的半径
    float mBigCircleRadius;
    // 小圆的半径
    float mSmallCircleRadius;
    // 绘制各种状态时候的小圆的中心点
    int mCenterX, mCenterY;
    // 扩展动画透明洞洞的半径
    float mHoleRadius = 0;
    // View对角线的一半
    private float mDiagonalDist;

    public RotateLoadingView(Context context) {
        this(context, null);
    }

    public RotateLoadingView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RotateLoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (!mInitialized) {
            initParams();
        }
    }

    private void initParams() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        int color0 = Color.parseColor("#FFAAFC");
        int color1 = Color.parseColor("#FFCCCC");
        int color2 = Color.parseColor("#CCFFCC");
        int color3 = Color.parseColor("#CCCCFF");
        int color4 = Color.parseColor("#CCFFFF");
        int color5 = Color.parseColor("#00FF00");
        mColors = new int[]{color0, color1, color2, color3, color4, color5};
        mPerAngle = (float) (Math.PI * 2 / mColors.length);
        mInitialized = true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mCurrentAnimateStatus == null) {
            // 初始状态为RotateAnimationStatus
            mCurrentAnimateStatus = new RotateAnimationStatus();
        }
        // 各个状态进行各自的绘制
        mCurrentAnimateStatus.draw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 初始化依赖获取view宽高的变量
        mViewHeight = MeasureSpec.getSize(heightMeasureSpec);
        mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
        mBigCircleRadius = Math.min(mViewHeight, mViewWidth) / 4;
        mSmallCircleRadius = mBigCircleRadius / 8;
        mCenterX = mViewWidth / 2;
        mCenterY = mViewHeight / 2;
        mDiagonalDist = (float) Math.sqrt(mCenterX * mCenterX + mCenterY * mCenterY);
    }

    // 当数据加载完毕 调用该方法 来停掉旋转动画并开启聚合动画
    public void loadComplete() {
        if (mCurrentAnimateStatus == null) {
            return;
        }
        mCurrentAnimateStatus.cancel();
        // 从RotateAnimationStatus进入MergeAnimationStatus
        mCurrentAnimateStatus = new MergeAnimationStatus();
    }

    //复用方法 RotateAnimationStatus和MergeAnimationStatus可以公用该方法
    private void drawSmallCircle(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
        for (int i = 0; i < mColors.length; i++) {
            mPaint.setColor(mColors[i]);
            double currentAngle = mPerAngle * i + mRotatedAngle;
            canvas.drawCircle((float) (mCenterX + Math.sin(currentAngle) * mBigCircleRadius), (float) (mCenterY - Math.cos(currentAngle) * mBigCircleRadius), mSmallCircleRadius, mPaint);
        }
    }

    //旋转动画 本质是ValueAnimator改变mRotatedAngle 旋转角 然后在onDraw方法不停绘制
    class RotateAnimationStatus extends AnimateStatus {

        ValueAnimator rotateAnimator;

        public RotateAnimationStatus() {

            //想一想 逆时针旋转如何实现
            rotateAnimator = ValueAnimator.ofFloat(0, (float) (Math.PI * 2));
            rotateAnimator.setDuration(ANIMATION_DURATION);
            //默认的插值器走走停停 使用匀速的插值器替换
            rotateAnimator.setInterpolator(new LinearInterpolator());
            rotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
            rotateAnimator.addUpdateListener(animation -> {
                mRotatedAngle = (float) animation.getAnimatedValue();
                invalidate();
            });
            rotateAnimator.start();
        }

        @Override
        void draw(Canvas canvas) {
            drawSmallCircle(canvas);
        }

        @Override
        void cancel() {
            rotateAnimator.cancel();
        }

        @Override
        void pause() {
            rotateAnimator.pause();
        }

        @Override
        void resume() {
            rotateAnimator.resume();
        }

    }

    //聚合动画 本质是ValueAnimator改变mBigCircleRadius 大圆半径 然后在onDraw方法不停绘制
    class MergeAnimationStatus extends AnimateStatus {
        private final ValueAnimator mValueAnimator;

        public MergeAnimationStatus() {

            //大圆半径从大变小
            mValueAnimator = ValueAnimator.ofFloat(mBigCircleRadius, 0);
            mValueAnimator.setDuration(ANIMATION_DURATION / 2);
            mValueAnimator.addUpdateListener(animation -> {
                mBigCircleRadius = (float) animation.getAnimatedValue();// 最大半径到 0
                // 重新绘制
                invalidate();
            });
            // 开始的时候向后然后向前甩
            mValueAnimator.setInterpolator(new AnticipateInterpolator(3f));
            // 等聚合完毕画展开
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mCurrentAnimateStatus = new ExtendAnimationStatus();//从MergeAnimationStatus进入ExtendAnimationStatus
                    Log.e("TAG", "MergeAnimationStatus: ");
                }
            });
            mValueAnimator.start();
        }

        @Override
        void draw(Canvas canvas) {
            drawSmallCircle(canvas);
        }

        @Override
        void cancel() {
            mValueAnimator.cancel();
        }

        @Override
        void pause() {
            mValueAnimator.pause();
        }

        @Override
        void resume() {
            mValueAnimator.resume();
        }
    }

    //扩散动画 本质是利用mPaint绘制空心圆 ValueAnimator改变画笔的粗细和半径 然后在onDraw方法不停绘制
    class ExtendAnimationStatus extends AnimateStatus {
        private final ValueAnimator mAnimator;

        public ExtendAnimationStatus() {
            mAnimator = ValueAnimator.ofFloat(0, (float) Math.sqrt(mCenterX * mCenterX + mCenterY * mCenterY));//透明圆的半径从0到View对角线的一半
            mAnimator.setDuration(ANIMATION_DURATION);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(Color.WHITE);
            mAnimator.addUpdateListener(animation -> {
                mHoleRadius = (float) animation.getAnimatedValue(); // 0 - 对角线的一半
                invalidate();
            });
            mAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
                    //最后的动画执行完毕 释放资源
                    mCurrentAnimateStatus = null;
                    RotateLoadingView.this.setVisibility(View.GONE);
                    Log.e("TAG", "ExtendAnimationStatus: ");
                }
            });
            mAnimator.start();
        }

        @Override
        void draw(Canvas canvas) {
            // 画笔的宽度
            float strokeWidth = mDiagonalDist - mHoleRadius;
            // 直觉上会使用mHoleRadius作为半径 但是mHoleRadius从0变到mDiagonalDist
            // 相反的strokeWidth从mDiagonalDist变到0
            // 因此当使用mHoleRadius作为半径时 一开始的画笔很粗 即使我们空心圆的半径为0 也会画成实心圆 因为画笔很粗
            // 真实的半径(绘制的半径)=透明的半径+strokeWidth/2 重点!!!
            // 当strokeWidth/2>代码设置的绘制的半径时 我们看不到空心的部分
            float radius = strokeWidth / 2 + mHoleRadius;
            mPaint.setStrokeWidth(strokeWidth);
            canvas.drawCircle(mCenterX, mCenterY, radius, mPaint);
        }

        @Override
        void cancel() {
            mAnimator.cancel();
        }

        @Override
        void pause() {
            mAnimator.pause();
        }

        @Override
        void resume() {
            mAnimator.resume();
        }
    }

    public AnimateStatus getCurrentAnimateStatus() {
        return mCurrentAnimateStatus;
    }

    abstract class AnimateStatus {
        abstract void draw(Canvas canvas);

        abstract void cancel();

        abstract void pause();

        abstract void resume();
    }
}

Activity

public class MainActivity extends AppCompatActivity {
    RotateLoadingView loadingView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 模拟获取后台数据完毕
        loadingView = findViewById(R.id.loading_view);
        new Handler().postDelayed(() -> loadingView.loadComplete(), 2000);
    }

    @Override
    protected void onPause() {
        super.onPause();
        RotateLoadingView.AnimateStatus status = loadingView.getCurrentAnimateStatus();
        if (status != null) {
            status.pause();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        RotateLoadingView.AnimateStatus status = loadingView.getCurrentAnimateStatus();
        if (status != null) {
            status.resume();
        }
    }
}

布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ccc"
    tools:context=".MainActivity">

    <TextView
        android:textColor="@android:color/background_dark"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <com.example.rotateloadingview.RotateLoadingView
        android:id="@+id/loading_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

后记

1能不能画透明的圆 不能
我的原本想法是给画笔一个透明色去绘制一个透明的圆,发现完全没有作用,设置的透明色不会覆盖原来绘制的颜色 只能通过绘制空心圆来搞出透明的效果
2 释放资源
在所有动画结束之后 将布局隐藏
3 让动画支持暂停和重启
给AnimateStatus添加pause和resume方法 这样 activity在退出后台的时候可以支持动画暂停
4 关于画笔的半径 我在做扩展动画的时候想了好久才明白
在这里插入图片描述
记住 真实的半径(代码中绘制的半径)=透明的半径+strokeWidth/2 重点!!!
以上面的截图为例 在上面的案例中即 我们代码设置的半径100 = 透明的半径50 +strokeWidth100/2
如图 上方的黑色框为300*300px
画圆的代码如下:

    public TestRadius(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.STROKE);//空心圆
        mPaint.setStrokeWidth(100);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,100,mPaint);
    }

即我想绘制半径为100的圆 设置了画笔粗细为100,最终我们看到的效果时画了半径为150的圆
我们设置的半径100=内部的透明半径50+画笔粗细100/2
真实的半径(代码中绘制的半径)=透明的半径+strokeWidth/2 重点!!!

完整代码
https://github.com/caihuijian/learn_darren_android

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值