效果
知识点
三角函数 动画结合画笔使用 状态机模式
关于数学中的度再逼逼两句
我们数学里面的度一半是是弧度的意思。即在一个圆中,角度代表当前角度对应的弧长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