Android 動畫總結(jié)(8) - Activity 轉(zhuǎn)場動畫

Android 動畫總結(jié)(1) - 概述
Android 動畫總結(jié)(2) - 幀動畫
Android 動畫總結(jié)(3) - 補(bǔ)間動畫
Android 動畫總結(jié)(4) - 插值器
Android 動畫總結(jié)(5) - 屬性動畫
Android 動畫總結(jié)(6) - 估值器
Android 動畫總結(jié)(7) - ViewGroup 子元素間的動畫
Android 動畫總結(jié)(9) - 過渡動畫


對于 Activity,在 startActivity 或 finish 后調(diào)用

overridePendingTransition(R.anim.activity_in, R.anim.activity_out)

對于 Fragment:

supportFragmentManager.beginTransaction().setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)

ActivityOptions

從 Android 5.0 之后,可以用 ActivityOptions 來實現(xiàn),ActivityOptionsCompat 是 support v4 的兼容實現(xiàn),可以支持到 4.1(SDK 16),它有幾個 make 開頭的方法

  1. makeCustomAnimation(Context context, int enterResId, int exitResId)
  2. makeScaleUpAnimation(View source, int startX, int startY, int startWidth, int startHeight)
  3. makeThumbnailScaleUpAnimation(View source, Bitmap thumbnail, int startX, int startY)
  4. makeClipRevealAnimation(View source, int startX, int startY, int width, int height)
  5. makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName)
  6. makeSceneTransitionAnimation(Activity activity, Pair<View, String>... sharedElements)

makeCustomAnimation

custom.onClick {
    val compat = ActivityOptionsCompat.makeCustomAnimation(ctx, R.anim.activity_in, R.anim.activity_out)
    start(it!!, compat)
}

private fun start(view: View, compat: ActivityOptionsCompat) {
    val intent = Intent(ctx, OptionAfterActivity::class.java)
    intent.putExtra("from", (view as Button).text)
    // SDK 16 以下會忽略 compat.toBundle()
    ActivityCompat.startActivity(ctx, intent, compat.toBundle())
}

最普通的,效果和過去的 overridePendingTransition 一樣。

OptionAfterActivity 的布局就只有一個 TextView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".OptionAfterActivity"
    android:background="#5500f2f0">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#000000"
        android:textSize="18sp"
        android:layout_centerInParent="true"/>
</RelativeLayout>

class OptionAfterActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_option_after)
        // 接收前一個方法傳過來的名字顯示出來
        tv.text = intent.extras["from"].toString()
    }

    override fun onBackPressed() {
        ActivityCompat.finishAfterTransition(this)
    }
}

發(fā)現(xiàn) ActivityCompat.finishAfterTransition(this) 并沒什么用。看源碼

public static void finishAfterTransition(Activity activity) {
    if (Build.VERSION.SDK_INT >= 21) {
        activity.finishAfterTransition();
    } else {
        activity.finish();
    }
}

好吧,ActivityCompat.startActivitySDK >= 16 就生效,這退出的必須 >= 21 才行。再繼續(xù)往下走

public void finishAfterTransition() {
    if (!mActivityTransitionState.startExitBackTransition(this)) {
        finish();
    }
}
public boolean startExitBackTransition(final Activity activity) {
    if (mEnteringNames == null || mCalledExitCoordinator != null) {
        return false;
    } else {
        if (!mHasExited) {
            // 能判斷對這里才真正執(zhí)行頁面返回的動畫
        }
        return true;
    }
}

而這個 mEnteringNames 的定義是:

The shared elements that the calling Activity has said that they transferred to this

很可惜,此時的 mEnteringNames 是 null,直接返回 false 調(diào)用 finish 了。下面會講到什么是 shared elements。

makeScaleUpAnimation

參照 Activity 上的某個 View,新 Activity 從指定大小放大到最大顯示。

動畫速度太快了,也沒找到可以控制時間的地方,查了許多資料,包括看源碼注釋,其實還不是很明白這個 View 到底有沒有放大。

使用的一個場景是可能點(diǎn)擊一個小 View,然后第二個頁面某個位置顯示的放大版的,這樣看著好像是點(diǎn)擊放大到另一個頁面似的。

scaleUp.onClick {
    val compat = ActivityOptionsCompat.makeScaleUpAnimation(image, image.width / 2, image.height / 2, 0, 0)
    start(it!!, compat)
}

看下它的 5 個參數(shù):

  1. View source - 參照物
  2. int startX - 相對于 source,新 Activity 開始的位置
  3. int startY - 同 startX,只不過這是 Y 軸方向上的
  4. int startWidth - 第二個 Activity 在做放大動畫前一開始的初始寬度
  5. int startHeight - 這當(dāng)然就是初始高度了

makeThumbnailScaleUpAnimation

和 makeScaleUpAnimation 的區(qū)別是,不再是放大頁面上的一個 View,而是指定一張圖,在轉(zhuǎn)場時,放大這張圖片。不過也許是太快了,根本看不見,也不知理解是否正確。

thumbnailScaleUp.onClick {
    val compat = ActivityOptionsCompat.makeThumbnailScaleUpAnimation(text, BitmapFactory.decodeResource(resources, R.drawable.timg), 0, 0)
    start(it!!, compat)
}

參數(shù):

  • View source - 圖片放大的參照物
  • Bitmap thumbnail - 要放大的圖片
  • int startX - 相對于參照物 source,這張圖片開始放大的 X 軸位置
  • int startY - 圖片開始放大相對于 source 的 Y 軸位置

makeClipRevealAnimation

說是一個點(diǎn)圓形漸變到全部顯示,參數(shù)含義和 makeScaleUpAnimation 的一樣。也是太快,沒看出什么特別的。

clipReveal.onClick {
    val compat = ActivityOptionsCompat.makeClipRevealAnimation(text, text.getWidth() / 2, text.getHeight() / 2, 0, 0)
    start(it!!, compat)
}

makeSceneTransitionAnimation 單個 View

Scene 就是場景,兩個 Activity 中的某些 View 協(xié)同完成過渡動畫。

在兩個 Activity 的布局文件中,要協(xié)同做動畫的 View 要有一個屬性 android:transitionName 并將值設(shè)為一樣的。

第一個 Activity 的 xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">
    ...
    <ImageView
        android:id="@+id/image1"
        android:layout_width="230dp"
        android:layout_height="78dp"
        android:layout_gravity="center_horizontal"
        android:background="@drawable/timg"
        android:transitionName="image_name" />
    ...
</LinearLayout>

要跳轉(zhuǎn)的新 Activity 的 xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    <ImageView
        android:id="@+id/image2"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@drawable/timg"
        android:layout_alignParentBottom="true"
        android:transitionName="image_name" />
</LinearLayout>

兩個要協(xié)同的 ImageView 都有一句 android:transitionName="image_name"。

sceneTransitionSingleView.onClick {
    val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this@OptionActivity, image1, "image_name")
    start(it!!, compat)
}

參數(shù):

  1. Activity activity - 當(dāng)前所在 Activity
  2. View sharedElement - 要協(xié)同過渡的 View,就是共享元素
  3. String sharedElementName - 就是 xml 里定義的 transitionName

上面說到 ActivityCompat.finishAfterTransition(this) 沒生效是在一處判斷 shared elements 為 null 就返回執(zhí)行普通的 finish 了,現(xiàn)在這里有 shared elements 了,發(fā)現(xiàn)返回原來頁面確實也有動畫效果了。


這種協(xié)同過渡用同類型甚至內(nèi)容都差不多的 View 來做看著效果好,但就算讓兩個完全不一樣的 View 做協(xié)同過渡,也是可以的,如第一個 Activity 的一個 Button,點(diǎn)擊就跳轉(zhuǎn)到新 Activity,就讓這個 Button 和新 Activity 里的一個 TextView 做過渡,也是可以,效果還好,就是返回時有個突變。

<Button
    android:id="@+id/sceneTransitionDifferent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="makeSceneTransitionAnimation(不同類型View過渡)"
    android:textAllCaps="false"
    android:transitionName="transition" />
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="#ff0000"
    android:textSize="12sp"
    android:background="#ffffff"
    android:padding="10dp"
    android:layout_margin="10dp"
    android:text="共享元素"
    android:transitionName="transition" />
sceneTransitionDifferent.onClick {
    val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this@OptionActivity, sceneTransitionDifferent, "transition")
    start(it!!, compat)
}

makeSceneTransitionAnimation 多個 View

修改 xml 再給其它 View 也加上 android:transitionName。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">
    ...
    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:background="#669812"
        android:text="我欲乘風(fēng)歸去"
        android:layout_gravity="center_horizontal"
        android:gravity="center"
        android:textSize="14sp"
        android:transitionName="text_name" />
        
    <ImageView
        android:id="@+id/image1"
        android:layout_width="230dp"
        android:layout_height="78dp"
        android:layout_gravity="center_horizontal"
        android:background="@drawable/timg"
        android:transitionName="image_name" />
    ...
</LinearLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="30dp"
        android:background="#669812"
        android:text="我欲乘風(fēng)歸去"
        android:layout_gravity="center_horizontal"
        android:gravity="center"
        android:textSize="26sp"
        android:transitionName="text_name" />
        
    <ImageView
        android:id="@+id/image2"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@drawable/timg"
        android:layout_alignParentBottom="true"
        android:transitionName="image_name" />
</LinearLayout>
sceneTransitionMultiView.onClick {
    val textPair = android.support.v4.util.Pair<View, String>(text, "text_name")
    val imagePair = android.support.v4.util.Pair<View, String>(image1, "image_name")

    val compat = ActivityOptionsCompat.makeSceneTransitionAnimation(this@OptionActivity, textPair, imagePair)
    start(it!!, compat)
}
activity_option1.gif

有共享元素時的動畫效果

使用共享元素時 Activity 的效果

有三種:

  • explode - 爆裂,從場景中間移動視圖進(jìn)入或者退出屏幕
  • slide - 滑動,視圖從場景的一個邊緣進(jìn)入或者退出屏幕
    • android:slideEdge 屬性控制滑動方向,取值可以是 LEFT, TOP, RIGHT, BOTTOM, START, END
  • fade - 淡入淡出,從場景添加或者移除一個視圖時改變他的透明

可以指定 target,只在某個 View 或排除某個 View 上做動畫。

主題中可以配置

  • android:windowEnterTransition - 當(dāng) A start B 時,B 頁面進(jìn)入場景的 transition
  • android:windowExitTransition - 當(dāng) A start B 時,A 頁面退出場景的 transition
  • android:windowReturnTransition - 當(dāng) B 返回 A 時,B 頁面退出場景的 transition
  • android:windowReenterTransition - 當(dāng) B 返回 A 時,A 頁面進(jìn)入場景的 transition

如果不在主題配置,在 Activity 的代碼設(shè)置,如 getWindow().setEnterTransition(new Explode());,那么

  • setEnterTransition - B 中設(shè)置
  • setExitTransition() - A 中設(shè)置
  • setReturnTransition() - B 中設(shè)置
  • setReenterTransition() - A 中設(shè)置

res/transiton 目錄創(chuàng)建兩個文件,可以定義其時間和插值器

transition_slide.xml

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:slideEdge="right" />

transition_set.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together" >
    <explode
        android:duration="2000"
        android:interpolator="@android:interpolator/accelerate_decelerate" />
    <fade
        android:duration="2000" />
</transitionSet>

然后配置主題

<item name="android:windowEnterTransition">@transition/transition_set</item>
<item name="android:windowReturnTransition">@transition/transition_slide</item>

如果不配置主題,也可以在 B 頁面(不是跳轉(zhuǎn)前的 A)代碼設(shè)置

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    val set = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_set)
    val slide = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_slide)
    window.enterTransition = set
    window.returnTransition = slide
}

也可以不用 xml,全部代碼配置

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

    val set = TransitionSet()
    set.ordering = TransitionSet.ORDERING_TOGETHER

    val explode = Explode()
    explode.duration = 2000
    explode.interpolator = AccelerateInterpolator()

    val fade = Fade()
    fade.duration = 2000
    fade.interpolator = AccelerateInterpolator()

    set.addTransition(explode)
    set.addTransition(fade)

    window.enterTransition = set

    val slide = Slide()
    slide.duration = 2000
    slide.slideEdge = Gravity.RIGHT
    window.returnTransition = slide
}

共享元素間的效果

  • changeBounds - 改變目標(biāo)視圖的布局邊界
    • android:resizeClip
  • changeClipBounds - 裁剪目標(biāo)視圖邊界
  • changeTransform - 改變目標(biāo)視圖的縮放比例和旋轉(zhuǎn)角度
    • android:reparent 是否追蹤父容器的變化

    • android:reparentWithOverlay 默認(rèn) true。

      When the parent change doesn't use an overlay, it affects the transforms of the child

  • changeImageTransform - 改變目標(biāo)圖片的大小和縮放比例
  • ChangeScroll

res/transition 目錄下創(chuàng)建 transition_elements_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<changeBounds xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:resizeClip="true" />

創(chuàng)建 transition_elements_return_set.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together">
    <changeTransform xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="2000" />
    <changeClipBounds android:duration="2000"
        android:resizeClip="true" />
</transitionSet>

配置主題

<item name="android:windowSharedElementEnterTransition">@transition/transition_elements_enter</item>
<item name="android:windowSharedElementReturnTransition">@transition/transition_elements_return_set</item>

也可以在 Activity B 中代碼設(shè)置

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {   
    // val set = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_elements_return_set)
    // val enter = TransitionInflater.from(ctx).inflateTransition(R.transition.transition_elements_enter)
    val set = TransitionSet()
    set.ordering = TransitionSet.ORDERING_TOGETHER
    
    val changeTransform = ChangeTransform()
    changeTransform.duration = 2000
    
    val changeClipBounds = ChangeClipBounds()
    changeClipBounds.duration = 2000
    
    set.addTransition(changeTransform)
    set.addTransition(changeClipBounds)
    //
    val enter = ChangeBounds()
    enter.duration = 2000
    enter.resizeClip = true
        
    window.sharedElementEnterTransition = enter
    window.sharedElementReturnTransition = set
}
activity_option2.gif

Overlap

To start an enter transition as soon as possible, use the Window.setAllowEnterTransitionOverlap() method on the called activity. This lets you have more dramatic enter transitions.

更鮮活的動畫效果?沒看出什么來。在 Activity B 中設(shè)置

window.allowEnterTransitionOverlap = true
window.allowReturnTransitionOverlap = true

或者配置主題

<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>

關(guān)于主題

看網(wǎng)上有些文章說必須在主題里設(shè)置 <item name="android:windowContentTransitions">true</item> 或者代碼里在 setContentView() 之前 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);,可我沒設(shè)置什么問題都沒有,都生效了啊。然后去看官方文檔 https://developer.android.com/training/transitions/start-activity.html,又出現(xiàn)一個新的配置 <item name="android:windowActivityTransitions">true</item>

First, enable window content transitions with the android:windowActivityTransitions attribute when you define a style that inherits from the material theme.

這意思是說用 material theme 時才需要設(shè)置這個屬性嗎?而我 Demo 用的主題是 Theme.AppCompat.Light.DarkActionBar。不太清楚到底是不是這個原因。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容