Android Transition Framework(过渡动画框架)

先说下什么是 Transition(过渡动画). Lollipop(5.0) 中 Activity 和 Fragment 的过渡动画是基于 Android 一个叫作 Transition 的新特性实现的。 初次引入这个特性是在 KitKat(4.4) 中,Transition 框架提供了一个方便的 API 来构建应用中不同 UI 状态切换时的动画。 这个框架始终围绕两个关键概念:场景和过渡。 场景 描述应用中 UI 的状态(这个定义太抽象了,下面会具体解释),过渡 就是确定两个场景转换之间的过渡动画。

具体作用:

  • 可以在界面(Activity & Fragment)之间跳转的时候添加动画
  • 动画共享元素之间的转换活动
  • 界面中布局元素的过渡动画。

1. Transitions between Activity & Fragment(Content Transition)

在 Android 5.0 中, 切换 Activitys 或者 Fragments 时可以使用 Transitions 来构建精致的转场动画。虽然在之前的版本中已经引入 Activity 和 Fragment 的切换动画(通过 Activity#overridePendingTransition() 和 FragmentTransaction#setCustomAnimation() 方法实现),但是动画的对象只能是Activity/Fragment整体。而新的 API 将这个特性延伸,可以协调 Activity/Fragment 中每一个 view ,为其设置单独的的进入和退出 transition,轻松搞定流畅的屏幕切换动作。

android.transition预定义了三种过渡动画:Explode,Slide和Fade。

Explode Slide Fade
从中心移入或移出 从边缘移入或移出 调整透明度产生渐变

不同的场景下,我们可以为同一个界面设置不同效果的过渡动画,这里解释下场景的意思:

假设 A 和 B 是两个 Activity,通过 A 来启动 B。 A 叫做 "调用Activity"(调用 startActivity() 的那个) B 就是 "被调用Activity"

根据上述情景,可以为activity划分出4种场景的动画:

  • Activity A 的 退出动画(ExitTransition ),即 A 启动 B 时 A 中 View 的动画
  • Activity B 的 进入动画(EnterTransition ),即 A 启动 B 时 B 中 View 的动画
  • Activity B 的 返回动画(ReturnTransition),即 B 返回 A 时 B 中 View 的动画
  • Activity A 的 重入动画(ReenterTransition), 即 B 返回 A 时 A 中 View 的动画

实现方法

接下来我们实际演示下为Activity添加过场动画的实现步骤,可通过xml和代码两种方式实现,以添加Fade动画为例:

  1. 如果是xml方式实现,首先在/res下创建transition文件夹。
    res/transition/slide_from_right
<?xml version = 1.0 encoding = "utf-8"?>
<transitionSet xmls:android = "http://schemas.android.com/apk/res/android">

<slide duration = "500"
       slideEage = "left"/>

</transitionSet>

然后在values/styles.xml中创建带动画的主题,再加到Manifest.xml中就可以了:

<style name="TransitionAppTheme" parent="AppTheme">
        <item name="android:windowContentTransitions">true</item>
        <item name="android:windowEnterTransition">@transition/slide_from_right</item>
        <item name="android:windowExitTransition">@transition/slide_from_right</item>
        <!--避免跳转的Activity动画重叠,则下面两个属性设为false-->
        <item name="android:windowAllowEnterTransitionOverlap">false</item>
        <item name="android:windowAllowReturnTransitionOverlap">false</item>
    </style>
  1. 如果直接用代码实现,可以如下实现
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ... ...
    
    Slide slideTracition = newSlide();
    slideTracition.setSlideEdge(Gravity.LEFT);
    slideTracition.setDuration(getResources().getInteger(R.integer.anim_duration_long));

    //也可以直接取res中的transition资源
    //Transition slideTracition = TransitionInflater.from(this).inflateTransition(R.transition.slide_from_left);
    getWindow().setEnterTransition(slideTracition);
    getWindow().setExitTransition(slideTracition);
    
     ... ...
 }

最后,启动动画

//跳转
Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle()
startActivity(i, bundle);
//返回
finshAfterTransition();

上面我们只添加了EnterTransition和ExitTransition,如果未设置ReturnTransition和ReenterTransition的话,后两者分别为前两者的反向动画。
EnterTransition < - > ReturnTransition
ExitTransition < - > ReenterTransition

2.Share elements between Activity(元素共享)

共享元素过渡动画的背后是通过过渡动画将两个不同布局中的不同view关联起来。Transition框架知道用适当的动画向用户展示从一个view向另外 一个view过度。请记住:共享元素过渡的过程中,view并没有真正从一个布局跑到另外一个布局,整个过程基本都是在后一个布局中完成的。


image

元素共享的实现

  1. 允许过渡动画
    需要在/res/style.xml添加
<item name="android:windowContentTransitions">true</item>
  1. 在对应的xml文件指定TransitionName属性
    例如,我们指定activityA中的ImageView和activityB中的TextView为共享元素:
<ImageView
        android:id="@+id/square_blue"
        style="@style/MaterialAnimations.Icon.Big"
        android:src="@drawable/circle_24dp"
        android:transitionName="@string/square_blue_name" />
<TextView
       android:id="@+id/title"   
       style="@style/MaterialAnimations.TextAppearance.Title.Invers"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_vertical|start"
       android:text="@{sharedSample.name}"
       android:transitionName="@string/sample_blue_title" />

注意:这里必须为共享的俩个元素指定同一个TransitionName,不然不会出现共享效果

  1. 启动Activity
Intent intent = new Intent(activity,target);
ActivityOptionCompat option = ActiviyoptionCampat.makeSceneTransitionAnimation(activity,  
new Pair<View, String>(viewHolder.binding.sampleIcon, activity.getString(R.string.square_blue_name)),
new Pair<View, String>(viewHolder.binding.sampleName, activity.getString(R.string.sample_blue_title)));

startActivity(intent,option.toBundle());

效果如下:



 

共享元素动画的效果设置

我们可以发现对于转场动画(Content transitions) 是根据每个过渡视图的可见性变化来调节的,而共享元素 transition 是根据每个共享元素视图的位置,大小和外观的变化来调节的。与转场动画类似,从 API 21 开始,框架提供了 几个自定义共享元素场景切换动画的 Transition 实现。

  • ChangeBounds - 捕获共享元素布局边界根据不同构造动画。 ChangeBounds 在共享元素 Transition 中经常使用,大多数共享元素在两个 Activity/Fragment 间会有大小 或/和 位置不同。
  • ChangeTransform - 捕获共享元素缩放和角度, 根据不同构建动画。
  • ChangeClipBounds - 捕获共享元素的 clip bounds (剪辑边界) ,根据不同构建动画。
  • ChangeImageTransform - 捕获共享元素 ImageView 的 变换矩阵( transform matrices) ,根据不同构建动画。结合 ChangeBounds, 可以让 ImageView 无缝的改变大小,形状和 ImageView.ScaleType 。

设置代码:

Slide slide = new Slide();
slide.setDuration(500);

ChangeBounds changeBounds = new ChangeBounds();
changeBounds.setDuration(500);

getWindow().setEnterTransition(slide);
getWindow().setSharedElementEnterTransition(changeBounds);

前面的代码我们并没有设置共享元素动画,因为如果我们的Theme使用的是 Material主题,当设置共享元素后会默认添加动画效果。

过渡动画以及共享元素在Fragment之间的实现

fragment使用过渡动画和共享元素,与Activity大同小异,也是直接添加Transition对象之后启动,主要是启动方法有所不同

private void addNextFragment(Sample sample, ImageView blue, boolean b) {
        SharedElementFragment2 elementFragment2 = SharedElementFragment2.newInstance(sample);
        Slide slide = new Slide();
        slide.setDuration(getResources().getInteger(R.integer.anim_duration_medium));
        slide.setSlideEdge(Gravity.RIGHT);
        
        ChangeBounds changeBounds = new ChangeBounds();
        changeBounds.setDuration(getResources().getInteger(R.integer.anim_duration_medium));
        
        elementFragment2.setEnterTransition(slide);
        elementFragment2.setAllowEnterTransitionOverlap(b);
        elementFragment2.setAllowReturnTransitionOverlap(b);
        elementFragment2.setSharedElementEnterTransition(changeBounds);

        getFragmentManager().beginTransaction().replace(R.id.sample2_content, elementFragment2).addToBackStack(null).addSharedElement(blue, getString(R.string.square_blue_name)).commit();
    }

效果如下:


3.TransitionManager 控制动画

TransitionManager是个很好用的工具,使用TransitionManager,我们给view添加一些简单属性动画的时候只需要得到这个view的根布局,然后设置下view动画之后的状态就可以了,TransitionManager会自动为每个view添加预设置好的属性动画,我们甚至可以用它对一个布局内的一组多个view一块儿添加动画,从而实现较为复杂的联动效果。事实上UI场景切换的效果就是通过它来实现的。

public class ExampleActivity extends Activity implements View.OnClickListener {
    private ViewGroup mRootView;
    private View mRedBox, mGreenBox, mBlueBox, mBlackBox;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //mRootView是要添加动画view的根布局
        mRootView = (ViewGroup) findViewById(R.id.layout_root_view);
        mRootView.setOnClickListener(this);

        mRedBox = findViewById(R.id.red_box);
        mGreenBox = findViewById(R.id.green_box);
        mBlueBox = findViewById(R.id.blue_box);
        mBlackBox = findViewById(R.id.black_box);
    }

    @Override
    public void onClick(View v) {
        TransitionManager.beginDelayedTransition(mRootView, new Fade());
        toggleVisibility(mRedBox, mGreenBox, mBlueBox, mBlackBox);
    }

    private static void toggleVisibility(View... views) {
        for (View view : views) {
            boolean isVisible = view.getVisibility() == View.VISIBLE;
            view.setVisibility(isVisible ? View.INVISIBLE : View.VISIBLE);
        }
    }
}

上面的例子我们为一组四个色块添加了Fade效果的动画,同样可以设置成Slide或者Explode,效果如下:


对于一些更为复杂的联动动画,TransitionManager还为我们提供了一种类似于切换布局就可以完成动画的方案,极大的简化了我们实现复杂动画

实现步骤如下:

  1. 定义需要切换 layout xml页面;
  2. 调用 Scene.getSceneForLayout() 保存每个Layout;
  3. 调用 TransitionManager.go(scene1, new ChangeBounds()) 切换。
    代码示例:
private void setupLayout() {
    scene0 = Scene.getSceneForLayout(binding.sceneRoot, R.layout.activity_animations_scene0, this);
    scene1 = Scene.getSceneForLayout(binding.sceneRoot, R.layout.activity_animations_scene1, this);
    scene2 = Scene.getSceneForLayout(binding.sceneRoot, R.layout.activity_animations_scene2, this);
    scene3 = Scene.getSceneForLayout(binding.sceneRoot, R.layout.activity_animations_scene3, this);
    scene4 = Scene.getSceneForLayout(binding.sceneRoot, R.layout.activity_animations_scene4, this);
    binding.sample3Button1.setOnClickListener(this);
    binding.sample3Button2.setOnClickListener(this);
    binding.sample3Button3.setOnClickListener(this);
    binding.sample3Button4.setOnClickListener(this);
}

@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.sample3_button1:
            TransitionManager.go(scene1, new ChangeBounds());
            break;
        case R.id.sample3_button2:
            TransitionManager.go(scene2, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds));
            break;
        case R.id.sample3_button3:
            TransitionManager.go(scene3,TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential));
            break;
        case R.id.sample3_button4:
            TransitionManager.go(scene3,TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential_with_interpolators));
            break;
    }
}

4.CircularReveal 显示或隐藏 的效果

ViewAnimationUtils.createCircularReveal()
当您显示或隐藏一组 UI 元素时,Circular Reveal 可为用户提供视觉连续性



参考说明:

Animator createCircularReveal (View view, // 将要变化的 View
            int centerX,                  // 动画圆的中心的x坐标
            int centerY,                  // 动画圆的中心的y坐标
            float startRadius,            // 动画圆的起始半径
            float endRadius               // 动画圆的结束半径
)

显示View:

private void animShow() {
    View myView = findViewById(R.id.my_view);
    // 从 View 的中心开始
    int cx = (myView.getLeft() + myView.getRight()) / 2;
    int cy = (myView.getTop() + myView.getBottom()) / 2;
    int finalRadius = Math.max(myView.getWidth(), myView.getHeight());

    //为此视图创建动画设计(起始半径为零)
    Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);
    // 使视图可见并启动动画
    myView.setVisibility(View.VISIBLE);
    anim.start();
}

隐藏View:

private void animHide() {
    final View myView = findViewById(R.id.my_view);
    int cx = (myView.getLeft() + myView.getRight()) / 2;
    int cy = (myView.getTop() + myView.getBottom()) / 2;

    int initialRadius = myView.getWidth();

    // 半径 从 viewWidth -> 0
    Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0);

    anim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            myView.setVisibility(View.INVISIBLE);
        }
    });
    anim.start();
}

以上效果都只支持 API 23 以上,所以在我们常用的 APP 中都还不常见,但是效果真的很不错,很值得研究下。

参考及拓展阅读:
在 Activity 和 Fragment 中使用 Transition (part 1)
深入理解Content Transition (part 2)
深入理解 Shared Element Transition (part 3a)
延迟共享元素的过渡动画 (part 3b)
Android共享元素转场动画兼容实践
【Transition】Android炫酷的Activity切换效果,共享元素

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

推荐阅读更多精彩内容