Android 动画总结(8) - Activity 转场动画

Android 动画总结(1) - 概述
Android 动画总结(2) - 帧动画
Android 动画总结(3) - 补间动画
Android 动画总结(4) - 插值器
Android 动画总结(5) - 属性动画
Android 动画总结(6) - 估值器
Android 动画总结(7) - ViewGroup 子元素间的动画
Android 动画总结(9) - 过渡动画


对于 Activity,在 startActivity 或 finish 后调用

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 来实现,ActivityOptionsCompat 是 support v4 的兼容实现,可以支持到 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)
    }
}

发现 ActivityCompat.finishAfterTransition(this) 并没什么用。看源码

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

好吧,ActivityCompat.startActivitySDK >= 16 就生效,这退出的必须 >= 21 才行。再继续往下走

public void finishAfterTransition() {
    if (!mActivityTransitionState.startExitBackTransition(this)) {
        finish();
    }
}
public boolean startExitBackTransition(final Activity activity) {
    if (mEnteringNames == null || mCalledExitCoordinator != null) {
        return false;
    } else {
        if (!mHasExited) {
            // 能判断对这里才真正执行页面返回的动画
        }
        return true;
    }
}

而这个 mEnteringNames 的定义是:

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

很可惜,此时的 mEnteringNames 是 null,直接返回 false 调用 finish 了。下面会讲到什么是 shared elements

makeScaleUpAnimation

参照 Activity 上的某个 View,新 Activity 从指定大小放大到最大显示。

动画速度太快了,也没找到可以控制时间的地方,查了许多资料,包括看源码注释,其实还不是很明白这个 View 到底有没有放大。

使用的一个场景是可能点击一个小 View,然后第二个页面某个位置显示的放大版的,这样看着好像是点击放大到另一个页面似的。

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

看下它的 5 个参数:

  1. View source - 参照物
  2. int startX - 相对于 source,新 Activity 开始的位置
  3. int startY - 同 startX,只不过这是 Y 轴方向上的
  4. int startWidth - 第二个 Activity 在做放大动画前一开始的初始宽度
  5. int startHeight - 这当然就是初始高度了

makeThumbnailScaleUpAnimation

和 makeScaleUpAnimation 的区别是,不再是放大页面上的一个 View,而是指定一张图,在转场时,放大这张图片。不过也许是太快了,根本看不见,也不知理解是否正确。

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

参数:

  • View source - 图片放大的参照物
  • Bitmap thumbnail - 要放大的图片
  • int startX - 相对于参照物 source,这张图片开始放大的 X 轴位置
  • int startY - 图片开始放大相对于 source 的 Y 轴位置

makeClipRevealAnimation

说是一个点圆形渐变到全部显示,参数含义和 makeScaleUpAnimation 的一样。也是太快,没看出什么特别的。

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

makeSceneTransitionAnimation 单个 View

Scene 就是场景,两个 Activity 中的某些 View 协同完成过渡动画。

在两个 Activity 的布局文件中,要协同做动画的 View 要有一个属性 android:transitionName 并将值设为一样的。

第一个 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>

要跳转的新 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>

两个要协同的 ImageView 都有一句 android:transitionName="image_name"

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

参数:

  1. Activity activity - 当前所在 Activity
  2. View sharedElement - 要协同过渡的 View,就是共享元素
  3. String sharedElementName - 就是 xml 里定义的 transitionName

上面说到 ActivityCompat.finishAfterTransition(this) 没生效是在一处判断 shared elements 为 null 就返回执行普通的 finish 了,现在这里有 shared elements 了,发现返回原来页面确实也有动画效果了。


这种协同过渡用同类型甚至内容都差不多的 View 来做看着效果好,但就算让两个完全不一样的 View 做协同过渡,也是可以的,如第一个 Activity 的一个 Button,点击就跳转到新 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="我欲乘风归去"
        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="我欲乘风归去"
        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 - 爆裂,从场景中间移动视图进入或者退出屏幕
  • slide - 滑动,视图从场景的一个边缘进入或者退出屏幕
    • android:slideEdge 属性控制滑动方向,取值可以是 LEFT, TOP, RIGHT, BOTTOM, START, END
  • fade - 淡入淡出,从场景添加或者移除一个视图时改变他的透明

可以指定 target,只在某个 View 或排除某个 View 上做动画。

主题中可以配置

  • android:windowEnterTransition - 当 A start B 时,B 页面进入场景的 transition
  • android:windowExitTransition - 当 A start B 时,A 页面退出场景的 transition
  • android:windowReturnTransition - 当 B 返回 A 时,B 页面退出场景的 transition
  • android:windowReenterTransition - 当 B 返回 A 时,A 页面进入场景的 transition

如果不在主题配置,在 Activity 的代码设置,如 getWindow().setEnterTransition(new Explode());,那么

  • setEnterTransition - B 中设置
  • setExitTransition() - A 中设置
  • setReturnTransition() - B 中设置
  • setReenterTransition() - A 中设置

res/transiton 目录创建两个文件,可以定义其时间和插值器

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 页面(不是跳转前的 A)代码设置

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 - 改变目标视图的布局边界
    • android:resizeClip
  • changeClipBounds - 裁剪目标视图边界
  • changeTransform - 改变目标视图的缩放比例和旋转角度
    • android:reparent 是否追踪父容器的变化

    • android:reparentWithOverlay 默认 true。

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

  • changeImageTransform - 改变目标图片的大小和缩放比例
  • ChangeScroll

res/transition 目录下创建 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" />

创建 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 中代码设置

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 中设置

window.allowEnterTransitionOverlap = true
window.allowReturnTransitionOverlap = true

或者配置主题

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

关于主题

看网上有些文章说必须在主题里设置 <item name="android:windowContentTransitions">true</item> 或者代码里在 setContentView() 之前 getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);,可我没设置什么问题都没有,都生效了啊。然后去看官方文档 https://developer.android.com/training/transitions/start-activity.html,又出现一个新的配置 <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 时才需要设置这个属性吗?而我 Demo 用的主题是 Theme.AppCompat.Light.DarkActionBar。不太清楚到底是不是这个原因。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容