Activity转场动画

在开始之前吐槽下简书markdown竟然不支持生成目录列表,弄半天没弄出来,如果哪位知道,烦请告知。这里就简单的截图一下目录,讲究着看吧....

1. 转场动画

转场动画就是Activity通过元素之间的转换提供不同状态之间的视觉连接。你可以为进入和退出转换以及Activity之间共享元素的转换指定定制动画

1.1 Api21之前如何实现转场动画?

Api21之前我们实现转场动画有两种方式。

1.1.1 使用 overridePendingTransition

进入动画

    startActivity(intent);
    overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);

退出动画

    finish();
    overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);

注意:

1.overridePendingTransition方法必须在startActivity()或者finish()方法的后面。
2.如果参数是0,表示没有动画。

1.1.2 使用 Activity主题Style配置

假设有两个Activity AB

A->B activityOpenEnterAnimation

B->A activityOpenExitAnimation

B退出A从新进入 activityCloseEnterAnimation

A退出 activityCloseExitAnimation

主题配置

    <style name="AppThisTheme" parent="Theme.AppCompat.Light.NoActionBar">
        ...
        <item name="android:windowAnimationStyle">@style/activityAnimation</item>
    </style>

    <style name="activityAnimation" parent="@android:style/Animation.Activity">
        <item name="android:activityOpenEnterAnimation">@anim/slide_right_in</item>
        <item name="android:activityOpenExitAnimation">@anim/slide_left_out</item>
        <item name="android:activityCloseEnterAnimation">@anim/slide_left_in</item>
        <item name="android:activityCloseExitAnimation">@anim/slide_right_out</item>
    </style>

1.2 Api21之后如何实现转场动画?

在说Api21之后实现转场动画之前先来看一张图,来理清跳转Activity之间跳转设置的动画方法。

1.2.1 Activity转换动画原理

image

从图中可以看到Activity之间跳转可以有4个不同动作。
一般如果没有特殊需求,指定两个就可以了,
exitTransitionenterTransition。 设置进入和退出动画时,在进行returnTransition时,如果没有设置就会用renterTransition动画设置的值动作相反,同理在进行reenterTransition时,如果没有设置就会用exitTransition动画设置的值动作相反。

上面四个动作其实是View INVISIBLEVISIBLE 或者 VISIBLEINVISIBLE 转换过程。

exitTransition: A退出钱先获取试图为VISIBLE场景,设置试图为INVISIBLE获取INVISIBLE场景,根据transition差异的不同创建执行动画。

enterTransition:B进入时会把B中试图设置为INVISIBLE获取INVISIBLE 场景,然后将视图设置为VISIBLE,获取VISIBLE时的场景,根据transition差异的不同创建执行动画。

同理returnTransitionrenterTransition 原理一样。

根据上面描述Activity场景转换动画时建立在Visibility基础上,支持Visibility有三种:

explode:爆炸效果(将视图从场景的中心移出或移出)

slide:移动效果(将视图从场景的一个边缘移动或移出,类似于目前项目中常用的activity切换动画,从右边进,从左边出。Slide还支持上面和下面进出)

fade :淡入淡出 。

Api21之后实现转场动画也有两种方式

1.2.2 使用 Activity主题Style配置

 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        ...
        <!--必须制定该属性不然动画不起作用-->
        <item name="android:windowActivityTransitions">true</item>
        <!--activity进入动画-->
         <item name="android:windowEnterTransition">@transition/slide_right</item>
        <!--activity退出动画-->
         <item name="android:windowExitTransition">@transition/slide_left</item>
         <!--是否同时执行,如果同时执行A页面动画还没退出,B页面已经开始动画,感觉不是很和谐-->
         <item name="android:windowAllowReturnTransitionOverlap">false</item>
        <item name="android:windowAllowEnterTransitionOverlap">false</item>
  </style>
 

注意:

  1. 如果使用主题继承自android:Theme.Material系列主题,则无需指定windowActivityTransitions,默认windowActivityTransitions属性为true。
  2. 使用主题指定activity进入和退出动画创建的xml 在 res/transition/ 文件下。
  3. 启动一个activity应使用兼容方式启动
    ActivityOptionsCompat optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this);
                ActivityCompat.startActivity(MainActivity.this,
                        new Intent(MainActivity.this, TransitionSlideActivity.class), optionsCompat.toBundle());

启动一个Activity使用Api21之前的方法是没有任何效果转换动画效果的。

4.退出一个Activity使用兼容方式退出

  ActivityCompat.finishAfterTransition(TransitionSlideActivity.this);

/res/transition/slide_left 文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="left">
    <targets>
        <target
            android:excludeId="@android:id/statusBarBackground"
            tools:targetApi="lollipop" />
    </targets>

</slide>

/res/transition/slide_right 文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right">
    <targets>
        <target
            android:excludeId="@android:id/statusBarBackground"
            tools:targetApi="lollipop" />
    </targets>
</slide>

duration:表示动画时长

slideEdge:表示从哪边进入或退出取值有left|top|right|bottom|

targets:标记作用,例如上面例子中标记排除系统状态栏,其他View都应用于转场动画中。除了 排除 某个View,还有targetId 只针对某个View,其他View都不作用于转场动画。同理explodefade也支持排除和针对某个View。

slide|explode|fade 之间还可以两两组合,比如退出的动画使用从左边退出和淡出。

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:duration="@android:integer/config_longAnimTime"
    android:transitionOrdering="together">
    <fade android:fadingMode="fade_in_out" />
    
    <slide android:slideEdge="right" />
    
    <targets>
        <target
            android:excludeId="@android:id/statusBarBackground"
            tools:targetApi="lollipop" />
    </targets>
</transitionSet>

效果图如下:

image

<center> 右进左出并且带有淡出淡入效果</center >

1.2.3 代码设置转场动画

Activity A:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setupWindowAnimations();
        }
        findViewById(R.id.btn_transition_slide_animation).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               ActivityOptionsCompat optionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this);
                ActivityCompat.startActivity(MainActivity.this,
                        new Intent(MainActivity.this, TransitionSlideActivity.class), optionsCompat.toBundle());
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void setupWindowAnimations() {
        TransitionSet transitionSet = new TransitionSet();
        transitionSet.setDuration(300);
        //一起动画
        transitionSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
        Slide slideTransition = new Slide();
        slideTransition.setSlideEdge(Gravity.LEFT);
        transitionSet.addTransition(slideTransition);
        Fade fadeTransition = new Fade();
        transitionSet.addTransition(fadeTransition);
        //排除状态栏
        transitionSet.excludeTarget(android.R.id.statusBarBackground, true);
        //是否同时执行
        getWindow().setAllowEnterTransitionOverlap(false);
        getWindow().setAllowReturnTransitionOverlap(false);
        //退出这个界面
        getWindow().setExitTransition(slideTransition);
    }

Activity B:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_slide_transition);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setupWindowAnimations();
        }
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
              ActivityCompat.finishAfterTransition(TransitionSlideActivity.this);
            }
        });
    }


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void setupWindowAnimations() {
        TransitionSet transitionSet = new TransitionSet();
        transitionSet.setDuration(300);
        //一起动画
        transitionSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
        Slide slideTransition = new Slide();
        slideTransition.setSlideEdge(Gravity.RIGHT);
        transitionSet.addTransition(slideTransition);
        Fade fadeTransition = new Fade();
        transitionSet.addTransition(fadeTransition);
        //排除状态栏
        transitionSet.excludeTarget(android.R.id.statusBarBackground, true);
        //是否同时执行
        getWindow().setAllowEnterTransitionOverlap(false);
        getWindow().setAllowReturnTransitionOverlap(false);
        //进入
        getWindow().setEnterTransition(slideTransition);
    }

具体explode效果代码和效果图就不贴了可以自己去尝试看看。

2. 共享元素动画

在说Activity之间共享动画之前还是来一张官方图

image

2.1 描述

共享动画是分析两个界面共享view的尺寸,位置,样式的不同创建动画化的。从上图看出Activity1到Activity2,Android小机器人是一个被共享的元素。由于两个页面共享元素尺寸位置不一样,所以实现效果就是Activity1小机器人被放大然后显示在Activity2上,当然还可以有Fade效果。关于尺寸改变动画可以使用ChangeBounds,另外还有
ChangeTransform,ChangeClipBounds以及ChangeImageTransform

关于四种ChangeXXX解释如下:

  1. ChangeBounds:检测view的位置边界创建移动和缩放动画
  2. ChangeTransform:检测view的scale和rotation创建缩放和旋转动画
  3. ChangeClipBounds:检测view的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域(setClipBound(Rect rect) 中的rect)。如果没有设置则没有动画效果
  4. ChangeImageTransform:检测ImageView(这里是专指ImageView)的尺寸,位置以及ScaleType,并创建相应动画。

说那么多不如来张上面效果图的动图:

image

2.2 实现共享动画

一般情况下两个Activity ShareElementActivityShareElement1Activity ,假如ShareElementActivity->ShareElement1Activity,配置ShareElement1Activity 中 进入的共享动画,ShareElementActivity中配置退出转场动画即可。

假设两个Activity:ShareElementActivityShareElement1Activity

ShareElementActivity 示例代码

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_share_element);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setupWindowAnimations();
        }
        final ImageView shareElement = findViewById(R.id.iv_share_element);
        shareElement.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                  //共享shareElement这个View
                ActivityOptionsCompat activityOptionsCompat = ActivityOptionsCompat.makeSceneTransitionAnimation(ShareElementActivity.this, shareElement,
                        "shareElement");
                ActivityCompat.startActivity(ShareElementActivity.this,
                        new Intent(ShareElementActivity.this, ShareElement1Activity.class), activityOptionsCompat.toBundle());
            }
        });

        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               ActivityCompat.finishAfterTransition(ShareElementActivity.this);
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void setupWindowAnimations() {
        //爆炸效果进入进出
        Explode explodeTransition = new Explode();
        explodeTransition.setDuration(300);
        //排除状态栏
        explodeTransition.excludeTarget(android.R.id.statusBarBackground, true);
        //是否同时执行
        getWindow().setAllowEnterTransitionOverlap(false);
        getWindow().setAllowReturnTransitionOverlap(false);
        //进入
        getWindow().setEnterTransition(explodeTransition);
    }

上述代码是从ShareElementActivity到ShareElement1Activity 其中共享的View是
shareElement,transitionName是shareElement,这个transitionName是开启共享动画的重要因素,通过,transitionName 指定的值来匹配下个页面共享的View。

ShareElement1Activity 示例代码

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_share_element1);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setupWindowAnimations();
        }

        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
               ActivityCompat.finishAfterTransition(ShareElement1Activity.this);
            }
        });
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void setupWindowAnimations() {
        ChangeBounds changeBounds = new ChangeBounds();
        changeBounds.setDuration(300);
        //排除状态栏
        changeBounds.excludeTarget(android.R.id.statusBarBackground, true);
        //是否同时执行
        getWindow().setAllowEnterTransitionOverlap(false);
        getWindow().setAllowReturnTransitionOverlap(false);
        //进入
        getWindow().setEnterTransition(changeBounds);
    }

    @Override
    public void onBackPressed() {
       ActivityCompat.finishAfterTransition(ShareElement1Activity.this);
    }

可以看到几乎都类似,只不过两个Activity中进入退出动画不一样,需要注意的地方就是返回该页面不能用finish,而是用finishAfterTransition,不然动画不起作用,还有在ShareElement1Activity的布局文件中必须制定共享View的transitionName属性。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".share.ShareElement1Activity">
    ...

    <ImageView
        android:id="@+id/iv_share_element1"
        ...
        android:transitionName="shareElement"
        tools:targetApi="lollipop" />
        ...
</RelativeLayout>

如果实现多个共享View动画使用以下伪代码获取ActivityOptions

ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this,
        Pair.create(view1, "transitionName1"),
        Pair.create(view2, "transitionName2"));

当然Activity设置主题属性也可以实现共享动画只需在该Activity主题中配置

<item name="android:windowSharedElementEnterTransition">..</item>
<item name="android:windowSharedElementExitTransition">...</item>

3. 实战

image

该gif图来源于UI志

可以看到这张gif图item跳转用到共享元素动画,在第二个详情页面共享动画进入后开启页面内其他元素透明+放大 动画。关闭详情页面依次反向执行开启的流程:详情页面描述和标题还有图片左上角的关闭按钮依次执行透明+缩放动画,等这些操作执行完毕,执行finishAfterTransition,共享元素自会回到之前列表中开启的位置。

最后看一下实现的效果图

实现代码就不贴了,点我查看代码

推荐阅读更多精彩内容