欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

利用属性动画实现一个不一样的SplashView

程序员文章站 2022-05-27 13:39:59
...

直接上效果
利用属性动画实现一个不一样的SplashView

这个效果是在学属性动画时做的一个效果,现在就当复习了,顺便记录下来。

自定义View重写onDraw方法,用属性动画控制旋转和缩放效果。
动画还是比较简单的可以分为三个过程:
state1.六个小球的旋转
state2.小球向中间收缩
state3.展开显示出下面的内容

我们可以想象成这种场景:我们从服务器请求数据时,开始让小球执行旋转动画,当数据请求成功加载完成后执行收缩动画和展开动画显示内容。

这里我们小小用了一下设计模式——策略模式,我们把每个state定义成一个类,让每个类去实现它自己的动画。

首先定一个抽象类,里面定一个drawState方法。

private SplashState mState;

    private abstract class SplashState{
        public abstract void drawState(Canvas canvas);
    }

然后我们有三个状态,定义三个类,分别实现我们的SplashState
重写drawState方法;

旋转状态 ⬇️

public class RotateState extends SplashState{
        @Override
        public void drawState(Canvas canvas) {

        }
    }

收缩状态⬇️

public class MergeState extends SplashState{  
        @Override
        public void drawState(Canvas canvas) {

        }
    }

展开状态⬇️

public class ExpandState extends SplashState{
        @Override
        public void drawState(Canvas canvas) {

        }
    }

这样一来每种模式的动画都有它们自己去实现了,在我们的onDraw方法里就不用if else去判断三种模式了,可以这样写:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mState==null){
            mState = new RotateState();
        }
        mState.drawState(canvas);

    }

接下来就挨个分析每种状态了,首先旋转状态:

public class RotateState extends SplashState{
        @Override
        public void drawState(Canvas canvas) {
            drawBackground(canvas);
            drawCircles(canvas);
        }
    }

drawState里就是画背景和画圆
drawBackground画背景

private void drawBackground(Canvas canvas) {
        canvas.drawColor(mBackgroundColor);
    }

drawCircles画圆

private void drawCircles(Canvas canvas) {
        float spaceAngle = (float) (Math.PI * 2 / mCircleColors.length);

        for (int i=0;i<mCircleColors.length;i++){
            double angle = spaceAngle * i + mRotateAngle;
            float cx = (float) (mCenterX + mCurrentRotateRadius * Math.cos(angle));
            float cy = (float) (mCenterY + mCurrentRotateRadius * Math.sin(angle));
            mCirclePaint.setColor(mCircleColors[i]);
            canvas.drawCircle(cx,cy,mCircleRadius,mCirclePaint);
        }
    }

六个圆按一周平均分布,spaceAngle是每个圆之间间隔角度,mCenterX,mCenterY是中心点的坐标,cx,cy是最终要确定的每个小圆的圆心,很简单的计算得到cx,cy的值,mCircleRadius为小圆的半径,通过canvas的drawCircle方法来依次画圆。mRotateAngle是用来控制小圆旋转用的,用ValueAnimaor使它在0到Math.PI*2之间变化,从而改变cx,cy的坐标,以达到旋转的效果。
现在mCurrentRotateRadius的值是小圆围绕着旋转的大圆的半径。
看一下控制mRotateAngle角度变化的代码,可以在RotateState的构造方法里执行。

public RotateState(){
            animator = ValueAnimator.ofFloat(0,(float) Math.PI * 2f);
            animator.setDuration(mRoatationDuration);
            animator.setInterpolator(new LinearInterpolator());
            animator.setRepeatCount(ValueAnimator.INFINITE);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    mRotateAngle = (float) valueAnimator.getAnimatedValue();
                    invalidate();
                }
            });
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    super.onAnimationStart(animation);
                    setVisibility(View.VISIBLE);
                }
            });
            animator.start();
        }

这里加了一个线线性插值器,如果不加的话,动画旋转一圈后会停一下,然后再旋转,效果不流畅。而且还设置了RepeatCount值是INFINITE,无限循环。因为停止旋转动画并开启后面的收缩和展开动画需要通过外部设置。提供一个给外部调用的接口,停止旋转动画,开启收缩动画。

public void splashDismiss(){
        animator.cancel();
        if(mState instanceof RotateState){
            mState = new MergeState();
        }
    }

我们在MainActivity中用Handler模拟加载数据,在3秒后调用splashDismiss开启后面的动画。

public class MainActivity extends AppCompatActivity {

    private SplashView splash;
    private Handler handler = new Handler();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        splash = (SplashView) findViewById(R.id.splash);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                splash.splashDismiss();
            }
        },3000);
    }
}

MainActivity的布局的代码很简单,

<?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:gravity="center"
    tools:context="com.example.splashview.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/content">

    </LinearLayout>

    <com.example.splashview.SplashView
        android:id="@+id/splash"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

这时就要开始执行收缩动画MergeState了,MergeState的drawState方法。

@Override
        public void drawState(Canvas canvas) {
            drawBackground(canvas);
            drawCircles(canvas);
        }

这里跟RotateState的drawState是一样的也调用了drawCircles方法,因为收缩只需要用ValueAnimator改变大圆的半径mCurrentRotateRadius就好了,在MergeState的构造方法里

public MergeState(){
            animator = ValueAnimator.ofFloat(0,mRotationRadius);
            animator.setDuration(mMergeDuration);
            animator.setInterpolator(new OvershootInterpolator(10));
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    mCurrentRotateRadius = (float) valueAnimator.getAnimatedValue();
                    invalidate();
                }
            });
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new ExpandState();
                }
            });
            animator.reverse();

        }

这里的收缩有一个overshoot的效果,先向外弹射一段距离,再收缩回来,用了OvershootInterpolator插值器。在动画结束后要开启展开动画ExpandState。drawState方法

@Override
        public void drawState(Canvas canvas) {
            drawBackground(canvas);
        }

展开动画对背景进行了操作,当然我们肯定要对drawBackground方法进行修改了。

private void drawBackground(Canvas canvas) {
        if(mHoleRadius>0){
            float mBgPaintStroke = mDiagnoalDist - mHoleRadius;
            float radius = mHoleRadius + mBgPaintStroke / 2;
            mBackgroundPaint.setStrokeWidth(mBgPaintStroke);
            canvas.drawCircle(mCenterX,mCenterY,radius,mBackgroundPaint);
        }else {
            canvas.drawColor(mBackgroundColor);
        }

    }

这里很巧妙的利用改变Paint的StrokeWidth的方法实现这种效果,画背景我们也可以当成是画了一个很大的圆,把屏幕都覆盖了,我们把画笔的宽度设置成了屏幕对角线距离的一半,然后来让画笔的宽度满满减小,对应的也就是中间空心圆的半径mHoleRadius慢慢变大,计算出画圆的半径radius,
我们用ValueAnimator来控制mHoleRadius使其慢慢变大,同样也是在ExpandState的构造方法里。

public ExpandState(){
            animator = ValueAnimator.ofFloat(0,mDiagnoalDist);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mHoleRadius = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    setVisibility(View.GONE);
                }
            });
            animator.setDuration(800);
            animator.start();
        }

除了上面那种通过改变Paint的StrokeWidth来实现效果,还有一种就是通过设置Paint的Xfermode来达到展开效果,用CLEAR模式就可以轻松实现,这时的drawBackground方法修改如下

private void drawBackground(Canvas canvas) {
        canvas.drawColor(mBackgroundColor);
        if(mHoleRadius>0){

            mHolePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
            canvas.drawCircle(mCenterX,mCenterY,mHoleRadius,mHolePaint);
            mHolePaint.setXfermode(null);
        }

    }

用xfermode实现看来稍简单点,不用计算,不了解xfermode的可以去了解下,CLEAR模式是最好理解的一种。

最后再贴一下定义的变量和一些初始化的代码

private ValueAnimator animator;
    //小圆的半径
    private float mCircleRadius = 20;
    //大圆的半径  (小圆围绕大圆旋转形成的)
    private float mRotationRadius = 100;
    //小圆旋转收缩时的半径
    private float mCurrentRotateRadius = mRotationRadius;

    //小圆的颜色
    private int[] mCircleColors;
    //小圆围绕旋转的时间
    private long mRoatationDuration = 1200;
    //收缩的时间
    private long mMergeDuration = 400;
    //旋转角度
    private float mRotateAngle = 0f;
    //画小圆用的画笔
    private Paint mCirclePaint;
    //画背景的画笔
    private Paint mBackgroundPaint;
    //背景的颜色
    private int mBackgroundColor = Color.WHITE;
    //中心点的坐标
    private float mCenterX;
    private float mCenterY;
    //对角线的距离的一半
    private float mDiagnoalDist;
    //展开动画时中间空心圆的半径
    private float mHoleRadius = 0f;
    //用xfermode CLEAR模式时 画空心圆的画笔
    private Paint mHolePaint;

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

    public SplashView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    private void initView(Context context) {
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCircleColors = context.getResources().getIntArray(R.array.splash_circle_colors);
        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setColor(Color.WHITE);
        mBackgroundPaint.setStyle(Paint.Style.STROKE);
        mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHolePaint.setStyle(Paint.Style.FILL);
        mHolePaint.setColor(mBackgroundColor);
        setLayerType(LAYER_TYPE_SOFTWARE,null);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCenterX = w/2f;
        mCenterY = h/2f;
        mDiagnoalDist = (float) (Math.sqrt(w*w+h*h) / 2);
    }

好了。效果实现了。总体来说不难。