效果

知识点

三角函数 动画结合画笔使用 状态机模式
关于数学中的度再逼逼两句
我们数学里面的度一半是是弧度的意思。即在一个圆中,角度代表当前角度对应的弧长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;}@Overrideprotected void onDraw(Canvas canvas) {if (mCurrentAnimateStatus == null) {// 初始状态为RotateAnimationStatusmCurrentAnimateStatus = new RotateAnimationStatus();}// 各个状态进行各自的绘制mCurrentAnimateStatus.draw(canvas);}@Overrideprotected 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进入MergeAnimationStatusmCurrentAnimateStatus = 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();}@Overridevoid draw(Canvas canvas) {drawSmallCircle(canvas);}@Overridevoid cancel() {rotateAnimator.cancel();}@Overridevoid pause() {rotateAnimator.pause();}@Overridevoid 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() {@Overridepublic void onAnimationEnd(Animator animation) {mCurrentAnimateStatus = new ExtendAnimationStatus();//从MergeAnimationStatus进入ExtendAnimationStatusLog.e("TAG", "MergeAnimationStatus: ");}});mValueAnimator.start();}@Overridevoid draw(Canvas canvas) {drawSmallCircle(canvas);}@Overridevoid cancel() {mValueAnimator.cancel();}@Overridevoid pause() {mValueAnimator.pause();}@Overridevoid 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() {@Overridepublic void onAnimationEnd(Animator animation, boolean isReverse) {//最后的动画执行完毕 释放资源mCurrentAnimateStatus = null;RotateLoadingView.this.setVisibility(View.GONE);Log.e("TAG", "ExtendAnimationStatus: ");}});mAnimator.start();}@Overridevoid 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);}@Overridevoid cancel() {mAnimator.cancel();}@Overridevoid pause() {mAnimator.pause();}@Overridevoid 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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 模拟获取后台数据完毕loadingView = findViewById(R.id.loading_view);new Handler().postDelayed(() -> loadingView.loadComplete(), 2000);}@Overrideprotected void onPause() {super.onPause();RotateLoadingView.AnimateStatus status = loadingView.getCurrentAnimateStatus();if (status != null) {status.pause();}}@Overrideprotected 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"><TextViewandroid:textColor="@android:color/background_dark"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!" /><com.example.rotateloadingview.RotateLoadingViewandroid: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);}@Overrideprotected 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

红橙Darren视频笔记 旋转加载界面相关推荐

  1. 红橙Darren视频笔记 类加载机制(API28) 自己写个热修复 查看源码网站

    第一部分 类加载机制 一个Activity是如何被Android虚拟机找到的? 在之前的文章 红橙Darren视频笔记 自定义View总集篇(https://blog.csdn.net/u011109 ...

  2. 红橙Darren视频笔记 利用阿里巴巴AndFix进行热修复

    注意 由于AndFix在2017年左右就停止更新了,在最新版本的apk上遇到很多问题,我最终也没有成功进行热修复.本节主要是学习热修复的原理 在上一篇 红橙Darren视频笔记 自己捕获异常并保存到本 ...

  3. 红橙Darren视频笔记 仿QQ侧滑效果

    这一篇没有什么新的内容 就是改写 红橙Darren视频笔记 仿酷狗侧滑效果 的侧滑的效果 1.去掉淡入淡出效果 2.加上黑色模板效果 效果: 去掉淡入淡出效果很简单 就是注释掉onScrollChan ...

  4. 红橙Darren视频笔记 ViewGroup事件分发分析 基于API27

    本节目标,通过案例,先看程序运行结果,然后跟踪源码,理解为什么会有这样的输出,继而理解view group的分发机制,感觉和证明题很像呢. 考虑以下程序的运行结果: case1: public cla ...

  5. 红橙Darren视频笔记 UML图简介

    整体架构复制自红橙原视频的课堂笔记 因为他这一课没有博客,所以没有转载链接,CSDN没有转载地址是无法作为转载类型的文章发表的,暂时标记为原创 参考链接 https://blog.csdn.net/r ...

  6. 红橙Darren视频笔记 代理模式 动态代理和静态代理

    红橙Darren视频笔记 代理模式 动态代理和静态代理(Android API 25) 关于代理模式我之前有过相关的介绍: https://blog.csdn.net/u011109881/artic ...

  7. 红橙Darren视频笔记 Behavior的工作原理源码分析

    主要coordinatorlayout的代码来自coordinatorlayout-1.0.0-sources.jar 本文从源码介绍 CoordinatorLayout 的 behavior 怎么工 ...

  8. 红橙Darren视频笔记 动画讲解 仿58同城 加载动画

    参考链接 https://www.jianshu.com/p/e4de28b4d8ac 效果 一.动画分类介绍 帧动画 和 补间动画 帧动画:一张一张的图片不断轮巡播放 补间动画:位移,透明度,像缩放 ...

  9. 红橙Darren视频笔记setContentView源码分析 xml加载的过程

    setContentView过程分析 从继承Activity的类开始进行分析 MainActivity setContentView(R.layout.activity_main); Activity ...

最新文章

  1. python turtle画气球-菲菲用python编程绘制的父亲节礼物
  2. personal-index 我的个人主页的介绍
  3. python测试代码运行时间_10种检测Python程序运行时间、CPU和内存占用的方法
  4. Python学习笔记:入门(1)
  5. java防止批量攻击_java 防止 XSS 攻击的常用方法总结
  6. 基于轻量型Web服务器Raspkate的RESTful API的实现
  7. Kotlin Weekly 中文周报 —— 16
  8. 基于大数据的精准教学模式探究
  9. 写一函数,将一个3*3的整型矩阵转置
  10. 小程序错误:Setting data field collected to undefined is invalid.
  11. 悬浮窗java_Android悬浮窗示例(floatingwindow)
  12. OSPF协议介绍及配置
  13. PCM设备的E1接头
  14. Android开发之摇一摇
  15. webstorm设置Ctrl+滚轮缩放字体大小
  16. Memcached单键超1M数据量的拆分设计及测试
  17. smtp 送信error原因
  18. 单元测试框架unittest和HtmlTestRunner报告
  19. 无人便利店系统代理发展前景分析
  20. 在备份数据库过程中出现错误, 未能打开数据库‘msdb’, ‘msdb ‘ 数据库处于回避紧急模式!

热门文章

  1. Windows 平台下基于MinGW和Qt 的OpenCV 之CMake 项目配置
  2. Expected one result (or null) to be returned by selectOne(), but found: 2
  3. [Java][内存模型]
  4. [转] 数据库加锁 sql加锁的
  5. C语言课后习题(61)
  6. git 忽略__pycache___图解git,用手绘图带你理解git中分支的原理和应用
  7. 【每日一练 088】性能优化-SQL tuning(一)
  8. 从一个真实案例看性能差异问题处理方法论
  9. Oracle中drop_column的几种方式和风险
  10. 单进程架构数据库谨防隐形杀手