Android動畫解析(一)—— Frame Animation(幀動畫)
動畫在我們實際開發中佔有很重要的地位,一個優秀的動畫能為我們的app應用增色很多,同時一個優秀的動畫銜接能夠增加我們app的邏輯展示。在Android系統中,系統給我們提供了幾種動畫的支援,分別是Frame Animation(幀動畫)、Tween Animation(補間動畫)以及3.0系統以後增加的Property Animator(屬性動畫)。這些動畫的熟練使用可以協助我們設計出perfect效果的動畫,下面就開始我們的學習吧!
一、概述
幀動畫,顧名思義就是這個動畫的效果是由一幀幀的圖片組合出來的。通過制定圖片展示的順序,達到動畫的展示效果。
在Android開發中,系統給我們提供了”animation-list” 節點用於我們配置幀動畫。
實現步驟
1、在res目錄下建立用於儲存xml動畫檔案的anim檔案夾,res/anim,也可以放在drawable目錄下
2、動畫配置,在animation-list節點中配置item項
2、將檔案設定到ImageView控制項的背景上,然後擷取背景轉換為AnimationDrawable對象進行播放動畫
iv_imageView.setBackgroundResource(R.drawable.frame_animation); AnimationDrawable animation = (AnimationDrawable)iv_imageView.getBackground(); animation.start();
二、執行個體講解
1、奔跑的飛馬
我們先來看下,第一個執行個體,飛奔的飛馬。
對於這種動畫效果使用Frame Animation動畫即可完成,我們只需要將gif動畫進行分幀切割成圖片,然後我們在animation-list標籤中指定圖片的順序進行播放即可。
代碼實現
<code class=" hljs xml"><animation-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@mipmap/Horse_start" android:duration="200"> <item android:drawable="@mipmap/Horse1" android:duration="200"> <item android:drawable="@mipmap/Horse2" android:duration="200"> <item android:drawable="@mipmap/Horse3" android:duration="200"> <item android:drawable="@mipmap/Horse4" android:duration="200"> <item android:drawable="@mipmap/Horse5" android:duration="200"> <item android:drawable="@mipmap/Horse6" android:duration="200"> <item android:drawable="@mipmap/Horse7" android:duration="200"> <item android:drawable="@mipmap/Horse8" android:duration="200"> <item android:drawable="@mipmap/Horse_start" android:duration="200"> </item></item></item></item></item></item></item></item></item></item></animation-list></code>
在animation-list中,item的先後順序就是圖片在動畫中播放的順序。順序設定好了以後,我們就將該anim綁定到我們的ImageView上,然後進行播放。
最後就是我們的代碼,擷取到該drawable,然後進行播放。
ImageView iv_animaView = (ImageView) findViewById(R.id.iv_frame); AnimationDrawable animationDrawable = (AnimationDrawable) iv_animaView.getBackground(); animationDrawable.start();
看下動畫效果:
是不是很簡單,簡單的幾行代碼就可以做出一個gif的動畫效果,不過現在貌似有開原始檔控制可以載入gif圖片,回頭研究下看看二者的效率如何。
2、裸奔的機器人
通過前面的一個案例,我們已經基本熟悉了Frame Animation的使用,下面我們在做一個例子,來鞏固下知識點。
代碼實現
<code class=" hljs xml"><animation-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@mipmap/zzlx1" android:duration="200"> <item android:drawable="@mipmap/zzlx2" android:duration="200"> <item android:drawable="@mipmap/zzlx3" android:duration="200"> <item android:drawable="@mipmap/zzlx4" android:duration="200"> <item android:drawable="@mipmap/zzlx5" android:duration="200"> <item android:drawable="@mipmap/zzlx6" android:duration="200"> <item android:drawable="@mipmap/zzlx7" android:duration="200"> <item android:drawable="@mipmap/zzlx8" android:duration="200"> </item></item></item></item></item></item></item></item></animation-list></code>
後面的代碼同上面,我們只需要看看我們的實現效果即可。
三、幀動畫原理分析
在上面的開發中,我們在將backgroud對應的Drawable對象轉換為一個AnimationDrawable對象,然後由這個對象啟動Frame動畫,那麼這個類究竟是由何方神聖呢?讓我們一起look look。
1、AnimationDrawable概述
AnimationDrawable用於建立frame-by-frame(逐幀)動畫,它定義了一些列的Drawable對象可用於設定View的backgroud背景屬性。frame-by-frame動畫最簡單的方式是通過XML檔案進行建立,然後將xml檔案放到res/drawable/folder檔案夾下,同時將此drawa對象設定到view的backgroud屬性。Xml檔案的組成:
animation-list:根節點,包含一系列的item item:每個item對應一個frame(幀)
下面是在代碼中建立和使用幀動畫:
ImageView img = (ImageView)findViewById(R.id.spinning_wheel_image); img.setBackgroundResource(R.drawable.spin_animation); AnimationDrawable frameAnimation = (AnimationDrawable) img.getBackground(); frameAnimation.start();
我們在來看看AnimationDrawable對象給我們提供的屬性。
AnimationDrawable_visible:設定是否可見 AnimationDrawable_variablePadding: AnimationDrawable_oneshot:設定是否只播放一次,true是,false否 AnimationDrawableItem_duration:設定每幀動畫之間的時間間隔 AnimationDrawableItem_drawable:設定每幀之間間隔的drawable對象
2、AnimationDrawable源碼分析
上面我們已經對AnimationDrawable進行了一個簡要的分析,瞭解了一些它的屬性,我們心中擷取對幀動畫還有一些疑惑,比如一些問題:
AnimationDrawable是如何形成一個個幀畫面? Frame Animation是如何?不斷迴圈播放? 我們能否通過代碼控制Frame動畫的播放?
下面我們就圍繞上面的幾個問題對AnimationdDrawable進行分析。查看源碼,我們可以看到AnimationDrawable暴露的public方法。
AnimationDrawable根據名稱,我們也能推算到這是一個Drawable的子類,我們仔細一想,為什麼通過getBackgroud()方法獲得的Drawable對象可以轉換到AnimationDrawable這個子類呢?這就需要我們去看下源碼。在Drawable類中,有一個方法createFromXml()方法:<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> /** * Create a drawable from an XML document. For more information on how to * create resources in XML, see * Drawable Resources. */ public static Drawable createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException { return createFromXml(r, parser, null); }
這個方法就是用於將我的XML檔案轉換成一個drawable對象,我們接著深入下去,看下createFromXml()這個方法。
public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme) throws XmlPullParserException, IOException { AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type=parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty loop } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException("No start tag found"); } Drawable drawable = createFromXmlInner(r, parser, attrs, theme); if (drawable == null) { throw new RuntimeException("Unknown initial tag: " + parser.getName()); } return drawable; }
在這裡我們看到了XML檔案轉換成Drawable的內部,在Android系統中,同樣是通過XmlPullParser進行Xml檔案的解析,在上面的方法中,首先進行xml檔案的開始標籤和結束標籤,判斷xml檔案內部是否為空白節點。然後通過:
Drawable drawable = createFromXmlInner(r, parser, attrs, theme);
進行XML檔案解析,最後轉換成Drawable對象。
public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { final Drawable drawable; final String name = parser.getName(); switch (name) { case "selector": drawable = new StateListDrawable(); break; case "animated-selector": drawable = new AnimatedStateListDrawable(); break; case "level-list": drawable = new LevelListDrawable(); break; case "layer-list": drawable = new LayerDrawable(); break; case "transition": drawable = new TransitionDrawable(); break; case "ripple": drawable = new RippleDrawable(); break; case "color": drawable = new ColorDrawable(); break; case "shape": drawable = new GradientDrawable(); break; case "vector": drawable = new VectorDrawable(); break; case "animated-vector": drawable = new AnimatedVectorDrawable(); break; case "scale": drawable = new ScaleDrawable(); break; case "clip": drawable = new ClipDrawable(); break; case "rotate": drawable = new RotateDrawable(); break; case "animated-rotate": drawable = new AnimatedRotateDrawable(); break; case "animation-list": drawable = new AnimationDrawable(); break; case "inset": drawable = new InsetDrawable(); break; case "bitmap": drawable = new BitmapDrawable(); break; case "nine-patch": drawable = new NinePatchDrawable(); break; default: throw new XmlPullParserException(parser.getPositionDescription() + ": invalid drawable tag " + name); } drawable.inflate(r, parser, attrs, theme); return drawable; }
在createFromXmlInner方法中,首先擷取我們都xml檔案的標籤,然後根絕我們對應的標籤名稱建立對應的drawable對象,比如我們這次建立的AnimationDrawable對象。然後調用inflater()方法,由於AnimationDrawable方法中已經對inflater方法進行了重寫,所以此時這個就是:
@Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable); super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible); updateStateFromTypedArray(a); a.recycle(); inflateChildElements(r, parser, attrs, theme); setFrame(0, true, false); }
至此,我們已經基本理清了從XML檔案到Drawable對象的轉換流程,現在我們就開始分析animation-list節點下的節點如何形成一個個幀動畫效果的。在進行分析之前,我們先瞭解下AnimationState類。這個類用於儲存我們的一系列drawable。通過源碼發現:
private final static class AnimationState extends DrawableContainerState
這個類繼承DrawableContainerState類,DrawableContainerState中有一個成員變數Drawable[] mDrawables;用於儲存我們的drawable資訊。明白這一點,我們就可以分析方法inflateChildElements方法。
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { int type; final int innerDepth = parser.getDepth()+1; int depth; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { if (type != XmlPullParser.START_TAG) { continue; } if (depth > innerDepth || !parser.getName().equals("item")) { continue; } final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawableItem); final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); if (duration < 0) { throw new XmlPullParserException(parser.getPositionDescription() + ": tag requires a 'duration' attribute"); } Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); a.recycle(); if (dr == null) { while ((type=parser.next()) == XmlPullParser.TEXT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException(parser.getPositionDescription() + ": tag requires a 'drawable' attribute or child tag" + " defining a drawable"); } dr = Drawable.createFromXmlInner(r, parser, attrs, theme); } mAnimationState.addFrame(dr, duration); if (dr != null) { dr.setCallback(this); } } }
在這方法裡面通過TypeArray擷取drawable的相關資訊,然後調用mAnimationState的addFrame方法,將一系列動畫資訊就儲存在drawable數組中。
通過上面的分析,一系列的動畫已經轉出並進行了儲存,我們接下來的任務就是進行start的分析,分析動畫的開啟。
public void start() { mAnimating = true; if (!isRunning()) { // Start from 0th frame. setFrame(0, false, mAnimationState.getChildCount() > 1 || !mAnimationState.mOneShot); } }
通過setFrame方法設定我們的drawable,裡面有selectDrawable(frame)進行設定。
基本的流程就是這個樣子,主要理解使用即可。後面會接著分析幾篇關於動畫的文章。