05
2017
10

(二十三)Animator 实例 —— 开场动画

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、效果

这里写图片描述

这个是国外的一个 App 开场动画,曾获得设计大奖 。

二、分析

看效果是有两个界面,一个是 Splash 小球旋转的加载界面,一个是主界面,很多时候习惯在这边做成两个 Activity 进行跳转。如果条件允许的话,尽量做成两个 View 同时存在一个 Activity 里面,这边采用这种模式。

1.预加载时候显示 SplashView —- 小圆旋转的动画时间是不确定。
2.SplashView 盖在了主界面上面。
3.动画:动画分为三步。小球的旋转动画;小球逃逸和聚合动画(平移);水波纹的扩散动画。

三、搭建

加载的界面,弄成一个自定义控件,这样就可以与主界面放在一个 Activity 里面了。SplashView 一开始就执行小球旋转的动画,当数据加载完的时候,调用 splashDisappear,则开始调用小球的聚合与水波纹扩散的动画。
SplashView:

public class SplashView extends View {

    // 整体的背景颜色
    private int mSplashBgColor = Color.WHITE;

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

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

    public SplashView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SplashView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(mSplashBgColor);
    }

    /** * 数据加载完成之后调用 * 开始后面的动画 */
    public void splashDisappear() {

    }
}

使用 ContentView 模拟主界面,虽然这边是继承 AppCompatImageView,只是为了方便,不是说这一定要是个 Image,这是一个 View。
ContentView:

public class ContentView extends android.support.v7.widget.AppCompatImageView {


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

    public ContentView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ContentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setImageResource(R.drawable.content);
    }
}

布局文件,使用 FrameLayout,让 SplashView 盖在 ContentView 上面。
activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout  xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.xiaoyue.animatorsplashview.MainActivity">

    <com.xiaoyue.animatorsplashview.ContentView  android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY"/>

    <com.xiaoyue.animatorsplashview.SplashView  android:id="@+id/splash_view" android:layout_width="match_parent" android:layout_height="match_parent"/>

</FrameLayout>

MainActivity:

public class MainActivity extends AppCompatActivity {

    private SplashView splashView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //AppCompatActivity 的设置全屏
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        splashView = (SplashView) findViewById(R.id.splash_view);

        startLoad();
    }

    Handler handler = new Handler();

    private void startLoad() {
        //handler 模拟加载数据
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //数据加载完毕,进入主界面,调用动画
                splashView.splashDisappear();
            }
        }, 5000);
    }
}

这时候运行项目是一个全白的界面,加载界面 SplashView 把主界面 ContentView 盖住了。

四、策略模式

这个开场效果可以分为 3 个动画效果,小球的旋转动画、小球逃逸和聚合动画(平移)和水波纹的扩散动画。每次动画执行的时候都要进行绘制,为了代码优雅,这边采用策略模式进行搭建。

在 SplashView.java 添加几个内部类。

1.动画抽象类

    //动画
    private ValueAnimator mAnimator;

    private SplashState mState = null;
    //策略模式:State---三种动画状态
    private abstract  class SplashState{
        //绘制各个状态的界面
        public abstract  void drawState(Canvas canvas);

        //取消动画
        public void cancel(){
            mAnimator.cancel();
        }
    }

2.旋转动画

    private class RotateState extends SplashState{
        public RotateState() {

        }

        @Override

        }
    }

3.聚合动画

    private class MergingState extends SplashState{

        public MergingState() {

        }

        @Override
        public void drawState(Canvas canvas) {

        }
    }

4.水波纹扩散动画

    private class ExpandState extends SplashState{

        public ExpandState() {

        }

        @Override
        public void drawState(Canvas canvas) {

        }
    }

这边动画效果差异不是很大,逻辑也没有很复杂,使用策略模式可能显得有点麻烦。当动画差异较大的时候,策略模式写出来的代码就比较好看一些。

五、旋转动画

1.小球颜色

在初始化的时候获取小球的颜色,这样的话可以自己定义小球个数和颜色,比较灵活。

    //小球颜色数组
    private int[] mCircleColors;

    // 绘制小球的画笔
    private Paint mPaint = new Paint();

    private void init(Context context) {
        mCircleColors = context.getResources().getIntArray(R.array.splash_circle_colors);        
        //画笔初始化
        //消除锯齿
        mPaint.setAntiAlias(true);
    }

color.xml

<resources>
    <color name="splash_bg">#F8F6EC</color>
    <color name="orange">#FF9600</color>
    <color name="aqua">#02D1AC</color>
    <color name="yellow">#FFD200</color>
    <color name="blue">#00C6FF</color>
    <color name="green">#00E099</color>
    <color name="pink">#FF3892</color>

    <array name="splash_circle_colors">
        <item>@color/blue</item>
        <item>@color/green</item>
        <item>@color/pink</item>
        <item>@color/orange</item>
        <item>@color/aqua</item>
        <item>@color/yellow</item>
    </array>
</resources>

2.onDraw

重写 onDraw 方法,当第一次进来的时候,没有任何动画,直接初始化一个旋转动画,然后调用各个动画的绘制方法 drawState。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mState == null) {
            mState = new RotateState();
        }
        mState.drawState(canvas);
    }

3.旋转动画

实现旋转动画,在构造函数中新建新建属性动画,改变旋转的角度,添加属性动画监听,每次执行动画时候进行重新绘制。

    // 大圆和小球旋转一圈的时间
    private final long mRotationDuration = 1200; //ms

    //当前圆旋转角度(弧度)
    private float mCurrentRotationAngle = 0F;

    /** * 1.旋转动画 * 控制各个小球的坐标---控制小球的角度变化----属性动画 ValueAnimator */
 private class RotateState extends SplashState {
        public RotateState() {
            //计算某个时刻当前的角度是多少? 0~2π
            mAnimator = ValueAnimator.ofFloat(0f, 2 * (float)Math.PI);
            mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mCurrentRotationAngle = (float) animation.getAnimatedValue();
                    //刷新,重新绘制
                    invalidate();
                }
            });
            //动画执行时间设置为 1200ms,
            mAnimator.setDuration(mRotationDuration);
            //设置无限循环
            mAnimator.setRepeatCount(ValueAnimator.INFINITE);
            //启动动画
            mAnimator.start();
        }

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

4.绘制背景

直接绘制白色原先是在 onDraw 方法中,移到这边。

    /** * 绘制背景(白色区域) * @param canvas */
    private void drawBackground(Canvas canvas) {
        canvas.drawColor(mSplashBgColor);
    }

5.屏幕中心

很明显,大圆是在屏幕中心(更准确应该是说是在该 View 中心),所以需要计算屏幕中心的坐标。

    // 屏幕正中心点坐标
    private float mCenterX;
    private float mCenterY;

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

6.绘制小球

根据小球个数先算出相邻小球间的间隔角度,从而可以计算每个小球距离 0 角度的间隔角度,再加上旋转的角度,则为小球当前的角度。

    // 大圆(里面包含很多小球的)的半径
    private final float mRotationRadius = 90;
    // 每一个小球的半径
    private final float mCircleRadius = 18;

    /** * 绘制小球 * @param canvas */
    private void drawCircles(Canvas canvas) {
        //每个小球之间的间隔角度 = 2π/小圆的个数
        float rotationAngle = (float) (2 * Math.PI / mCircleColors.length);

        for (int i=0; i < mCircleColors.length; i++){
            /** * x = r * cos(a) +centerX * y= r * sin(a) + centerY * 每个小球 i * 间隔角度 + 旋转的角度 = 当前小球的真正角度 */
            double angle = i*rotationAngle + mCurrentRotationAngle;
            float cx = (float) (mRotationRadius * Math.cos(angle) + mCenterX);
            float cy = (float) (mRotationRadius * Math.sin(angle) + mCenterY);
            mPaint.setColor(mCircleColors[i]);
            canvas.drawCircle(cx, cy ,mCircleRadius, mPaint);
        }
    }

7.效果

这时候运行代码:
这里写图片描述

小球开始不停的绕着大圈旋转,但是认真看会发现,旋转动画旋转一回后,会卡顿一下。这是由于我们旋转动画设置为重复执行,动画执行过程中,默认是线性的,但是,动画的衔接不会是线性的。这时候需要为动画添加线性插值器,这样可以使衔接过程也是线性。

        mAnimator.setInterpolator(new LinearInterpolator());

六、聚合动画

1.切换动画

在开头搭建的时候已经预留了切换的方法,在数据加载完毕之后(用 Handler 延迟 5s 模拟)调用切换动画。
直接把要执行的动画 mState 指向聚合动画即可。

    /** * 数据加载完成之后执行的动画 * 小球聚合和水波纹扩散 */
    public void splashDisappear() {
        if (mState != null && mState instanceof RotateState) {
            mState.cancel();
            post(new Runnable() {
                @Override
                public void run() {
                    mState = new MergingState();
                }
            });
        }
    }

2.聚合动画

在旋转动画的时候,计算小球的坐标的时候,是根据大圆的半径进行计算,这边直接改变大圆的半径,从而达到聚合的效果。为了保持初始值,从新定义一个当前大圆的半径,用这个来进行计算,初始值为大圆默认半径。(在绘制小球 drawCircles 方法里也要同步改过来)

小球聚合前有一个向外扩散的效果,这里使用一个回荡秋千插值器 AnticipateInterpolator

这里写图片描述

    //当前大圆的半径
    private float mCurrentRotationRadius = mRotationRadius;

    /** * 2.聚合动画 * 要素:大圆的半径不断地变大--变小----》小球的坐标 */
    private class MergingState extends SplashState {

        public MergingState() {
            mAnimator = ValueAnimator.ofFloat(mRotationRadius, 0f);
            mAnimator.setDuration(mRotationDuration);
            //插值器实现先扩散再聚合效果
            mAnimator.setInterpolator(new AnticipateInterpolator(6f));
            mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    mCurrentRotationRadius = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
            mAnimator.start();
        }

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

3.效果

这里写图片描述

七、扩散动画

1.切换动画

扩散动画是在聚合动画结束之后才开始执行,所以为聚合动画添加一个监听,当执行完毕之后,切换动画为扩散动画。

            mAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    mState = new ExpandState();
                }
            });

2.分析

扩散动画是由中间一个小圆慢慢扩大,可以实现的方法很多,这边介绍个比较好的想法。

这里写图片描述

图中米黄色表示手机屏幕,中间彩色小圆为主界面,这是后加载界面是最外面一个大圆,被挖去中间一个小圆(圆环)。在这边把两个黑色圆圈组成的圆环理解为边沿很粗很粗的圆,则实际圆是圆环中间的那个圆(即黄色的圆)。 通过不断的缩小圆环的边沿,保持圆环的最大一个圆不变,从而使中间的主界面不断扩大。

3.屏幕对角线

需要计算出屏幕对角线一半的长度,即圆环的大圆半径。在 onSizeChanged 时候通过勾股定理计算出来。

    //屏幕对角线一半
    private float mDiagonalDist;

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

4.扩散动画

扩散动画变化的是中心要显示的主界面的圆半径大小,为了不显得变换的很突兀,扩散动画的初始显示大小设置为小球的大小。

    //空心圆初始半径
    private float mHoleRadius = 0F;

    /** * 3.水波纹扩散动画 * 画一个空心圆----画一个圆,让它的画笔的粗细变成很大---不断地减小画笔的粗细。 * 空心圆变化的范围:小球半径 ~ 对角线/2 */
    private class ExpandState extends SplashState {

        public ExpandState() {
            //花1200ms,计算某个时刻当前的空心圆的半径是多少
            mAnimator = ValueAnimator.ofFloat(mCircleRadius, mDiagonalDist);
            mAnimator.setDuration(mRotationDuration);
            mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    //当前的空心圆的半径是多少?
                    mHoleRadius = (float) valueAnimator.getAnimatedValue();
                    invalidate();
                }
            });
            mAnimator.start();
        }

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

5.绘制背景

在扩散效果中,背景不能直接画为全白了,要画一个边沿很粗的圆。为绘制背景添加一个新的画笔,在 init 中进行初始化。

    // 绘制背景的画笔
    private Paint mPaintBackground = new Paint();

    private void init(Context context) {
        mCircleColors = context.getResources().getIntArray(R.array.splash_circle_colors);
        //每个小球之间的间隔角度 = 2π/小圆的个数
        rotationAngle = (float) (2 * Math.PI / mCircleColors.length);
        //画笔初始化
        //消除锯齿
        mPaint.setAntiAlias(true);
        mPaintBackground.setAntiAlias(true);
        //设置样式---边框样式--描边
        mPaintBackground.setStyle(Paint.Style.STROKE);
        mPaintBackground.setColor(mSplashBgColor);
    }   

    /** * 绘制背景(白色区域) * @param canvas */
    private void drawBackground(Canvas canvas) {
        if(mHoleRadius>0f){
            //得到画笔的宽度 = 对角线/2 - 空心圆的半径
            float strokeWidth = mDiagonalDist - mHoleRadius;
            mPaintBackground.setStrokeWidth(strokeWidth);
            //画圆的半径 = 空心圆的半径 + 画笔的宽度/2
            float radius = mHoleRadius + strokeWidth/2;
            canvas.drawCircle(mCenterX,mCenterY,radius,mPaintBackground);
        }else {
            canvas.drawColor(mSplashBgColor);
        }
    }

6.效果

这里写图片描述

到这里就结束了。
可以把动画执行时间,小球半径等属性提取出来作为自定义属性。

八、附

代码链接:http://download.csdn.net/download/qq_18983205/10006709

上一篇:Android应用层通信机制 下一篇:React Native项目结构