Android屬性動畫詳解(一),屬性動畫基本用法

dumaokun發表於2016-08-02

Hello,大家好,今天又來裝逼了,裝逼也上癮啊,最近公司不是特別忙,我想這也就是我出來裝逼的最好時機吧!額,,哈哈,進入正題。如有疑問歡迎留言,如有謬誤歡迎批評指正。

在Tween動畫的討論中,我們提到在Android中動畫可以分為三類:①幀動畫②Tween(補間動畫)③Property Animation(屬性動畫),在前面的文章中,分別對幀動畫和Tween動畫進行了非常詳細的討論,如果有興趣可以去上面的連結去閱讀。那麼今天就來和大家一起討論下Property Animation,相信通過本系列部落格的討論你將對Android中的動畫有個非常詳細的瞭解。

我的部落格地址blog.csdn.net/dmk877,大家可以去看看,幀動畫Tween動畫的講解。

通過本篇部落格你將學到以下內容:

①為什麼要引入屬性動畫

②屬性動畫的基本用法

③屬性動畫的監聽器

④組合動畫的實現

⑤屬性動畫的XML實現

1、為什麼要引入屬性動畫

首先來看為什麼要引入屬性動畫,我相信很多人跟我一樣,看到屬性動畫,在腦海裡閃現的第一個問題就是為什麼要引入屬性動畫?我們都知道Android中已經有幀動畫和補間動畫了,那麼為什麼還要引入屬性動畫呢?要想得到這個問題的正確答案,無疑要去谷歌的官網了,首先我們來看看官網(官網地址)對Property Animation與補間動畫的區別進行的介紹:

補間動畫只提供了對View進行增加動畫的能力,所以如果你想對除View之外的其它物件做動畫,你必須實現你自己的程式碼來達到這樣的效果。另外,補間動畫只能對View的幾個方面進行動畫的新增,例如View的縮放和旋轉,而不是View的背景顏色等等。

補間動畫的另一個缺點是它只修改了檢視繪製的地方,而不是實際View的本身。比如,你給Button定義了一個在螢幕上移動的動畫,這個Button的繪製是正確的,但是這個Button實際可以點選的位置是沒有改變的,所以你必須用你自己的邏輯來解決這個問題。

使用屬性動畫這些約束將完全被解除,並且你可以對任何物件(Views and non-Views)的任何屬性新增動畫,並且這個物件的本身實際也是改變的。從更高層次上來說,你可以選擇你想要的屬性,來給其新增動畫,如顏色、位置或大小,並且你可以通過插值器或者多個動畫的同步,來定義你所需要的動畫。

然而補間動畫需要較少的時間來設定,並且也需要更少的程式碼。如果補間動畫完成了你所需要做的一切或者現有的程式碼就是按照你想要的方式工作的,那麼你沒有必要使用屬性動畫。針對不同的情況有時候也許需要這兩種動畫進行工作才是有意義的。

以上三段就是官網給出的屬性動畫與補間動畫的區別,可能看著比較費勁,其實引入屬性動畫主要有三點原因:

①因為補間動畫只能對View進行操作,而不能對一個物件的屬性,如顏色等進行操作,而屬性動畫可以,並且屬性動畫的操作範圍不僅僅是View,它可以是任何物件。

②補間動畫只能對View的幾個方面做動畫,也就是說補間動畫不僅把範圍縮小到View,而且並不是能對View的各個方面做動畫,而只能是alpha(漸變)、scale(縮放)、translate(位移)、rotate(旋轉)這四種動畫。

③補間動畫只是改變了View繪製的地方,而並沒有真正改變View本身。什麼意思?假如手機螢幕上有一個View,我們讓他做補間動畫向右移動20px,我們會看到這個View向右移動了20px,而此時你會發現這個View是不能響應你的點選事件的,只有你點選原來的位置才能觸發這個View的點選事件。因為這個View實際還在原來的位置,只不過補間動畫將這個View繪製的地方向右移動了20px,而這個View真正的屬性並沒有改變。

通過上面的介紹,相信大家已經明白了屬性動畫產生的原因,瞭解了它產生的背景之後,接下來的一步就是要學習它的用法了。

2、屬性動畫的介紹

屬性動畫常用的有兩個類分別是ValueAnimator和ObjectAnimator,它的繼承關係圖如下: 這裡寫圖片描述 從繼承關係圖中我們可以清晰的看到ValueAnimator和AnimatorSet是繼承與抽象類Animator的,而ObjectAnimator和TimeAnimator是繼承自ValueAnimator的。這個繼承關係圖,大家要好好識記一下,後面會用到,知道這些關係後,我們的討論的方向就更加明確了,總共就這麼幾個類,逐一來看看唄。

3、ValueAnimator的基本使用

在學習ValueAnimator的基本使用之前,先來看下它的一些常用的方法

//設定操作屬性的一系列值float
ofFloat(float... values)
ofInt(int... values)
ofObject(TypeEvaluator evaluator,Object... values)
//用於實現組合動畫等
ofPropertyValuesHolder(PropertyValuesHolder... values)
setFrameDelay(long frameDelay)
ValueAnimator setDuration(long duration) 
//獲取ValueAnimator在運動時,當前運動點的值   
Object getAnimatedValue();  
//開始動畫  
void start()  
//設定迴圈次數,設定為INFINITE表示無限迴圈  
void setRepeatCount(int value)  
//設定迴圈模式 value取值有RESTART,REVERSE,  
void setRepeatMode(int value)  
// 取消動畫  
void cancel()複製程式碼

在上面的方法中,可能大家比較陌生的就是of開頭的那幾個,其中ofFloat,ofInt它們接收的引數型別都是可變參,所以我們可以傳入任何數量的值;傳進去的值列表,就表示動畫時的變化範圍;比如ofInt(3,9,6)就表示從數值3變化到數值9再變化到數值6。而ofObject接收兩個引數一個TypeEvaluator型別的,另一個是Object型別的可變引數,關於TypeEvaluator,後續的文章會有詳細的討論。然後就是ofPropertyValuesHolder多屬性動畫同時工作管理類,有時候我們需要對一個物件的多個屬性做動畫,此時就會用到它。setFrameDelay設定多長時間重新整理一幀,預設是10ms。但最終依賴系統的當前狀態,一般我們不用管。

說了這麼多廢話,到底怎麼用,其實ValueAnimator的使用非常簡單,首先來看個最基礎的用法,假如我們想建立一個值從0到1的動畫,動畫的時長為200毫秒,程式碼應該這樣寫:

ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,1);
valueAnimator.setDuration(200);
valueAnimator.start();複製程式碼

執行上面的程式碼就執行了一個值從0到1平滑過渡的動畫,從上面的程式碼中可以看出它並沒有與任何的控制元件的任何屬性有關係,從它的名字也能看出來它是對值做平滑過渡的,我們怎麼知道呢?很簡單隻需要對它做監聽就可以了,我們只需要新增如下程式碼:

ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,1);
valueAnimator.setDuration(200);

valueAnimator.addUpdateListener(new AnimatorUpdateListener() {

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float value=(Float) animation.getAnimatedValue();
        Log.i("MainActivity","currentValue:"+value);
    }
});
valueAnimator.start();複製程式碼

在上述程式碼中我們給valueAnimator新增了一個addUpdateListener監聽,在監聽的回撥中,回撥給我們的是當前狀態的ValueAnimator 的例項,得到這個例項後通過呼叫getAnimatedValue就可以拿到當前的值,然後將其列印,這裡有一點需要提醒大家注意的是拿到的這個值的型別是與of..後的值得型別相對應的,ofFloat拿到的就是float型別,ofInt拿到的就是int型別。

執行上述程式碼列印結果如下:

這裡寫圖片描述

從列印結果中可以看到valueAnimator的值在200毫秒內從0逐漸變化到了1,這些中間的過程谷歌已經幫我們實現好了。

在上面我們提到ofFloat(float... values)接收的引數型別是可變參,也就是說這裡傳遞的引數的個數是沒有限制,我們也可以傳遞多個引數,比如

ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,3,1);
valueAnimator.setDuration(200);
valueAnimator.start();複製程式碼

上述程式碼就表示在200毫秒內,valueAnimator的值從0變化到3,然後再變化到1。ofInt的使用與ofFloat類似,只不過傳的值的型別不同。

到這裡可能有的同學會問,說了半天沒有看到ValueAnimator做一個動畫啊,那接下來就讓一個View做位移動畫,程式碼如下:

ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,100);
valueAnimator.setDuration(3000);

valueAnimator.addUpdateListener(new AnimatorUpdateListener() {

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float value=(Float) animation.getAnimatedValue();
        ivImage.setTranslationX(value);
    }
});
valueAnimator.start();複製程式碼

在上述程式碼中通過對valueAnimator新增監聽,拿到當前幀的值後,不斷的設定ImageView的TranslatonX(該View在X軸的偏移量)值,從而讓其移動。它的執行效果如下:

這裡寫圖片描述

可以看到我們通過使用ValueAnimator實現了在3秒內在X軸方向上移動100px的效果。這個動畫的操作是在ValueAnimator的監聽中實現的。

小總結: ValueAnimator是計算動畫過程中變化的值,包含動畫的開始值,結束值,持續時間等屬性。但是這些值與我們的控制元件是無關的,要想把計算出來的值應用到物件上,必須為ValueAnimator註冊一個監聽器,該監聽器負責更新物件的屬性值。在實現這個監聽器的時候,可以通過getAnimatedValue()的方法來獲取當前動畫的值,拿到這個值後,我們就可以為所欲為了。

4、ObjectAnimator的基本用法

相比於ValueAnimator,在開發中可能ObjectAnimator要比ValueAnimator用的多,因為ObjectAnimator可以直接操作物件的屬性,而不用像ValueAnimator那麼麻煩。 假如讓一個ImageView做旋轉的動畫,程式碼可以這樣寫:

ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(ivImage,"rotation",0,360);
objectAnimator.setDuration(3000);
objectAnimator.start();複製程式碼

從上述程式碼我們可以看到ObjectAnimator與ValueAnimator的用法有點相似,又有不同,在上述程式碼中objectAnimator呼叫了ofFloat()方法來去建立一個ObjectAnimator的例項,與ValueAnimator不同的是,這裡的ofFloat()方法當中接收的引數有點變化了。這裡第一個引數要求傳入一個object物件,即進行動畫的物件,在上面我們傳了一個ImageView。第二個引數是屬性的名字,因為做旋轉動畫所以這裡傳的屬性的名字為“rotation”。後面就是可變引數了,這裡我們傳的是0,360,表示讓ImageView旋轉360度,然後設定時長,呼叫start方法。美女效果如下,啊,不是,是執行效果如下: 這裡寫圖片描述

可以看到美女還是不錯的,啊。。不是,是執行效果還是不錯的。

假如想看到透明度漸變的效果呢,程式碼可以這麼寫:

ObjectAnimator objectAnimator;
objectAnimator=ObjectAnimator.ofFloat(ivImage,"alpha",0,1);
objectAnimator.setDuration(3000);
objectAnimator.start();複製程式碼

執行效果如下:

這裡寫圖片描述

在上面的程式碼中我們設定裡的“alpha”屬性,讓其在3秒內完成透明度從0到1的變化。

到這裡從總體上看,屬性動畫的用法還是比較簡單的,肯定有的童鞋會有疑問,ofFloat中的第二個引數都是能傳哪些值呢?上面的程式碼中傳了個“alpha”和"rotation",但是究竟它能傳哪些值呢?這一點從其名字中可以看出“屬性”動畫,無疑它是操作物件的屬性的,所以它可以接收任意值,但是這裡有一個前提,那就是這個屬性必須要有get和set方法,什麼意思呢?屬性動畫針對我們傳入的屬性值,比方說“alpha”,它會去尋找這個屬性名所對應的get和set方法,內部會通過java反射機制來呼叫set函式修改物件屬性值。由此我們可以推斷出ImageView中肯定會有對alpha屬性的get和set操作,通過尋找你會發現這兩個方法在ImageView的父類View中,通過尋找在View中確實找到了這兩個方法如下:

public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha);
public float getAlpha();複製程式碼

這也進一步驗證我們的說法,到這裡我們也知道,所有繼承自View的控制元件都可以進行alpha變換,因為View中就有getAlpha和setAlpha方法。也許到這有的童鞋還會心有餘悸心想上述說的我理解了,但是假如說我想對View的屬性進行變換,不可能每次都要去View的原始碼裡去看看它有沒有get和set方法吧,這裡呢,對經常用到的屬性做一個小的總結:

①translationX和translationY:表示在X軸或者Y軸方向上的位移

② scaleX和scaleY:表示在X軸或者Y軸方向上的縮放

③rotation、rotationX和rotationY:這三個屬性控制View物件圍繞支點進行2D和3D旋轉。

④ pivotX和pivotY:這兩個屬性控制著View物件的支點位置,圍繞這個支點進行旋轉和縮放變換處理。預設情況下,該支點的位置就是View物件的中心點。

⑤x和y:這是兩個簡單實用的屬性,它描述了View物件在它的容器中的最終位置,它是最初的左上角座標和translationX和translationY值的累計和。

⑥ alpha:它表示View物件的alpha透明度。預設值是1(不透明),0代表完全透明(不可見)。

當然我們可以操作的屬性遠遠不止這些,任何屬性只要有get和set方法,我們都可以操作。

ObjectAnimator是屬性動畫框架中最重要的實行類,建立一個ObjectAnimator只需通過他的靜態工廠類直接返回一個ObjectAnimator物件。傳的引數包括一個物件和物件的屬性名字,但這個屬性必須有get和set函式,還包括屬性的初始值,最終值,還可以呼叫setInterpolator設定曲線函式。

5、Animator監聽

對於Animator的監聽在上面的程式碼中也略有體現,我們通過呼叫addUpdateListener這個方法給ValueAnimator新增了一個監聽,其實從ValueAnimator的原始碼中可以看出它總共是有兩個監聽器的,監聽器相關原始碼:

 //第一種監聽:AnimatorUpdateListener 這個監聽是在ValueAnimator類中的
 public static interface AnimatorUpdateListener {
     void onAnimationUpdate(ValueAnimator animation);
 }
 //新增AnimatorUpdateListener監聽,系統提供給我們的方法
 public void addUpdateListener(AnimatorUpdateListener listener)

 //第二種監聽:這個監聽是在Animator這個類中的
 public static interface AnimatorListener {

      void onAnimationStart(Animator animation);
      void onAnimationEnd(Animator animation);
      void onAnimationCancel(Animator animation)void onAnimationRepeat(Animator animation);
   }
 //新增AnimatorListener呼叫的方法
 public void addListener(AnimatorListener listener);複製程式碼

這裡有一點大家需要明白,大家可以回到開始我們給出的繼承關係圖,從繼承關係圖中我們可以看出 AnimatorSet和ValueAnimator是繼承自Animator的,而ObjectAnimator是繼承自ValueAnimator的。所以對於Animator類中的監聽,AnimatorSet、ValueAnimator、ObjectAnimator都可以用,而ValueAnimator類中的監聽,AnimatorSet中是沒有的,而ObjectAnimator是繼承自ValueAnimator的,所以ValueAnimator和ObjectAnimator都是可以呼叫的。理論說完,就上例項我們可以這樣為屬性動畫新增AnimatorListener 監聽:

objectAnimator.addListener(new AnimatorListener() {

        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {

        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }
    });複製程式碼

可以看到AnimatorListener提供了對動畫開始、動畫重複、動畫結束、取消動畫做了監聽。但是有時候我們並不需要這麼多啊,比如我們只想監聽動畫的開始,假如用這種方法需要把這四個方法都重寫才行,程式碼太冗餘了,谷歌的攻城獅也是想到了這一點,給我們提供了一個介面卡AnimatorListenerAdapter,有這個類我們就可以選擇性的,根據需要新增監聽了,比如我們只需要新增動畫開始時的監聽,我們可以這麼做:

objectAnimator.addListener(new AnimatorListenerAdapter() {

    @Override
    public void onAnimationStart(Animator animation) {
        super.onAnimationStart(animation);
    }
});複製程式碼

有木有比上面簡化了很多,可以看出谷歌對屬性動畫的優化還是下了很多功夫的。

6、組合動畫的實現

上面我們都是對一個物件進行單一的動畫,但是一個很酷的動畫往往需要多個動畫協同完成,谷歌也是給我提供了多種實現方式,一起來看看吧。

要想完成多個動畫協同工作需要藉助AnimatorSet這個類,這個類主要提供了三個播放方法,play(),playSequentially(),playTogether()。其中playSequentially()表示多個動畫按順序執,它主要有兩種形式的引數playSequentially(Animator... items)和playSequentially(List <Animator> animator);一個是可變引數,另一個是動畫集合。 playTogether()表示幾個動畫同時執行,它接收的引數型別與playSequentially()一致。最後就是play方法了,play方法接收一個Animator動畫例項,play(Animator anim),呼叫它之後會返回一個AnimatorSet.Builder的例項,AnimatorSet.Builder中包括以下四個方法

  • after(Animator anim) 將現有動畫插入到傳入的動畫之後執行
  • after(long delay) 將現有動畫延遲指定毫秒後執行
  • before(Animator anim) 將現有動畫插入到傳入的動畫之前執行
  • with(Animator anim) 將現有動畫和傳入的動畫同時執行 好了,理論完了之後就要聯絡實際了,那接下來我們來做一個這樣的組合效果:讓一張圖片旋轉出廠的同時伴隨著漸變和縮放,程式碼可以這樣寫:
    ObjectAnimator scaleYAnimator=ObjectAnimator.ofFloat(ivImage,"scaleY",0,1);
    ObjectAnimator scaleXAnimator=ObjectAnimator.ofFloat(ivImage,"scaleX",0,1);
    ObjectAnimator rotationXAnimator=ObjectAnimator.ofFloat(ivImage,"rotation",0,360);
    ObjectAnimator alphaAnimator=ObjectAnimator.ofFloat(ivImage,"alpha",0,1);
    AnimatorSet set=new AnimatorSet();
    set.playTogether(scaleXAnimator,scaleYAnimator,alphaAnimator,rotationXAnimator);
    set.setDuration(3000);
    set.start();複製程式碼
    執行效果如下

這裡寫圖片描述

可以看出它是漸變、旋轉、縮放、三種動畫的組合,效果還算不錯。

接著我們來看下play的用法,與上述動畫類似,我們來實現這樣一個動畫,讓一張圖片縮放旋轉出廠,出廠之後讓它消失,可以用play實現,程式碼如下:

ObjectAnimator scaleYAnimator=ObjectAnimator.ofFloat(ivImage,"scaleY",0,1);
ObjectAnimator scaleXAnimator=ObjectAnimator.ofFloat(ivImage,"scaleX",0,1);
ObjectAnimator rotationXAnimator=ObjectAnimator.ofFloat(ivImage,"rotation",0,360);
ObjectAnimator alphaAnimator=ObjectAnimator.ofFloat(ivImage,"alpha",1,0); 
AnimatorSet set=new AnimatorSet();
/**讓scaleYAnimator、scaleXAnimator、rotationXAnimator同時執行
  *執行完之後執行alphaAnimator*/
set.play(scaleXAnimator).with(scaleYAnimator);
set.play(scaleYAnimator).with(rotationXAnimator);
set.play(alphaAnimator).after(rotationXAnimator);

set.setDuration(3000);
set.start();複製程式碼

執行效果如下:

這裡寫圖片描述

這樣我們就用play實現了一個比較不錯的組合動畫了。

7、xml檔案實現

前面我們在學Tween動畫的時候,我們是分兩篇介紹的,一篇是xml檔案配置的實現,一篇是程式碼的實現,上述我們都是用程式碼實現的屬性動畫,那麼怎麼配置xml檔案實現的?它的實現也很簡單,首先需要做的就是在res下建立一個animator資料夾,然後建立一個xml檔案,/res/animator/roation.xml。 在xml檔案中總共有可以用三個標籤,與程式碼實現是對應著的

  • <animator> 對應程式碼中的ValueAnimator
  • <objectAnimator> 對應程式碼中的ObjectAnimator
  • <set< 對應程式碼中的AnimatorSet 那麼它們都可以設定哪些屬性值呢? animator中的屬性如下:

    複製程式碼
  • android:duration:表示動畫播放的時長

  • android:valueFrom:動畫屬性開始的值;取值範圍為float,int和color,如果未指定,動畫開始屬性通過屬性的get方法獲得。顏色用6位16進位制數表示(例如:#333333)

  • android:valueTo:動畫結束值;取值範圍同valueFrom

  • android:startOffset:取值範圍為int,動畫的start方法被呼叫後,延遲多少毫秒執行。

  • android:repeatCount:動畫重複的次數,可以設定為-1或者正整數,-1表示無限迴圈,假如我們設定成1,<font color="#FF000000>表示重複執行一次,所以它總共會執行2次。

  • android:repeatMode:動畫重複模式,取值為repeat和reverse;repeat表示正序重播,reverse表示倒序重播,這與前面講的Tween動畫是類似的。

  • android:valueType:表示引數值型別,取值為intType和floatType;與android:valueFrom、android:valueTo相對應。如果這裡的取值為intType,那麼android:valueFrom、android:valueTo的值也就要對應的是int型別的數值。float也是一樣。如果如果android:valueFrom、android:valueTo的值設定為color型別的值,則不需要設定這個引數;

  • android:interpolator:設定加速器;

objectAnimator標籤中的屬性如下:

複製程式碼

可以看到與animator中的屬性是差不多的,這裡多了一個

  • android:propertyName="string"表示要做動畫的屬性名字。其它的屬性與animator中的一致,就不再浪費口舌和篇幅了。

set標籤中的屬性如下: set標籤只有一個屬性如下:

  • android:ordering=["together" | "sequentially"],其中together表示set標籤下的動畫同時執行,而sequentially表示set標籤下的動畫逐個執行。

理論終於說完了,掌握了理論之後,就可以來看妹子了。 最後我們以一個用xml實現的組合動畫結束本篇的內容,我們實現的效果是這樣的,先讓這個妹子進入到螢幕的正中央,然後讓她旋轉360度,再然後讓她離開螢幕,離開螢幕的同時伴隨著透明度的變化。先看效果:

這裡寫圖片描述

效果還算比較炫酷吧, 這也算是一個稍微複雜一點的動畫了,與之對應的xml配置內容如下:


<set xmlns:android="http://schemas.android.com/apk/res/android"  
    android:ordering="sequentially" >  

    <objectAnimator  
        android:duration="2000"  
        android:propertyName="translationX"  
        android:valueFrom="-500"  
        android:valueTo="0"  
        android:valueType="floatType" >  
    </objectAnimator>  

    <set android:ordering="sequentially" >  
        <objectAnimator  
            android:duration="3000"  
            android:propertyName="rotation"  
            android:valueFrom="0"  
            android:valueTo="360"  
            android:valueType="floatType" >  
        </objectAnimator>  

        <set android:ordering="together" >  
            <objectAnimator  
                android:duration="2000"  
                android:propertyName="alpha"  
                android:valueFrom="1"  
                android:valueTo="0"  
                android:valueType="floatType" >  
            </objectAnimator>  
            <objectAnimator  
                android:duration="2000"  
                android:propertyName="translationX"  
                android:valueFrom="0"  
                android:valueTo="200"  
                android:valueType="floatType" >  
            </objectAnimator>  
        </set>  
    </set>  

</set>複製程式碼

怎樣將其xml檔案載入到程式中呢?程式碼也很簡單,只需要這樣寫:

Animator animator = AnimatorInflater.loadAnimator(MainActivity.this,
            R.animator.animator_set);
animator.setTarget(ivImage);
animator.start();複製程式碼

可以看到,直接呼叫AnimatorInflater的loadAnimator將xml檔案載入進來,並給其設定目標物件,最後呼叫start方法啟動,就完成了。xml檔案的配置大家可以根據執行效果自己分析分析。 由於篇幅原因以及今天是陰天的原因,這一篇就寫到這裡了,因為我得趕緊去買個避雷針去。

這裡寫圖片描述

如果你想看我裝逼,那就鎖定本臺期待接下來的關於屬性動畫的文章吧。 或者去我的部落格blog.csdn.net/dmk877/arti…

參考文章:developer.android.com/guide/topic…

相關文章