效果
思路
思路比较简单 先自定义view 该view只是一个圆,可以设置绘制的颜色。再自定义一个ViewGroup,在里面放三个之前自定义好的view。初始化的部分就完成了。下面接着看动画部分,动画可以分为两部分,一部分是向外移动 一部分是向内移动,这里使用属性动画+AnimatorSet很容易实现,接着就是监听动画执行完毕,执行向内移动的动画,向内移动的动画结束执行向外移动的动画,循环执行。注意需要提供一个方法,在加载完毕的时候停止动画释放资源。
遇到的坑
1.crash问题
2021-01-09 15:43:03.653 21938-21938/com.example.circleloadingview E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.circleloadingview, PID: 21938
java.lang.NullPointerException: Attempt to get length of null array
at android.animation.ValueAnimator.initAnimation(ValueAnimator.java:555)
at android.animation.ObjectAnimator.initAnimation(ObjectAnimator.java:894)
at android.animation.ValueAnimator.startAnimation(ValueAnimator.java:1231)
at android.animation.ValueAnimator.start(ValueAnimator.java:1041)
at android.animation.ValueAnimator.start(ValueAnimator.java:1065)
at android.animation.ObjectAnimator.start(ObjectAnimator.java:852)
at android.animation.ValueAnimator.startWithoutPulsing(ValueAnimator.java:1058)
at android.animation.AnimatorSet.handleAnimationEvents(AnimatorSet.java:1142)
at android.animation.AnimatorSet.startAnimation(AnimatorSet.java:1227)
at android.animation.AnimatorSet.start(AnimatorSet.java:729)
at android.animation.AnimatorSet.start(AnimatorSet.java:684)
at com.example.circleloadingview.CircleLoadingView.startInAnimate(CircleLoadingView.java:106)
at com.example.circleloadingview.CircleLoadingView.lambda$nASjswge2JPrJsLIt_Asff7ch5s(Unknown Source:0)
原因出在创建ObjectAnimator的方式上
不应该这样创建
ObjectAnimator translationRightIn = new ObjectAnimator();
translationRightIn.ofFloat(mCircleRight, "translationX", mAnimateDistance, 0);
而应该这样创建
ObjectAnimator translationRightIn = ObjectAnimator.ofFloat(mCircleRight, "translationX", mAnimateDistance, 0);
其实Android studio一开始就有警告了
Static member ‘android.animation.ObjectAnimator.ofFloat(java.lang.Object, java.lang.String, float…)’ accessed via instance reference
只不过被我忽视了,自己坑自己…
2.复用AnimatorSet时遇到问题
private void innerAnimation() {
// 左边跑
ObjectAnimator leftTranslationAnimator = ObjectAnimator.ofFloat(mLeftView,"translationX",-mTranslationDistance,0);
// 右边跑
ObjectAnimator rightTranslationAnimator = ObjectAnimator.ofFloat(mRightView,"translationX",mTranslationDistance,0);
AnimatorSet set = new AnimatorSet();
set.setInterpolator(new AccelerateInterpolator());
set.setDuration(ANIMATION_TIME);
set.playTogether(leftTranslationAnimator,rightTranslationAnimator);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 往里外面跑
// 切换颜色顺序 左边的给中间 中间的给右边 右边的给左边
int leftColor = mLeftView.getColor();
int rightColor = mRightView.getColor();
int middleColor = mMiddleView.getColor();
mMiddleView.exchangeColor(leftColor);
mRightView.exchangeColor(middleColor);
mLeftView.exchangeColor(rightColor);
expendAnimation();
}
});
set.start();
}
这是视频里面的代码 我看到执行动画每次都会创建一个AnimatorSet,于是想将他抽成类变量,结果动画出现卡顿,之后竟然ANR了,后面发现虽然我把AnimatorSet抽成类变量,但是每次动画都会执行set.setInterpolator set.setDuration set.playTogether set.addListener很大可能是这里出错了,后面我加了判断,只有初始化的时候才执行就OK了
3.其他优化
利用AnimatorSet的reverse方法 可以只使用一个动画 另外一个动画反过来执行就可以,不过这样用起来逻辑可能比较混乱,有时候代码复用之后,逻辑会变得没有原来清晰,这时候,是否复用代码就见仁见智了.
这里我虽然发现可以使用AnimatorSet的reverse方法减少很多冗余代码,但是为了让逻辑清晰,我最终放弃了这种方案.
@RequiresApi(api = Build.VERSION_CODES.O)
private void startInAnimate() {
Log.d(TAG, "startInAnimate: ");
if (animatorSet4In == null) {
animatorSet4In = new AnimatorSet();
ObjectAnimator translationLeftIn = ObjectAnimator.ofFloat(mCircleLeft, "translationX", -mAnimateDistance, 0);
ObjectAnimator translationRightIn = ObjectAnimator.ofFloat(mCircleRight, "translationX", mAnimateDistance, 0);
animatorSet4In.setInterpolator(new AccelerateInterpolator(2f));
animatorSet4In.playTogether(translationLeftIn, translationRightIn);
animatorSet4In.setDuration(mAnimateDuration);
animatorSet4In.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
i++;
if (i % 2 != 0) {
exchangeColor();
animatorSet4In.reverse();
Log.d(TAG, "startInAnimate reverse: "+i);
} else {
Log.d(TAG, "startInAnimate start: "+i);
animatorSet4In.start();
}
}
});
}
Log.d(TAG, "startInAnimate first: "+i);
animatorSet4In.start();
}
最终代码
自定义View
class SingleCircle extends View {
private Paint mPaint;
private int mRadius;
private int mColor;
public SingleCircle(Context context) {
this(context, null);
}
public SingleCircle(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SingleCircle(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
@Override
protected void onDraw(Canvas canvas) {
//onMeasure onLayout onDraw的执行顺序 此时可以知道测量宽高
mRadius = Math.min(getMeasuredHeight(), getMeasuredWidth()) / 2;
canvas.drawCircle(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, Util.dip2px(mRadius, getContext()), mPaint);
}
public void changeColor(int color) {
mPaint.setColor(color);
mColor = color;
invalidate();//交换颜色之后需要重新绘制 否则颜色没有实际变化
}
public int getColor() {
return mColor;
}
}
自定义ViewGroup
class CircleLoadingView extends RelativeLayout {
private static final String TAG = "CircleLoadingView";
private SingleCircle mCircleLeft, mCircleCenter, mCircleRight;
private float mAnimateDistance;
private int mAnimateDuration = 500;
AnimatorSet mAnimatorSet4Out;
AnimatorSet mAnimatorSet4In;
public CircleLoadingView(Context context) {
this(context, null);
}
public CircleLoadingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mAnimateDistance = Util.dip2px(80, context);
inflate(context, R.layout.layout_circle_loading_view, this);
mCircleLeft = findViewById(R.id.circle_left);
mCircleCenter = findViewById(R.id.circle_center);
mCircleRight = findViewById(R.id.circle_right);
mCircleLeft.changeColor(Color.RED);
mCircleCenter.changeColor(Color.GREEN);
mCircleRight.changeColor(Color.BLUE);
post(this::startOutAnimate);
}
//往外跑
private void startOutAnimate() {
if (mAnimatorSet4Out == null) {
ObjectAnimator translationLeftOut = ObjectAnimator.ofFloat(mCircleLeft, "translationX", 0, -mAnimateDistance);
ObjectAnimator translationRightOut = ObjectAnimator.ofFloat(mCircleRight, "translationX", 0, mAnimateDistance);
mAnimatorSet4Out = new AnimatorSet();
mAnimatorSet4Out.setInterpolator(new DecelerateInterpolator(2f));//2f表示比原来的动画更明显 减速动画
mAnimatorSet4Out.playTogether(translationLeftOut, translationRightOut);
mAnimatorSet4Out.setDuration(mAnimateDuration);
mAnimatorSet4Out.start();
mAnimatorSet4Out.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
startInAnimate();//往里跑
}
});
}
mAnimatorSet4Out.start();
}
//往里跑
private void startInAnimate() {
if (mAnimatorSet4In == null) {
ObjectAnimator translationLeftIn = ObjectAnimator.ofFloat(mCircleLeft, "translationX", -mAnimateDistance, 0);
ObjectAnimator translationRightIn = ObjectAnimator.ofFloat(mCircleRight, "translationX", mAnimateDistance, 0);
mAnimatorSet4In = new AnimatorSet();
mAnimatorSet4In.setInterpolator(new AccelerateInterpolator(2f));//2f表示比原来的动画更明显 加速动画
mAnimatorSet4In.playTogether(translationLeftIn, translationRightIn);
mAnimatorSet4In.setDuration(mAnimateDuration);
mAnimatorSet4In.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
exchangeColor();//交换颜色
startOutAnimate();//往外跑
}
});
}
mAnimatorSet4In.start();
}
//点集中到中间 交换颜色
private void exchangeColor() {
int leftColor = mCircleLeft.getColor();
int centerColor = mCircleCenter.getColor();
int rightColor = mCircleRight.getColor();
mCircleCenter.changeColor(leftColor);
mCircleRight.changeColor(centerColor);
mCircleLeft.changeColor(rightColor);
}
//释放资源
public void loadingComplete() {
mAnimatorSet4Out.cancel();
mAnimatorSet4In.cancel();
removeAllViews();
}
}
自定义ViewGroup的布局
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.circleloadingview.SingleCircle
android:id="@+id/circle_left"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_centerInParent="true" />
<com.example.circleloadingview.SingleCircle
android:id="@+id/circle_right"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_centerInParent="true" />
<com.example.circleloadingview.SingleCircle
android:id="@+id/circle_center"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_centerInParent="true" />
</RelativeLayout>
Activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CircleLoadingView circleLoadingView = findViewById(R.id.loadingView);
circleLoadingView.postDelayed(circleLoadingView::loadingComplete, 1000 * 10);//10s后假装加载完毕
}
}
Activity布局
<?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"
tools:context=".MainActivity">
<com.example.circleloadingview.CircleLoadingView
android:id="@+id/loadingView"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
后记
没有做这个自定义View的时候,我觉得这是个很简单的功能,预计最多两小时完成.视频里面也说个小时就可以收工.然而由于各种问题+优化+笔记,最后我还是花了一个下午才完成.即使是很小的东西,我也从中学到了不少东西,比如ObjectAnimator创建方式不对会导致crash,AnimatorSet的复用如果不正确会导致动画ANR,代码的复用与逻辑的清晰有时不能兼得.
所以再小的东西,也要认真对待呀.
收工!
代码:
https://github.com/caihuijian/learn_darren_android