Android好奇宝宝_06_聊一聊Android里的动画

这一篇我们来聊一聊高大上的动画效果。

首先说一个常识,一个对理解动画最重要的概念,亦是动画的本质:

动画的原理是利人眼的视觉暂留的特性,即如果一帧帧图像切换的足够快的话,人眼就察觉不到停顿,看起来就像连续的动画了。

动画的原理很简单,就是让图像进行快速的切换。动画的难点是计算出每两帧之间的差异,比如一个位移动画,对于每一帧你都必须计算出它的位置,如果是直线匀速的。很容易计算,但如果是曲线的而且还是有加速度(即移动的速度是会变化的)的,那么计算就会变的复杂了。

总结一下,动画有两个要素,一个是若干的帧图像,一个是变化。

回到Android的动画体系,有一道很普遍的面试题:Android中动画的种类?

在我以前面试时,答案还只有两种,不过现在3.0版本以后现在变成3种了。

下面我们一种一种讲。

(1)Frame Animation(帧动画)

这个是最简单的,即我们提供第一帧到最后一帧的所有帧,然后系统帮我们快速的显示出来而已,没啥好说的。

这种动画在实际开发中也比较少用,因为需要大量的图片资源,浪费存储空间。

(2)传统View动画

我先说下这种动画的原理,然后我们再到源码中去验证。

前面说过,动画有两个要素:若干的帧和变化。

而传统View动画的原理就是:我们只提供一帧和变化,然后系统基于我们提供的这一帧和变化,去生成动画需要的所有帧,然后不停的刷新界面轮播帧直到动画结束。

来看一下一个最简单的动画实现:

		Button btn=new Button(MainActivity.this);
		ScaleAnimation anim=new ScaleAnimation(0, 1, 0, 1);
		btn.startAnimation(anim);

这里的Button的初始状态就是我们提供的一帧,构造ScaleAnimation的参数列表就是我们提供的变化。

startAnimation是通知系统开始执行动画的方法:

    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }

startAnimation会将要执行的动画保存(setAnimation),然后请求重绘。所以我们可以确定在重绘过程中,一定会对这个保存动画的变量进行是否为空和动画类型的判断。

这里呢不打算详细讲请求重绘的过程,我们只需要知道重绘的请求会一直向上向父View传递,然后到最顶层父View后再反向向下传递,我们这里只关注传递到Button的父View时发生的事。

我们知道所有容器View都是ViewGroup或其子类,在重绘时子View是由父View来绘制出来的,这里我们从下面的ViewGroup的dispatchDraw方法开始追踪:

    protected void dispatchDraw(Canvas canvas) {

        //...省略非关键代码...

        //看到回调方法onAnimationStart在这里被调用,说明动画是从这里之后就开始的
            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }

        //...省略非关键代码...

        boolean more = false;
        final long drawingTime = getDrawingTime();
        //这个是判断child是否有特定的绘制顺序,跟我们的动画实现无关,我们只关注一种情形
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                //如果这个child可见或者有动画需要执行的话
                //由于我们之前在 startAnimation方法中调用了setAnimation(animation)方法
                //所以getAnimation()结果不为空
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    //more用来标示动画的状态,more==true时表示动画还没结束,为false则表示动画已经结束了
                	more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
        //...省略非关键代码...
    }

跳到drawChild(canvas, child, drawingTime)方法:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

跳到child的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,这个方法很长,我只说明一些关键语句:

boolean more = false;

同样用来标示动画是否结束

Transformation transformToApply = null;

之前说过动画的所有帧都有系统生成,而Transformation是用来描述每一帧的状态信息,Transformation中有3个成员变量:

	//一个矩阵,通过改变它可以改变画布
	//会影响画布的大小、位置和旋转角度
	//而画布的改变就会影响到绘制在其上面的View
	//传统View动画就是通过改变画布(Canvas)的方式去产生所有的帧
    protected Matrix mMatrix;
    //影响透明度
    protected float mAlpha;
    //动画需要改变的类型
    //大小,位移、旋转需要改变mMatrix
    //透明度转变动画需要改变mAlpha
    protected int mTransformationType;

继续:

    final Animation a = getAnimation();
    if (a != null) {
        more = drawAnimation(parent, drawingTime, a, scalingRequired);
        //...省略非关键代码...

        //这一句后面会说明
        transformToApply = parent.getChildTransformation();
    }

先看下drawAnimation(ViewGroup parent, long drawingTime,Animation a, boolean scalingRequired)方法:

    private boolean drawAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        //这里判断动画是否进行过初始化,若否则进行初始化
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }
        //这里注意帧信息是保存在parent的
        final Transformation t = parent.getChildTransformation();
        //这一句是关键,是计算变化的地方
        //getTransformation方法的作用就是要根据动画已经进行的时间和一些其它信息
        //来计算出当前时刻要显示的这一帧的状态信息,并保存到t中
        //接下来后面就会从t取出信息来改变画布
        boolean more = a.getTransformation(drawingTime, t, 1f);
        //...省略非关键代码...

        //如果动画还没到结束时间
        if (more) {
            //会在这里调用 parent.invalidate()方法请求重绘,然后呢又会去计算下一帧的信息,改变画布,绘制帧,一直循环直到动画结束
        }
        return more;
    }

回到draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,前面没说明的:

transformToApply = parent.getChildTransformation();

现在我们知道了drawAnimation方法计算出来的绘制当前帧的信息是保存在parent里的,这里就是把它取出来。

接下来还是在draw(Canvas canvas, ViewGroup
parent, long drawingTime)方法里,如果是动画类型是需要改变Matrix的话,会调用:

canvas.concat(transformToApply.getMatrix());

要改变透明度的话会调用:

canvas.saveLayerAlpha();

最后调用:

draw(canvas);

把改变后的画布传给draw(canvas)方法,开始绘制帧。

小结:传统View动画通过改变Canvas来生成动画所需要的帧,每一帧Canvas的变化信息由Transformation类来保存,而该如何变化由Animation实现类来决定,具体是每个Animation的子类都重写了方法:

protected void applyTransformation(float interpolatedTime, Transformation t)

参数interpolatedTime是动画已经进行的时间(注:这是在没有设置Interpolator的情况下,关于Interpolator我会在后面再说明),t用来保存计算出来的结果。

补充:传统View动画有一个经常会出现的问题就是,一个按钮在进行位移动画之后,如果设置了setFillAfter(true),那么会停留在最后一帧,但是点击的触发位置还是在原位置。如果你仔细阅读了上面得源码分析,你就明白原因了:传统View动画只是改变画布,对于进行动画的物体(比如我们例子中的Button)并不会进行改变,而触摸、点击等事件的位置判断并不受画布的影响。

一个不完美的解决方法:不要设置setFillAfter(true),设置动画监听,在动画结束时调用layout()方法进行重新布局。

例子:

对一个按钮(btn)进行了一个向右移100、向下移200的动画:

		anim = new TranslateAnimation(0, 100, 0, 200);
		btn.startAnimation(anim);

设置监听:

		anim.setAnimationListener(new AnimationListener() {
			public void onAnimationStart(Animation animation) {
			}
			public void onAnimationRepeat(Animation animation) {
			}
			@Override
			public void onAnimationEnd(Animation animation) {
				// TODO Auto-generated method stub
				btn.layout(btn.getLeft() + 100, btn.getTop() + 200,
						btn.getRight() + 100, btn.getBottom() + 200);
			}
		});

这种方法的缺陷:

(1)layout方法会导致Button闪一下。

(2)这里只是一个简单的位移动画,如果动画复杂的话,想计算出layout方法的4个参数也会变得很复杂。

(3)Property Animation(属性动画)

属性动画跟传统View动画是类似的,其实所有动画都是类似的,不同的是我们是通过改变什么来达到动画的视觉效果的。

传统View动画通过改变View所在的画布,让View随着画布的变化而变化,但直接改变要进行动画的物体本身可能更简单。

属性动画就是通过改变物体的属性来达到动画效果的,这里说物体而不是View是因为Android的属性动画并没有规定进行动画的必须是View(当然大多数、几乎全部、差不多都是View),它只是依据我们提供的改变去改变一个对象的属性值,至于改变了这个属性值之后会发生什么事,它是不管的。

说下属性动画最重要的两个类:

(1)ValueAnimator

这个类就像它的名字一样,值的动画师,它只关注值的变化,依据我们给出的变化来提供某个时刻的值应该为多少。

例子:

我们想让一个值在1s的时间内从0匀速地变为1,那么ValueAnimator会在0ms时返回0,500ms时返回0.5,以此类推。

(2)ObjectAnimator

ObjectAnimator是ValueAnimator的子类,它与ValueAnimator的区别是它不仅仅提供值,它还会在得出值后去改变属性值。

ObjectAnimator的实现原理:

		ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(btn, "x", new float[]{0,100});
		objectAnimator.setDuration(1000);
		objectAnimator.start();

第一个参数就是我们想改变其属性的对象。

第二个参数是属性的名称,要注意的是这并不意味着这个对象就必须要拥有这个属性变量。

因为ObjectAnimator是通过反射属性的getter和setter方法去改变和获取属性值,所以你只要有对应的getter和setter方法(要符合驼峰命名规则)。

第三个参数就是我们提供的变化,一个数组参数,数组的大小必须为2或者1。

2的情况:数组第一个元素做为初始值,第二个作为结束值,这种情况下对应的getter方法不是必须的。

1的情况:ObjectAnimator通过反射调用getter方法将获得的结果作为初始值,将数组中唯一元素作为结束值,这种情况下getter方法是必须的。

我来用文字来描述一下上面几条语句表达的意思:

请在1s内匀速的将btn的"x"属性值从0增长到100,谢谢!

注:像我上面说的,改变属性值不是真的类似btn.x=value这种方式,比如上面这个例子,ObjectAnimator每隔一段时间,当ValueAnimator计算出新的值时,它就会通过反射去执行语句btn.setX(value);,直到动画时间结束。

当然,对于View来说,在调用setX方法时肯定会去请求重绘,而在重绘过程中,不管setX做了什么,最终肯定会影响到View绘制出来的水平位置。当这些都不关ObjectAnimator事,ObjectAnimator只是改变属性值,不关心改变后会发生什么。

关于属性动画的源码我就不分析了,有兴趣的推荐一篇博客,讲得很好:传送门

附:Interpolator

前面的例子都是匀速地变化,而Interpolator就是可以改变改变速度的东东(是两个改变,我没打错),可以实现类似物理中的加速度,但其实可以实现更多。

不管是传统View动画还是属性动画,都得先计算出下一帧的信息再去请求刷新,而Interpolator就是提供一个对计算出来的值一次修改的机会。还是上面的例子:

在没有设置Interpolator的情况下,"x"的值在500ms时应该为50;

如果设置了一个越来越快的Interpolator,那么"x"的值在500ms时应该小于50,btn开始会移动地比较慢,然后越来越快。Interpolator就是在得出"x"为50的基础上再进行一次修改,这次修改可以是任意的,但一般不会偏差太大。可以是40、60、100,这是比较正常的,也可以是200、99999999,这些也是可以的,不过我们不会这么做。

回到前面的:

protected void applyTransformation(float interpolatedTime, Transformation t)

参数名称是interpolatedTime,表明Interpolator是通过改变动画已经进行的时间来改变最终值的。

我们可以自定义Interpolator,只要实现Interpolator接口重写float
getInterpolation(float input);方法,这里的input并不是50,而是动画已进行时间的百分比。系统已经实现了几个常用的Interpolator,一般情况下是够用了。如AccelerateInterpolator就是上面说的开始慢,然后越来越快,与之对应的DecelerateInterpolator则相反,开始快然后越来越慢。

时间: 2024-10-14 00:44:30

Android好奇宝宝_06_聊一聊Android里的动画的相关文章

Android好奇宝宝_04_一个有3个功能的Adapter

感觉Android好奇宝宝这个系列是脱离不了ListView和GridView了... 这一篇呢来分享点好东西 一个自定义Adapter,可以快速实现三个功能: (1)自动缓存处理 好吧,这个功能不是我实现的.我只是照搬鸿洋大大的,我会简单说下,不过还是请先看下他的原文,再来看我添加的两个功能,传送门 (2)支持item的不同布局 提供一个接口来通过position和该position的数据来设置不同的布局 (3)局部刷新 只刷新指定item的某个子View,避免一直调用notifyDataSe

Android好奇宝宝_番外篇_看脸的世界_08

废话少说,先上效果图: (左侧的图片是我用window画图软件1分钟画的,所以就不要嫌丑了,You can you up no bb.) 这是我发过最挫的效果图了,不过这是由于没有图片素材导致的,就不要在意这些细节了,知道实现原理后完全可以发挥你的想象去实现更美观的效果. 这个效果也是有开源库的,不过我又把名字给忘了,不过我记得原理,于是就试着自己写了一下. 其实原理很简单,我在另一篇博客(一个有吃豆人删除动画的ListView)也说过了,这一篇当做兑换那些年少轻狂不更事时许下的诺言(是不是瞬间

Android好奇宝宝_番外篇_看脸的世界_05

上一篇番外篇讲了一个炒鸡炒鸡简单的自定义ProgressBar,这一篇基于上一篇的基础扩展为SeekBar,没看过上一篇的,请先看一遍:传送门 先上效果图(2G内存的机子运行模拟器,所以有点卡): 这个效果之前不知道在哪里看到过,我也忘了. 下面进入正题: 测量大小和绘制部分沿用上一篇ProgressBar的,不清楚的请走上面的传送门. 对比上一篇的扩展: (1)SeekBar能通过触摸改变刻度 (2)SeekBar上方添加一个显示当前刻度的浮动View(后面用FloatView表示) (1)通

Android好奇宝宝_10_RecyclerView+CardView+Palette

这几天被ListView搞得有点烦,听说官方出了个新控件来替换它,于是顺带另外两个5.0版本的新东西写了个Demo. 先上效果图: (1)RecyclerView RecyclerView就是官方用来替代ListView的,其实同时也可以替代GridView,上面第二幅图的编码实现只跟第一幅差了一行代码而已. RecyclerView一般需要两个东西搭配使用,LayoutManager和Adapter,比ListView和GridView(后面简称LG组合)多了一个LayoutManager.其

Android好奇宝宝_11_SwipeRefreshLayout原理浅析

上一篇文章写了一个RecyclerView的Demo,然后就想加个下拉刷新功能进去试试,由于RecyclerView算比较新的东西,所以暂时找不到什么开源库使用.于是想到了官方提供的SwipeRefreshLayout,号称能为任何View添加下拉刷新功能. SwipeRefreshLayout的使用很简单: (1)将要下拉刷新的View嵌套到SwipeRefreshLayout中: <jjj.demo.newstuffdemo.JJJSwipeRefreshLayout android:id=

Android好奇宝宝_番外篇_看脸的世界_03

无聊刷帖看到一个求助,试着写了一下. 一个自定义Switch控件,附带动画效果. 说是控件,其实是一个布局容器,先上效果图: 先讲原理,再看高清源码. 原理: 好像没啥原理,汗... 跟其它自定义容器控件一样,一般要注意: (1)计算好大小,宽度和高度 (2)计算好子View的布局位置 不是一般要注意的: (3)动画是用的nineoldandroids (4)遮挡效果是通过控制子View的绘制顺序 高清源码: (1)计算大小: protected void onMeasure(int width

Android好奇宝宝_05_PopupWindow与悬浮窗

这一篇讲讲PopupWindow与悬浮窗之间那些不得不说的故事. 之所以把PopupWindow与悬浮窗这两个放到一起讲,是因为这两个的实现原理基本是一致的,只是有点不同而已. 原理: 使用系统服务(WindowManagerService)将要显示的View添加进Window中. WindowManagerService和ActivityManagerService是Android系统中两个最重要的服务,其中一个管理窗口显示,一个管理四大组件. ActivityManagerService这里

Android好奇宝宝_番外篇_看脸的世界_02

一个有吃豆人删除动画的ListView 这是一个无聊的效果,由一个无聊的程序猿,在无聊的情况下写的. 虽然这效果不中看中用,不过就当学习了. 先上图 效果一目了然,主要是: (1)移除item时执行吃豆人动画 (2)滚动时吃豆人也相应移动 (3)应对可见与不可见状态间的切换 简单原理分析: (1)吃豆人.豆.和左边的白色矩形(当然所有颜色都是可以改的,你想换成图片也行)都是用canvas画出来的. (2)问:canvas那里来的?答:ListView的canvas.具体是重写ListView的这

Android好奇宝宝_09_Handler Looper Message

发现自己讲的东西都是UI相关的,这一篇就来讲讲Android很重要的知识点:Handler Looper Message. 废话少说,直接入正题. (1)存在的意义: 我一直把Handler Looper Message 这几个类当成几个可以搭配使用的工具类,特别之处在于系统提供了这些工具类,并且系统自己也使用了这些类. 既然是工具类,那么其功能是什么呢? 答:在当前线程建立一个唯一的消息队列,通过Handler可以向消息队列添加消息,Looper不断从消息队列取出消息,再转发给发送该消息的Ha