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

来源:互联网 发布:导航升级软件下载 编辑:程序博客网 时间:2024/05/29 08:49

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

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

一、效果

这里写图片描述

这个是国外的一个 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

阅读全文
0 0
原创粉丝点击