Android View原理(View树遍历,View重绘,View动画)

一、屏幕绘图基础

Android中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户端需要绘制时,首先请求服务端创建一个窗口,然后在窗口中进行具体的视图内容绘制;对于每个客户端而言,他们都感觉自己独占了屏幕,而对于服务端而言,它会给每一个客户端窗口分配不同的层值,并根据用户的交互情况动态改变窗口的层值,这就给用户造成了所谓的前台窗口和后台窗口的概念;

Android的屏幕绘制架构如下图:

  1. SurfaceFlinger进程:简称sf,该进程在系统开机时自动启动,在init.rc中定义,他的作用就是给每个客户端分配窗口,在程序中用Surface类来表示这个窗口,即每个窗口都是一个平面,而每个平面在程序中都会对应一块屏幕缓冲区;
    sf服务进程的启动命令源码位于./frameworks/native/cmds/surfaceflinger/;
    sf服务的具体源码位于./frameworks/native/services/surfaceflinger/;
  2. SystemServer进程:前面已经重点讲过该进程的相关信息了,他是zygote进程孵化出的第一个进程,里面加载的是所有android系统运行所需的各种系统服务,当然最重要的就是Ams和Wms,而Wms正是屏幕绘图服务端最重要的窗口管理服务,客户端应用程序要请求窗口展示直接与Wms打交道,然后Wms进行相关的窗口管理,并通过sf的客户端驱动接口和sf打交道,从而完成窗口内容的绘制;
    Wms的具体实现源码位于com.android.server.wm.WindowManagerService;
  3. Surface/Canvas类:主要用来记录窗口的宽、高、位置、层值等相关信息,Wms就是用他来和sf客户端驱动打交道(新版本中实际上是通过SurfaceSession来作为纽带了),具体的是当Surface初始化时会通过sf客户端接口创建真正窗口需要的屏幕缓冲区;完成之后应用程序可以通过lockCanvas()来获取一个Canvas对象,再之后便可以调用各式各样的api完成具体内容的绘制;
    注意:Surface对象并不能由应用程序直接初始化,他只对SDK内部开放,主要在ViewRootImpl中创建;不过android系统提供了一个SurfaceView可以让应用程序来间接的使用Surface;
  4. Skia图形库:用C/C++编写的图形驱动库,用来完成各种平面绘制,Canvas类中的各种drawXXX方法实际上都是交由Skia库执行的;
    Skia的具体源码位于./external/skia/;

Canvas/Drawable/Paint的关系与区别

Canvas:画布,所有的绘图都需要经过他再调用底层Skia完成具体绘制,画布一般是通过Surface完成屏幕缓冲区初始化之后获取;

Drawable:一个抽象类,其子类代表某个特定的图案,比如SDK中包括了BitmapDrawable、ColorDrawable等,他仅仅是一个功能类,无法完成具体的绘制工作,但他提供了一个abstract方法draw(Canvas canvas),通过它直接转交给canvas实际处理绘制;你可以根据需要实现自定义的Drawable;

Paint:画笔,用来保存图案绘制时所用的颜色、样式(是否有阴影、粗细、圆角、字体、对齐方式等),一般在canvas.drawXXX中作为参数进行使用;


二、View树遍历

View状态分类

在View视图中定义了多种和界面效果相关的状态,比如拥有焦点Focused、按下Pressed等,不同的状态一般会显示不同的界面效果,而且视图状态会随着用户的操作而改变,一般通过xml文件中selector来申明各种状态下使用的背景图;所有的状态码位于StateListDrawable中,常用的状态码包括:

  1. enable:当前View是否可用,开发者可以通过setEnable()改变,他完全由开发者控制;
    当状态为不可用时,View将不会响应任何事件;
  2. focused:当前View是否正拥有焦点,一个窗口中只能有一个View拥有焦点,一般随用户操作而动态改变;
    该状态主要是针对按键的,因为所有的按键消息都将派发给focused视图;
  3. pressed:当前View是否正被按下,主要是针对触摸消息的,一般当用户按下时视图会有一个明显的变化,也是随用户操作而动态改变;
  4. selected:当前View是否已被选中,一个窗口中可以有多个视图处于选中状态;开发者可以通过setSelected()改变,他完全由开发者控制;

导致View树重新遍历的总体诱因

遍历View树意味着整个View需要重新对其包含的子视图分配大小并重绘;一般情况下导致重新遍历的原因有三个:其一,视图本身内部状态发生变化,比如显示属性由GONE到VISIBLE;其二,ViewGroup中添加或删除了视图导致需要重新为子视图分配位置;其三,视图本身的大小发生变化,比如TextView中的文本内容变多变少了;

在代码层面这三种情况最后都会直接或间接调用到View中的三个函数:requestLayout/requestFocus/invalidate;由于是View树遍历,所以最后都会执行到最顶级父视图中的ViewRootImpl.scheduleTraversals();在该方法内,系统会发起一个异步消息(老版本中直接通过Handler发,新版本4.1中引入了Choreographer,以及对VSync和三级Buffer支持,让页面显示和操作更流畅,具体可以详见《Android Project Butter分析》),然后在异步消息执行过程中调用performTraversals()完成具体的View树遍历;可以参见下图:


View中超多的属性变量如何管理?

在庞大的View类中会涉及到非常多的状态码,比如是否可用、是否处于按下状态、是否需要重新分配位置、是否需要重绘等等;View树在遍历重绘时会根据不同的变量值来进行相应的操作,为此View中引入了bit标示位来管理这些状态值,分别用mViewFlags和mPrivateFlags变量来管理(随着状态码的增加,在新版本4.2中还有mPrivateFlags2/mPrivateFlags3变量),他们都是int类型的,也就是说理论上每个变量可以用来标示32个状态值,当对各个状态值修改时采用位运算符&|来完成;

其中mViewFlags变量主要用来保存和视图状态相关的值,比如是否可单击、是否可双击、是否可用、是否拥有焦点等;

mPrivateFlags变量主要用来保存和内部逻辑相关的属性,比如是否需要重新分配位置、是否需要重绘、是否刷新View缓存等;

注意:这两个变量之间是有紧密联系的,经常会需要两个变量同时设置某些状态值,可以参见setFlags(..)方法中的具体内容;


requestLayout()

该方法的执行过程很简单,因为当View树进行重新布局时,总是重新给所有的视图都进行布局,而不像重绘是可以指定只绘制某一个小区域的;

从代码层面他只是为mPrivateFlags变量添加FORCE_LAYOUT标识而已;然后逐层请求mParent.requestLayout();详见下图:


invalidate()

该方法的作用是请求View树重绘;视图及其父视图在界面上是分层先后显示的,父视图位于子视图下面,绘制过程中,首先绘制最底层的根视图,然后绘制其包含的子视图,子视图若是ViewGroup,则继续绘制其子视图,如此迭代至没有子视图为止;

在具体的重绘过程中,一般不会对所有视图都进行重绘,而是只绘制那些“需要绘制”的视图,那如何找出“需要绘制”的区域呢?这就是invalidate方法要完成的功能!

大致的思路是:当View需要重绘时会给mPrivateFlags变量添加DRAWN标识,然后根据所有带该标识的视图边界一起确定最终要重绘的矩形区块,这里面会涉及到不同坐标体系间的换算,可以参见下图:

代码的具体执行过程是:

  1. View.invalidate()中设置必要的状态位标识之后,会执行到mParent.invalidateChild(..);
    这里的mParent有两种情况,一种是有父视图ViewGroup,另一种是已经到顶层了为ViewRootImpl;
  2. 若是ViewGroup,会执行完 invalidateChildInParent(…)之后继续调用mParent.invalidateChildInParent(…);
  3. 最终调用到ViewRootImpl.invalidateChildInParent(…),进而执行scheduleTraversals();
    注意:这里会提前判断 mWillDrawSoon局部变量值,若当前已经在执行performTraversals()遍历重绘了,那就不会调用scheduleTraversals(),也就不会发起重绘的异步消息了,但View中设置的各种状态值仍然是有效的,只是会在下次重绘时生效;

scheduleTraversals()

该方法会在多个地方被调用,比如requestLayout()/invalidate()中,而我们又会经常会看到连续调用这两个方法的情况,那这样岂不是会发起两次View树遍历重绘请求?其实是不会的,因为在scheduleTraversals()方法内设置了一个局部变量mTraversalScheduled,若先执行了requestLayout(),那此时mTraversalScheduled为false,发起一个异步消息请求重绘,并将mTraversalScheduled变量值设为true,这样接着调用invalidate()时判断mTraversalScheduled变量值已经不是false了,这样就确保了只发起一个异步重绘请求;参见下图:


performTraversals()

该方法时系统内进行View树遍历并进行页面重绘的核心方法,内部逻辑还是非常复杂的,约800行代码;老实说偶目前还未完全看懂里面的细节,中间涉及的关联变量实在太多了;但大致的主体流程还是清晰的,就是根据之前设置好的各种状态值,判断是否需要重新计算视图大小(Measure)、是否需要重新分配视图的位置(也叫布局Layout)、以及是否需要重绘视图(Draw),框架过程参见下图,其中每项的具体过程详见后面的具体描述:


三、View重绘过程

计算视图大小的过程(Measure)

视图大小,准确的来说应该是指视图的布局大小;我们在layout.xml中为每个UI控件设置的layout_width/layout_height两个属性被用来设置父视图给当前视图分配的“窗口”大小,为了开发的方便和对不同屏幕分辨率的兼容适配对这两个参数的赋值一般都使用相对值(也可以使用具体值,比如100dp),比如WRAP_CONTENT/MATCH_PARENT,在代码中用常量-2/-1表示;计算视图布局大小的过程本质上就是把视图布局时使用的“相对值”转换成具体值的过程;


Measure递归调用过程

View系统启动measure过程是从ViewRootImpl中调用host.measure(…)开始的,参见下图:

从上图中可以看出measure过程主要就是从顶层父视图向子视图的递归调用view.measure(…),注意以下几点:

  1. View.measure(...)该方法是final的,不允许重载,View子类只能通过重载onMeasure(...)来完善自己的测量逻辑;
  2. MeasureSpec测量规格在measure过程中经常作为输入参数,该值为int型,其值由两部分组成,高16位代表规格模式specMode,低16位代表具体尺寸specSize;其中specMode只有三种值:
    • MeasureSpec.EXACTLY:确定模式,即父视图希望子视图的大小是确定的,由specSize决定;
    • MeasureSpec.AT_MOST:最多模式,即父视图希望子视图的大小最多是specSize指定的值;
    • MeasureSpec.UNSPECIFIED:未指定模式,此时父视图完全尊重子视图的设计; 
  3. 最顶层视图DecorView测量时的MeasureSpec从何而来呢?
    是在ViewRootImpl中调用 getRootMeasureSpec(..)获得,LayoutParam宽高参数均为MATCH_PARENT;
    获得的
    specMode就是EXACTLY,specSize为物理屏幕大小;
  4. 视图的布局大小由父视图和子视图共同决定;
    layout.xml中对含有子视图的布局器中的layout_width/layout_height属性实际扮演了2个角色;一个是和父视图一起对布局器自身进行measure操作;另一个角色是作为其子视图的父视图参与子视图的measure操作;
  5. ViewGroup类中提供了 measureChildWithMargins(…)方法,用来抽象和简化父子视图之间的padding、margin、实际内容区域间的测量和尺寸计算,让开发者可以无需关注这些公共的边界测量区域;
    • 调用 child.getLayoutParams(),并强制转换成MarginLayoutParams;只要是ViewGroup的子类,就要求LayoutParam继承于 MarginLayoutParams,即要在在 generateDefaultLayoutParams()中返回MarginLayoutParams或者其子类,否则将无法在layout.xml中使用layout_margin参数,并且代码中不能使用measureChildWithMargins(…)方法,否则会抛强制类型转换错误;
    • 分别就宽高调用 getChildMeasureSpec(…);此时是传入父视图的测量规格,以及子视图在layout.xml中的设置值,返回的是子视图进行具体测量时的测量规格;父子视图之间的测量规格对应关系参见下图:

    • 根据子视图自己的测量规格调用child.measure(…)进行最终的测量;
    • 以上的逻辑由三方组成,包括父视图、开发者、子视图的设计者;父视图声明了自己可以提供的大小,而开发者在xml中使用layout_width/layout_height设置希望的大小,而视图设计者则进行最后的“拍板”,一般良好的视图设计者会根据子视图的layout_xx参数设置合适的布局大小,以尊重开发者的意图;

Layout递归调用过程

View系统启动layout过程是从ViewRootImpl中调用host.layout(…)开始的,参见下图:

从上图中可以看出layout过程也是从顶层父视图向子视图的递归调用view.layout(…),即父视图根据上一步measure子视图所得到的布局大小和布局参数,将子视图放在合适的位置上,布局参数核心指layout_gravity;注意以下几点:

  1. 与measure方法不同,View.layout(…)方法可被重载,ViewGroup.layout(…)为final的不可重载,ViewGroup.onLayout(…)为abstract的,子类必须重载,里面可以实现自己的位置分配逻辑;
  2. measure操作完成后得到的是对每个View经测量过的宽高measuredWidth/measuredHeight;
    layout操作完成之后得到的是对每个View进行位置分配后的mLeft/mTop/mRight/mBottom;
  3. layout_gravity一般出现于布局容器中,比如LinearLayout,他指的是当前容器内子视图的排列方式和顺序;
    gravity一般出现于具体的View中,比如TextView,他指的就是TextView中实际文字的位置排放方式;
  4. 凡是以layout_开头的布局参数基本都针对的是包含子视图的容器视图的,比如核心的layout_width/layout_height/layout_weight/layout_gravity,会在初始化LayoutParam时从xml中读取并转换为相应参数;
    当对一个没有父容器的View设置相关layout_开头的属性时,实际上是没有任何意义的;

Draw递归调用过程

绘制过程就是把View对象绘制到屏幕上,如果该View是一个容器ViewGroup,则需要递归绘制其所包含的所有子视图;视图中可绘制的元素包括:

  1. View背景Backgroud:每个视图都可以有一个背景,背景可以是一个颜色值,也可以是一副图片,甚至可以是任何Drawable对象;
  2. 视图自身的内容:一般由视图设计者在视图的onDraw方法中完成具体的内容绘制,比如TextView的内容就是具体的文字,若是视图容器ViewGroup,则需要递归完成子视图的具体内容绘制;
    只有这项内容是需要开发者设计并实现的,其余三项内容均由系统自动完成绘制;
  3. 渐变边框FadingEdge:其作用是为了让视图的边框看起来更有层次感,其本质就是一个Shader对象,当然可以通过配置项关闭该效果;
  4. 滚动条ScrollBar:用来显示当前滚动的位置和状态,与PC不一样,该滚动条一般不能直接按住拖动;

View系统启动draw过程是从ViewRootImpl中调用host.draw(…)开始的,总体过程与measure/layout极其类似,即从顶级父视图开始向子视图进行递归调用view.draw(…),具体可参见下图:

下面就来看看这个过程中的几个核心方法都做了哪些具体的事情;

  1. ViewRootImpl.draw()内部流程参见下图:

    里面要特别注意的一个核心点就是会根据根视图内部的Scroller对象来调用
    mScroller.computeScrollOffset()判断当前视图是否还处于滚动状态,若处于滚动状态,则会进行滚动偏移量计算,并且在最后再次调用scheduleTraversals()来发送一个异步重绘请求;
    另外一点就是:Surface会按照底层驱动模式自动执行显卡模式或CPU模式;前者采用显卡来进行页面绘制,支持硬件图形加速,一般基于OpenGL实现;后者也被成为软件模式,即通过CPU及内存来模拟图形绘制,不支持硬件加速;目前的一些高端机型几乎都是显卡模式,低端机型更多采用CPU模式;
  2. View.draw()内部流程主要就是为了完成前面提到的视图中的各种具体元素的绘制,大致过程如下:
    1. 绘制背景,由于可能会出现滚动条,所以绘制时可能会涉及到Canvas画布的平移和恢复,即 canvas.translate(scrollX, scrollY);
    2. 判断是否需要显示渐变框,若不需要则直接进入后续3、4、5绘制逻辑;
    3. 绘制视图自身内容,通过回调onDraw()实现;
    4. 调用dispatchDraw()绘制子视图,对于ViewGroup而言,默认已重载该方法,如有特别需求子类无需再次重载该函数;
    5. 调用onDrawScrollBars()绘制滚动条;
  3. ViewGroup.dispatchDraw()的作用是绘制父视图中包含的子视图,其本质就是给不同的子视图分配合适的画布Canvas,至于子视图具体如何绘制,则又会递归回调View.draw()方法;该方法内部将根据onLayout()中为子视图分配的具体区块调整Canvas的内部剪切区,从而让子视图认为画布是他自己独享的,坐标也是从(0,0)开始;其内部具体执行流程参见下图:

    有几点需要注意:
    1. 区分View动画和ViewGroup布局动画:前者指的是View自身的动画,可以通过setAnimation(.)添加;而后者是专门针对ViewGroup而言的,指的是该ViewGroup在显示内部的子视图时而设置的动画,可以在layout.xml中对容器标签设置layoutAnimation属性,比如可以对LinearLayout设置子视图在显示时出现逐行显示、随机显示、或落下等不同的动画效果;
    2. 在获取画布剪切区时会自动处理掉padding,让子视图获取的画布无需关注这些附加逻辑;
    3. 默认情况下子视图的ViewGroup.drawChild()绘制顺序与子视图被添加的顺序一致,但开发者可以重载ViewGroup.getChildDrawingOrder()方法提供不同的顺序;
    4. 当给一个子视图添加了移除动画时,该子视图会被添加到mDisappearingChildren队列中,在动画结束之前该子视图将一致存在,但此时该子视图无法被点击,也无法获得任何消息事件,仅仅是可见而已;
  4. ViewGroup.drawChild()的核心过程是为子视图分配合适的Canvas剪切区,其大小取决于child的布局大小,位置取决于child的内部滚动值、以及当前动画,其内部流程参见下图:

    有几点需要注意:
    1. 该方法在新的版本(比如4.2.2)中已经被放在View中了,ViewGroup中直接中转调用child.draw(canvas, this, drawingTime);
      这是在看android源码时需要特别注意的,View/ViewGroup之间有很多方法重载,且存在父子视图的递归调用,经常会把人搞晕,看代码时需要非常平静才行;
    2. 对于ViewGroup中子视图的动画支持有两种方式,一种是通过setAnimation(.)添加,另外一种是通过重载ViewGroup.getChildStaticTransformation(View child, Transformation t)方法实现;
      注意:这里使用的这种代码设计方式我个人觉得是很不优雅的,有点反模式的味道,他在ViewGroup中声明了一个变量 mChildTransformation,是一个空的Transformation实例,然后在View.draw(…)方法中使用时将该变量传递给上面的需要开发者重载的getChildStaticTransformation方法,开发者需要在方法内对输入参数t进行具体动画参数赋值,并return true,标示采用该静态回调方法来处理子视图的动画;
      更符合常规的设计思路不应该是让开发者直接重载getChildStaticTransformation(View child);方法并返回具体的Transformation实例吗?
  5. View.onDrawScrollBars()的作用是绘制滚动条;
    1. 滚动条是View的基本元素之一,每个View都可以有滚动条,只是某些视图从用户体验的角度无需显示而已,可以在layout.xml中通过scrollBarXXX相关的属性设置滚动条;
    2. 滚动条可以是水平的、垂直的、或者两者均有,滚动条的展示封装在ScrollBarDrawable中,包括三个基本尺寸range/offset/extent、以及用来标识滚动条背景和自身的两个图片track/thumb;
      range:代表滚动条从头至尾滚动过程中所跨越的范围有多大,比如你想用滚动条来标示一万行代码,那range就可以设置10000;
      offset:代表滚动条当前的偏移量,或者是可视的第一行在整个滚动跨度的什么位置,比如当前已经滚动到600行了,那offset就是600;
      extent:代表显示滚动条的视图View在屏幕上的可视高度或宽度,比如200dp;
      track:代表滚动条显示的背景或轨道,一般其宽高度和extent一致;
      thumb:代表滚动条显示的具体前景,其宽高度、位置会根据range/offset/extent三者的具体值进行计算获得;
      以上描述的几个变量具体展示参见下图:

    3. 滚动条一共有三种状态ON/OFF/FADDING,即显示、隐藏、正在隐藏(处于显示状态,但通过设置透明度悄悄改变状态);
      一般而言,从用户体验的角度来说滚动条会在滚动完毕后自动隐藏,而滚动时自动显示,开发者可以通过scrollBarFadeDuration参数设置自动隐藏间隔时间,也可以调用setScrollbarFadingEnabled()设置是否自动隐藏;
    4. 开发者可重载3个 computeVerticalScrollXXX()方法来实现对滚动条具体显示位置和thumb大小的控制,可以调用awakenScrollBars()方法强制显示滚动条;
      自定义滚动位置时可调用scrollTo()/scrollBy()来完成滚动到具体位置、或者滚动具体距离;


四、View动画机制

从动画效果的影响范围角度看,View系统中的动画可以分为三类:

  1. 窗口动画:窗口对应的动画,其作用对象是Surface;窗口可以是Activity对应的窗口,也可以是对话框这样的子窗口,当然还可以是直接通过WindowManager.addView()添加的任意窗口;
  2. 布局动画:指ViewGroup容器对象包含的动画,该动画在ViewGroup对象中定义,但实际上影响的却是该容器内的子视图,其本质过程是根据布局动画为每个子视图设置不同的动画,从而使得整体上看起来像是作用于整个容器;
  3. 视图动画:作用于某个具体View对象的动画;

动画类型与设计思路

动画使用Animation类来表示,该类中会保存动画的起始时间,并在动画开始后,Animation会在不同的时间返回不同的动画参数,由Transformation类来保存这些动画参数信息(内含mMatrix、mAlpha两个变量),并将这些参数应用到对应的动画主体(对于窗口动画,主体就是Surface;对于布局动画和视图动画,主体都是View)中,然后invalidate相关的动画主体,在下次重绘时就使用新的参数从而显示新的画面,这样在整个动画的时间间隔内,就像我们看电影一样产生了连续的动画效果,但其本质仍然是由一个个静态的画面所组成,只是当每秒超过60帧人眼就基本上不会感觉到卡顿或不连续了;

View系统目前支持五种基本动画:平移translate、缩放scale、旋转rotate、扭曲skew、透明阿尔法alpha;

除了扭曲Skew之外,其余四种动画都可以在xml中用对应标签来申明,代码层面就是分别对应Animation的几个子类实现,比如TranslateAnimation/ScaleAnimation/RotateAnimation/AlphaAnimation;


动画的组合使用

在我们开发过程中可能经常会组合使用动画,比如先平移、再缩放;这就需要一个统一的模型来处理这5种基本动画,其实对于每种基本动画都对应一个基本的数学变换公式,而矩阵运算更是可以方便的把这几种变换的公式都统一起来,在进行具体变换时,只需要改变矩阵中的某行某列的值即可,android系统中采用3X3矩阵(原则上2X3的矩阵已能够满足这几种动画参数的保存,但无法对2个2X3的矩阵进行组合运算,所以用3X3矩阵)来保存这5种动画的相关参数,代码层面就是Matrix类,该类中封装了除alpha之外的其余4种动画参数、以及相关操作api;主要分为三类:

  • setXXX:该系列方法用来直接设置相关动画参数,调用该系列方法将直接清除之前设置过的相关参数;
    比如代码为m.preTranslate(..); m.setRotate(.); 这样set之前的preTranslate将会被清除,相当于该句代码不奇效;
  • preXXX:该系列方法用来设置先进行某个动画,每次pre方法调用都是基于前一次pre方法的;
    比如代码为m.preTranslate(..); m.preScale(.); 这样表示先执行缩放、再平移;
  • postXXX:该系列方法用来设置后进行某个动画,每次post方法调用同样是基于前一次post方法的,并且post方法永远都在pre方法之后,与代码顺序无关;
    比如代码为m.postTranslate(..); m.preScale(.); m.postRotate(.); 这样表示先缩放、再平移、最后旋转;
    再比如m.postTranslate(..); m.setSkew(.); m.postRotate(.); m.preScale(.); 这样表示先缩放、再扭曲、最后旋转,平移因为在set之前所以被清除;

要实现组合动画的效果,可以继承Animation实现自定义的类,核心是重载void applyTransformation(float interpolatedTime, Transformation t)方法;

注意:改变的动画参数实际上是保存在参数t中,t会在类初始化时自动new一个空的Transformation对象,之后调用t.getMatrix()获取矩阵,并进行相关设值;

在代码层面还需认识Interpolator类,他是一个插值器,他的作用是可以让开发者自定义动画随时间的变化曲线,比如常见的有做匀速运动、直线加速运动、曲线加速、先加速后减速等,其实这是一个数学模型问题,开发者可以实现任意的插值器;


视图动画的使用

  1. 可以在res/drawable目录中新建xml文件,和我们新建shape/selector效果图片集一样,内部采取animation_list标签来设定各个时刻需要显示的图片,其本质上是创建了一个AnimationDrawable,可以简单理解为具有动画效果的GIF图片,也有叫逐帧动画,使用Drawable的地方均可以拥有该动画效果,动画的开始与结束可调用AnimationDrawable.start()/stop()方法;
  2. 可以在res/anim目录下新建xml文件,内部采用animationSet/scale/rotate/translate/alpha标签来设置这几种简单的动画;
    之后利用AnimationUtil.loadAnimation(.)将其加载转换为Animation对象,并调用View.setAnimation(.);
  3. 实现自定义的Animation子类,直接调用View.setAnimation(.);完成动画设定,通过Animation.start()/cancel()控制动画开始和结束;
    视图动画在执行时,实际上改变的是View的位置,并没有改变View自身的属性,比如你对一个Button进行缩放操作,在动画执行过程中表面看Button尺寸发生了变化,但实际上Button的点击响应区域并没有变化;

布局动画的使用

  1. 可以在res/anim目录下新建xml文件,内部采用layoutAnimation标签来定义布局动画,代码层面就对应于 LayoutAnimationController类,内部再通过android:animation标签引用具体的视图动画;之后在layout.xml中对需要使用动画的布局容器(比如ListView/LinearLayout)通过标签layoutAnimation实现对刚才定义的布局动画的引用;布局容器初始化时会自动解析相关动画设置,并在执行dispatchDraw()方法中将布局动画转换为对应子视图的视图动画,也通过ViewGroup.startLayoutAnimation()启动动画;
    疑问:布局动画可以手工停止吗?有兴趣的同学自己去找答案;
  2. 实现自定义 LayoutAnimationController子类,关键的是要重载long getDelayForView(View view)方法;用来根据子视图在父视图中的index返回具体的delay毫秒数;可直接调用ViewGroup.setLayoutAnimation(.);完成动画设定,通过LayoutAnimationController.start()启动动画;

窗口动画的使用

窗口动画的具体实现是在WindowManagerService.applyAnimation()方法中,这里将主要介绍Activity对应窗口动画的使用,即Activity间切换时的动画;

  1. 与视图动画一样在res/anim目录下新建xml动画文件分别用于窗口进入、关闭时的效果,之后在执行Activity.startActivity(Intent intent);之后调用Activity.overridePendingTransition(..)完成对老窗口关闭、新窗口进入动画效果的设定;
  2. 通过 ActivityOptions.makeXXX(…)系列构造方法返回具体实例,并通过ActivityOptions.toBundle()将其转换为Bundle对象,最后调用Activity.startActivity(Intent intent, Bundle bundle);启动Activity;
    注意:和上一种方式的区别是启动时多了一个Bundle参数,该参数里面就存放了相关动画参数;
    ActivityOptions.makeXXX()系列方法目前提供了三种类型:
    1. makeCustomAnimation:加载自定义动画,和第一种加载方式一致;
    2. makeScaleUpAnimation:加载从指定点然后缩放至最终显示窗口的动画;
    3. makeThumbnailScaleXXXAnimation:加载从显示窗口缩小到无、或从无到指定窗口的动画;

属性动画的出现

属性动画是在3.0中出现的,相关的类位于android.animation里面,而老式动画(以上提到的几种都是)支持则位于android.view.animation里面;

与视图动画比较而言,属性动画更具灵活性,他不仅可以用于视图View,也可以用于任何对象;动画期间他改变的是View的具体属性,同样是Button按钮缩放操作,动画期间该Button的具体大小和位置就已经发生了变化,有效点击响应区域也已随属性变化而变化;常见使用方式如下:

  1. 最常见的方式是使用 ObjectAnimator.ofXXX(Object target, String propertyName, …)方法获得Animator实例,并调用start()方法启动动画;
    其中target为待执行动画的主体,比如某个Button对象,propertyName为待改变的属性名,比如alpha;若要对多个属性进行变化,则可以使用 PropertyValuesHolder来保存各个属性的相关变化信息;
    注意:要使用ObjectAnimator必须要求待改变的属性具有set/get方法;
  2. 使用ValueAnimator.ofXXX(..)方法获得Animator实例,并调用 addUpdateListener(AnimatorUpdateListener listener)完成属性变化Listener的注册,其中参数listener为AnimatorUpdateListener接口的实现类;之后调用start()方法启动动画;
    该方式具有非常好的扩展性和通用性,动画的执行和过程中要做的具体属性值改变完全解耦了,开发者可以在AnimatorUpdateListener接口实现里做任何想做的事情;
  3. 可以在res/anim目录下新建xml文件,内部采用set/animator/objectAnimator等标签来定义动画信息,代码层面就对应于AnimatorSet/ValueAnimator/ObjectAnimator,之后再通过AnimatorInflater.loadAnimator(..)来完成对xml描述的加载进而获得Animator对象,通过animation.setTarget(.)设定目标主体,并用start()启动动画; 
  4. 初始化LayoutTransition,并通过LayoutTransition.setAnimator(int transitionType, Animator animator)完成对具体场景下动画的指定,之后调用ViewGroup.setLayoutTransition(.)完成对布局容器添加动画支持,从而完成当ViewGroup中子视图可视状态发生变化时的动画应用 
     transitionType的具体场景包括:
    • APPEARING:当一个元素变为Visible时对其应用的动画
    • DISAPPEARING:当一个元素变为InVisible时对其应用的动画
    • CHANGE_APPEARING:当一个元素变为Visible时,因系统要重新布局有一些元素需要移动,这些要移动的元素应用的动画
    • CHANGE_DISAPPEARING:当一个元素变为Gone时,因系统要重新布局有一些元素需要移动,这些要移动的元素应用的动画

五、相关引用

http://blog.csdn.net/sfdev(以上转载出处)

http://m.blog.csdn.net/blog/hellolinshoujie/25911563

http://djt.qq.com/article/view/987(Android显示原理简介)

http://blog.csdn.net/xu_fu/article/details/7829721(View绘图,Android内核剖析笔记)



  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值