Android過渡動畫,發現掘金小祕密

語言: CN / TW / HK

不知道大家有沒有發現,Android版的掘金有下面這個小小動畫:點選作者頭像跳轉到作者的詳情頁,而作者頭像會從當前介面通過動畫過渡到詳情頁介面。

知識貧乏限制了我的視野,真心想不到這怎麼實現的?

最近在寫動畫方面文章時候,從網上找到了答案:Activity過渡動畫中的共享元素過渡

本文的初衷,是和大家一起掃盲,如果對你有用,歡迎點贊,讓更多的小夥伴多學點知識。小小的動畫,隱藏著巨大的知識點;怪不得面試造火箭,工作擰螺絲,這是知識儲備,雖然可能一輩子也用不上。

系列好文推薦

Android屬性動畫,看完這篇夠用了吧

Android向量圖動畫:每人送一輛掘金牌小黃車

一、Activity切換過渡動畫

Activity過渡動畫包含進入過渡退出過渡共享元素過渡三個動畫,它們同樣僅支援Android 5.0+版本。

一)、共享元素過渡動畫

共享元素過渡指的兩個Activity共享的檢視如何在兩個Activity之間進行過渡。例如上面的Gif圖,共享檢視就是ImageView

共享元素也分一個元素和多個元素。

定義共享元素過渡效果步驟如下:

  1. 在兩個Activity定義兩個相同型別的View;
  2. 給兩個View設定相同的transitionName屬性;
  3. 通過ActivityOptions.makeSceneTransitionAnimation()函式生成Bundle物件;
  4. startActivity()函式傳遞bundle物件。

栗子講解,清晰易懂:

  1. 分別在activity_first.xmlactivity_second.xml佈局檔案定義ImageView元件,並將transitionName屬性設為activityTransform
<!--activity_first.xml檔案內容-->

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/ivImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_one"
        android:transitionName="activityTransform" />

    <TextView
        android:id="@+id/tvText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:text="我是第一個Activity"
        android:textColor="@color/c_333"
        android:textSize="18sp" />
</LinearLayout>

<!--activity_second.xml檔案內容-->
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/ivImage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:adjustViewBounds="true"
        android:src="@mipmap/ic_one"
        android:transitionName="activityTransform" />

    <TextView
        android:id="@+id/tvText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_above="@id/ivImage"
        android:layout_marginBottom="10dp"
        android:gravity="center"
        android:text="我是第2個Activity"
        android:textColor="@color/c_333"
        android:textSize="18sp" />
</RelativeLayout>
複製程式碼

預覽圖 activityTransform屬性也可以通過程式碼設定。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    ivImage.transitionName="activityTransform"
}
複製程式碼
  1. FirstActivity中給ImageView設定點選事件,跳轉到第二個Activity。
ivImage.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//判斷Android版本
        val bundle =
            ActivityOptions.makeSceneTransitionAnimation(this, ivImage, "activityTransform")
                .toBundle()
        startActivity(Intent(this, SecondActivity::class.java), bundle)
    } else {
        startActivity(Intent(this, SecondActivity::class.java))
    }
}
複製程式碼

程式碼中,先判斷當前Android版本是否大於等於5.0,大於或等於Android 5.0的話就設定共享元素動畫,小於5.0 就正常啟動第二個Activity

通過ActivityOptions.makeSceneTransitionAnimation()建立啟動Activity過渡的一些引數,makeSceneTransitionAnimation()函式第一個引數為Activity物件;第二個引數為共享元素元件,這裡設定為idivImageImageView檢視;第三個引數為transitionName屬性的值,這裡是activityTransform。在呼叫AcivityOptions物件toBundle函式,包裝成Bundle物件。

效果圖:

多個共享元素過渡

多個共享元素過渡也很簡單,只需要呼叫makeSceneTransitionAnimation()函式的另外一個過載函式即可。

  1. 在前面XML佈局的基礎上,給TextView增加transitionName屬性:textTransform
<!--activity_first.xml檔案內容-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/ivImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_one"
        android:transitionName="activityTransform" />

    <TextView
        android:id="@+id/tvText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:gravity="center"
        android:transitionName="textTransform"
        android:text="我是第一個Activity"
        android:textColor="@color/c_333"
        android:textSize="18sp" />
</LinearLayout>

<!--activity_second.xml檔案內容-->
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/ivSecondImage"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:adjustViewBounds="true"
        android:src="@mipmap/ic_one"
        android:transitionName="activityTransform" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:transitionName="textTransform"
        android:layout_above="@id/ivSecondImage"
        android:layout_marginBottom="10dp"
        android:gravity="center"
        android:text="我是第2個Activity"
        android:textColor="@color/c_333"
        android:textSize="18sp" />
</RelativeLayout>
複製程式碼
  1. 構建多個Pair物件,並傳遞給makeSceneTransitionAnimation()函式,啟動Activity
ivImage.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {             
    
    val imagePair=Pair<View,String>(ivImage,"activityTransform")
    val textPair=Pair<View,String>(ivImage,"textTransform")
    
    val bundle =
        ActivityOptions.makeSceneTransitionAnimation(this,
                imagePair,textPair).toBundle()
        
        startActivity(Intent(this, SecondActivity::class.java), bundle) 
    } else {
        startActivity(Intent(this, SecondActivity::class.java))
    }
}
複製程式碼

這裡主要是通過將共享檢視和transitionName屬性的值包裝到Pair物件,其他操作和一個共享元素的操作步驟並無區別。

效果圖:

深坑提醒

有時從RecyclerView介面進入到詳情頁,由於詳情頁載入延遲,可能出現沒有效果。例如ImageView從網路載入圖片,可能A介面到B介面沒效果,B回到A介面有效果。

解決步驟:

  1. setContentView後新增下面程式碼,延遲載入過渡動畫。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    postponeEnterTransition()
}
複製程式碼
  1. 在共享元素檢視載入完畢,或者圖片載入完畢後呼叫下面程式碼,開始載入過渡動畫。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    startPostponedEnterTransition()
}
複製程式碼

例如我是在Glide載入完再呼叫:

 Glide.with(mContext)
                    .asBitmap()
                    .load(value?.avatar ?: "")
                    .listener(object : RequestListener<Bitmap> {
                        override fun onResourceReady(resource: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                            animatorCallback?.invoke()//回撥開始載入過渡動畫
                            return false
                        }

                        override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
                            animatorCallback?.invoke()//回撥開始載入過渡動畫
                            return false
                        }
                    })
                    .apply(RequestOptions.circleCropTransform())
                    .placeholder(R.mipmap.ic_default)
                    .error(R.mipmap.ic_default)
                    .into(authorBinding!!.ivAvatar)
複製程式碼

大家也可以考慮下面程式碼:

shareElement.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    shareElement!!.viewTreeObserver.removeOnPreDrawListener(this)
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        animatorCallback?.invoke()
                    }
                    return true
                }
            })
複製程式碼

二)、進入過渡與退出過渡動畫

與共享元素相反的,就是Activity進入與退出過渡動畫,兩個Activity之間在沒有共享的檢視情況下進行動畫切換。下面先看三種動畫效果圖:爆炸式效果淡入淡出式效果滑動式效果

  • 爆炸式:將檢視移入場景中心或從中移出;
  • 滑動式:將檢視從場景的其中一個邊緣移入或移出;
  • 爆炸式:通過更改檢視的不透明度,在場景中新增檢視或從中移除檢視;

第一個介面採用Fade淡入淡出效果,第二個介面採用了Explode爆炸效果。

前後兩個介面都採用了Slide滑入滑出效果。

利用Android現有的過渡框架,實現起來是很簡單的,步驟如下:

  1. ActivityonCreate()方法中呼叫 setContentView()前設定啟用視窗過渡屬性;
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
複製程式碼
  1. 建立過渡效果物件SlideExplodeFade;
val slide=Slide()
slide.slideEdge=Gravity.START
slide.duration=300//效果時長,一般Activity切換時間很短,不建議設定過長
複製程式碼

如果是Slide效果,可以設定slideEdge屬性來指定滑動方向,預設是Gravity.BOTTOM

  1. 將過渡效果設定給window相關屬性,設定;
//退出當前介面的過渡動畫
window.exitTransition = slide
//進入當前介面的過渡動畫
window.enterTransition = slide
//重新進入介面的過渡動畫
window.reenterTransition = slide
複製程式碼
  1. 呼叫第二個Activity介面,使用過渡效果。
 startActivity(
        Intent(this, SecondActivity::class.java),
        ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
複製程式碼

那麼ActivityOnCreate()方法看起來是這樣子的。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
            window.allowEnterTransitionOverlap=false
            Slide().apply {
                duration = 300
                excludeTarget(android.R.id.statusBarBackground, true)
                excludeTarget(android.R.id.navigationBarBackground, true)
            }.also {
                window.exitTransition = it
                window.enterTransition = it
                window.reenterTransition = it
            }
        }
        
        setContentView(R.layout.activity_first)

        ivContent.setOnClickListener {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                startActivity(
                    Intent(this, SecondActivity::class.java),
                    ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
                )
            }
        }
    }
複製程式碼

上面程式碼中呼叫 了excludeTarget()方法將狀態列和導航欄排除在過渡動畫效果之外。否則會跟著一起起動畫效果,不是很美觀。

正常情況,退出與進入過渡動畫會有一小段交叉的過程,而window.allowEnterTransitionOverlap=false就是禁止交叉,只有退出過渡動畫結束後才會再顯示進入過渡動畫。

如果第二個Activityfinish掉後,回到第一個Activity介面也想有過渡效果,就不要手動呼叫finish(),可以呼叫finishAfterTransition ()方法。

三)、相容Android 5.0前

如果Android 5.0前也想要有切換動畫怎麼辦?

  1. res/anim資料夾下建立想要的效果:
<alpha 
    xmlns:android="http://schemas.android.com/apk/res/android"
        android:interpolator="@interpolator/decelerate_quad"
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:duration="@android:integer/config_longAnimTime" />
複製程式碼
  1. 在啟動Activity後呼叫overridePendingTransition()方法。
val intent = Intent(this, TestActivity2::class.java)
startActivity(intent)
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
複製程式碼

overridePendingTransition()方法第一個引數為下一個介面進入動畫,第二個引數為當前介面退出動畫。

到這裡,Activity的切換使用過渡動畫基本就結束了。有朋友可能會問,只有Activity切換才能應用過渡效果麼?

二、佈局變化過渡動畫

在上一節要理解一個概念:場景。佈局的顯示與隱藏可以理解分別為一個場景,過渡動畫就是解決場景切換帶來的生硬視覺感受。Activity切換過渡動畫指在兩個Activity之間,而佈局變化過渡動畫,是指同個Activity之間View的變化過渡動畫。

一)、手動建立Scene

手動建立場景的話,需要我們自己建立起始和結束場景,利用現有的過渡效果來達到兩個場景的切換。預設情況下,當前介面就是起始場景。

  1. 建立起始場景和結束場景的xml佈局。起始場景和結束場景需要有相同根元素,例如下面程式碼idflConatentFrameLayout佈局。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvText"
        android:text="內容過渡動畫"
        android:gravity="center"
        android:textSize="18sp"
        android:layout_width="match_parent"
        android:layout_height="50dp"/>

    <FrameLayout
        android:id="@+id/flContent"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp">
      <include layout="@layout/layout_first_scene"/>
    </FrameLayout>

</LinearLayout>
複製程式碼

初始檢視,第一個場景,佈局layout_first_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/tvFirst"
        android:textSize="18sp"
        android:layout_marginTop="100dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal|top"
        android:text="感謝大家閱讀文章" />
</LinearLayout>
複製程式碼

第二個場景,佈局layout_second_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:textSize="18sp"
        android:layout_marginTop="100dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal|top"
        android:text="我是新小夢\n歡迎大家點贊支援一下" />
</LinearLayout>
複製程式碼
  1. 建立起始場景和結束場景。
val firstScene = Scene.getSceneForLayout(flContent, R.layout.layout_first_scene, this)
val secondScene = Scene.getSceneForLayout(flContent, R.layout.layout_sencod_scene, this)
複製程式碼

預設情況下,過渡動畫應用整個場景,如果場景某個View不參加,可以通過過渡效果物件removeTarget()方法進行移除。

Slide(Gravity.TOP).removeTarget(tvNoJoin)
複製程式碼
  1. 點選時,進行場景過渡。
tvText.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        if (isFirst) {
            TransitionManager.go(secondScene, Slide(Gravity.TOP))
        }else{
            TransitionManager.go(firstScene, Slide(Gravity.TOP))
        }
        isFirst=!isFirst
    }
}
複製程式碼

TransitionManager.go()第一個引數表示結束場景,第二個引數表示當前場景退出時過渡效果,當前場景就是初始場景。

效果圖:

二)、系統自動建立Scene

這種情況,我們呼叫TransitionManager.beginDelayedTransition(sceneRoot)函式時,系統會自動記錄當前sceneRoot節點下所有要進行動畫的檢視作為起始節點,下一幀中再次記錄sceneRoot子節點下所有 起始場景進行動畫狀態的檢視作為結束場景。這種一般用來改變檢視的屬性,然後進行動畫過渡,如View的寬高。

栗子

定義只有一個正方形的View,通過改變正方形的寬高為原來的2倍,來看看動畫效果。

  1. activity_text.xml佈局檔案,定義idsceneRoot的根節點,也是場景的根節點。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/sceneRoot"
    android:background="@color/colorPrimary">

    <View
        android:id="@+id/vSquare"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="50dp"
        android:background="@color/white" />
</RelativeLayout>
複製程式碼
  1. TestActivityOnCreate方法中呼叫下面程式碼,將正方形的寬高設定200dp。
vSquare.setOnClickListener {
   TransitionManager.beginDelayedTransition(sceneRoot)
   vSquare.layoutParams.apply {
       width = dp2px(200f, this@TestActivity)
       height = dp2px(200f, this@TestActivity)
   }.also {
       vSquare.layoutParams = it
   }
}
複製程式碼

效果圖:

三、過渡動畫效果

上面的動畫效果,都是採用系統內建的,那具體有哪些動畫效果,或支援自定義麼?

過渡效果類都繼承自Transition類,Transition類持有場景切動畫的相關資訊,子類的主要作用是捕獲屬性值(例如起始值和結束值)和如何演奏動畫。從這裡也可以看出,過渡動畫也是屬性動畫的一個擴充套件與應用。

一)、內建過渡動畫

系統支援將任何擴充套件Visibility類的過渡作為進入或退出過渡,內建繼承自Visibility的類有ExplodeSlideFade;支援共享元素過渡的有:

  • changeScroll 為目標檢視滑動新增動畫效果
  • changeBounds 為目標檢視佈局邊界的變化新增動畫效果
  • changeClipBounds 為目標檢視裁剪邊界的變化新增動畫效果
  • changeTransform 為目標檢視縮放和旋轉方面的變化新增動畫效果
  • changeImageTransform 為目標圖片尺寸和縮放方面的變化新增動畫效果

程式碼示例:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    TransitionSet().apply {
        addTransition(ChangeImageTransform())
        addTransition(ChangeBounds())
        addTransition(Fade(Fade.MODE_IN))
    }.also {
       window.sharedElementEnterTransition=it
    }
}
複製程式碼

TransitionSet物件是動畫的合集,可以將多個過渡效果組織起來。

也可以通過XML佈局來實現,在res/transition資料夾建立``:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:transitionOrdering="together">
    <changeImageTransform />
    <changeBounds />
    <fade />
</transitionSet>
複製程式碼

transitionSetfade...等等的一些屬性和 Android向量圖動畫:每人送一輛掘金牌小黃車文章 講到的一些屬性大同小異,這裡不再複述。

程式碼呼叫:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    TransitionInflater.from(this).inflateTransition(R.transition.transition_set).also {
        window.sharedElementEnterTransition=it
    }
}
複製程式碼

效果圖:

當現有的過渡效果不滿足日常需求時,可以通過繼承Transition,定製自己的動畫特效。

二)、自定義過渡動畫

子類繼承Transition類,並重寫其三個方法。

class MyTransition : Transition() {
   override fun captureStartValues(transitionValues: TransitionValues?) {}

   override fun captureEndValues(transitionValues: TransitionValues?) {}

   override fun createAnimator(
       sceneRoot: ViewGroup?,
       startValues: TransitionValues?,
       endValues: TransitionValues?
   ): Animator {
       return super.createAnimator(sceneRoot, startValues, endValues)
   }
   
}
複製程式碼

captureStartValues()captureEndValues()方法是必須實現的,捕獲動畫的起始值和結束值,而createAnimator()方法,是用來建立自定義的動畫。

引數TransitionValues可以理解是用來儲存View的一些屬性值,引數sceneRoot為根檢視。

自定義過渡效果感興趣可以參考:Android自定義Transition動畫

好啦,過渡動畫就講到這裡~

參考文章:

官網文件

酷炫的Activity切換動畫,打造更好的使用者體驗

【碼字不易,點個贊,日後好檢視】

本文使用 mdnice 排版