带你走一波Transition Animator转场动画相关事项(一)

4,611 阅读10分钟

本篇文章已授权微信公众号guolin_blog(郭霖)独家发布

动画系列文章
带你走一波Android自定义Animator属性动画相关事项(一)

一、简述

Transition可以简单理解为一个过渡框架方便在开始场景到结束场景(不局限于ActivityFragment等页面跳转过程,页面中的控件的变化过程也是场景)设置转场动画(例如,淡入/淡出视图或更改视图尺寸)的一个API。 在Andorid 4.4.2引入的Transition框架,Andorid 5.0以上的版本跳转过渡则建立在该功能上。

二、关键概念

有两个关键概念:场景scene跟转场transition

  • scene:定义给定应用程序的UI。
  • transition:定义两个场景之间的动态变化。

scene开始时,Transition有两个主要职责:   

  1. 在开始和结束的scene捕捉每个视图的状态。
  2. 创建一个Animator根据视图,将动画的差异从一个场景到另一个。

官方示意图.png

三、关键类TransitionManager

SceneTransition联系起来,提供了几个设置场景跟转场的设置方法。

image.png

四、Transition相关内容

系统内置transition.png

系统有实现了部分的转场动画的类,自己根据需求去处理,我这里就简单演示一下里面的几个类,其它的大家自己去试试

1.transition的创建

1.1. 使用布局的方式:在res下创建transition目录,接着创建.xml文件

创建单一转场效果res/transition/slide_transition.xml

<slide  xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="500"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:slideEdge="top" />

创建 多转场res/transition/mulity_transition

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together">
    <explode
        android:duration="1000"
        android:interpolator="@android:interpolator/accelerate_decelerate" />
    <fade
        android:duration="1000"
        android:fadingMode="fade_in_out"
        android:interpolator="@android:interpolator/accelerate_decelerate" />
    <slide
        android:duration="500"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:slideEdge="top" />
</transitionSet>

载入.xml文件(多转场跟单一转场都是使用同一方法)

val transition =TransitionInflater.from(this).inflateTransition(R.transition.fade_transition)

1.2. 使用代码创建translation的方式

//------------------------------- 创建单一转场效果
val translation =  Slide().apply { 
       duration = 500
       interpolator = AccelerateDecelerateInterpolator()
       slideEdge = Gravity.TOP
}

//------------------------------- 创建多转场效果
val transitionSet = TransitionSet()
                transitionSet.addTransition(Fade())
                transitionSet.addTransition(Slide())
                transitionSet.setOrdering(ORDERING_TOGETHER)

2. 使用&常用API

  • 基本使用
//root_view是本布局中的最底层的布局,自己可以指定 但是要包含将要进行动画的控件
//单转场
TransitionManager.beginDelayedTransition(root_view, translation) 
toggleVisibility(view_text,view_blue, view1_red, view_yellow)

//多转场
TransitionManager.beginDelayedTransition(root_view, transitionSet) //多转场
toggleVisibility(view_text,view_blue, view1_red, view_yellow)
    /**
     * 四个有颜色的方块的隐藏跟显示
     */
private fun toggleVisibility(vararg views: View?) {
  for (view in views) {
   view!!.visibility =
      if (view!!.visibility == View.VISIBLE) View.INVISIBLE else View.VISIBLE
   }
}

效果图:

gifeditor_20191218_165504.gif

这里你可以略清楚转场动画的用意,就是你指定两个场景 比如例子中的开始是View都显示,第二个场景是View都隐藏,设置的transitionSet或者translation就是用于中间变化的过程使用的动画。实际上也是里面使用了属性动画进行处理的。(下面自定义转场动画的时候会说到)

//点击按钮
R.id.btn_change_bounds -> {
  TransitionManager.beginDelayedTransition(root_view, ChangeBounds())
  var lp = view1_red.layoutParams
  if (lp.height == 500) {
    lp.height = 200
    } else {
    lp.height = 500
    }
  view1_red.layoutParams = lp
}
//红框剪切的
R.id.btn_change_clip_bounds -> {
  TransitionManager.beginDelayedTransition(root_view, ChangeClipBounds())
  val r = Rect(20, 20, 100, 100)
  if (r == view1_red.clipBounds) {
    view1_red.clipBounds = null
  } else {
    view1_red.clipBounds = r
  }
}
// 蓝色方块中的字内部滑动
R.id.btn_change_scroll -> {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val t = ChangeScroll()
    TransitionManager.beginDelayedTransition(root_view, t)
  }
  if(view_text.scrollX == -50 && view_text.scrollY == -50){
    view_text.scrollTo(0,0)
  }else{
    view_text.scrollTo(-50,-50)
  }
}

gifeditor_20191220_180102.gif

3. translation.Targets

配置Transition可以给一些特殊目标的View或者去掉目标View指定Transitions.

增加动画目标addTarget(View target)
addTarget(int targetViewId)
addTarget(String targetName) : 与 TransitionManager.setTransitionName方法设定的标识符相对应。
addTarget(Class targetType) : 类的类型 ,比如android.widget.TextView.class
移除动画目标
removeTarget(View target)
removeTarget(int targetId)
removeTarget(String targetName)
removeTarget(Class target)
排除不进行动画的view
excludeTarget(View target, boolean exclude)
excludeTarget(int targetId, boolean exclude)
excludeTarget(Class type, boolean exclude)
excludeTarget(Class type, boolean exclude)
排除某个 ViewGroup 的所有子View
excludeChildren(View target, boolean exclude) excludeChildren(int targetId, boolean exclude)
excludeChildren(Class type, boolean exclude)

4. 自定义 Transition动画

主要三个方法,跟属性定义。

  1. 属性定义:官网提醒我们避免跟其他的属性名同名,建议我们命名规则:package_name:transition_class:property_name

  2. 三个方法:captureStartValues()captureEndValues()createAnimator()

  • captureStartValues(transitionValues: TransitionValues) 开始场景会多次调用,在这里你调用transitionValues.values[你定义的属性名]并将此时属性的值赋值给它
  • captureEndValues(transitionValues: TransitionValues) 结束场景会多次调用,在这里你调用transitionValues.values[你定义的属性名]并将此时属性的值赋值给它
  • createAnimator( sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues? ): Animator? 重点是这个函数,我们在这里根据开始的场景跟结束的场景值,定义对应的属性动画,并通过监听属性动画addUpdateListener的方法,进行对应的属性改变。
  1. 补充说明:captureStartValues()captureEndValues()实际上是用于将此时的改变的属性值,存储到TransitionValues中的hashMap中(定义的属性名为key属性值为对应的value),方便我们在后面createAnimator根据存储的值进行属性动画的创建。
  • 例子:自定义背景颜色属性转场动画
   /**
 * Create by ldr
 * on 2019/12/23 16:02.
 */
class ChangeColorTransition : Transition() {

    companion object {
        /**
         *  根据官网提供的命名规则 package_name:transition_class:property_name,避免跟与其他 TransitionValues 键起冲突
         *  将颜色值存储在TransitionValues对象中的键
         */
        private const val PROPNAME_BACKGROUND = "com.mzs.myapplication:transition_colors:background"
    }

    /**
     * 添加背景Drawable的属性值到目标的TransitionsValues.value映射
     */
    private fun captureValues(transitionValues: TransitionValues?) {
        val view = transitionValues?.view ?: return
        //保存背景的值,供后面使用
        transitionValues.values[PROPNAME_BACKGROUND] = (view.background as ColorDrawable).color
    }
    //关键方法一 :捕获开始的场景值,多次调用
    override fun captureStartValues(transitionValues: TransitionValues) {
        if (transitionValues.view.background is ColorDrawable)
            captureValues(transitionValues)
    }
   //关键方法二 :捕获结束的场景值,多次调用。
   // 将场景中的属性值存储到transitionValues的
    override fun captureEndValues(transitionValues: TransitionValues) {
        if (transitionValues.view.background is ColorDrawable)
            captureValues(transitionValues)
    }

 //关键方法三:根据    override fun createAnimator(
        sceneRoot: ViewGroup?,
        startValues: TransitionValues?,
        endValues: TransitionValues?
    ): Animator? {
        //存储一个方便的开始和结束参考目标。
        val view = endValues!!.view
        //存储对象包含背景属性为开始和结束布局
        var startBackground = startValues!!.values[PROPNAME_BACKGROUND]
        var endBackground = endValues!!.values[PROPNAME_BACKGROUND]
        //如果没有背景等的直接忽略掉
        if (startBackground != endBackground) {
          //定义属性动画。
            var animator = ValueAnimator.ofObject(ArgbEvaluator(), startBackground, endBackground)
        //设置监听更新属性
            animator.addUpdateListener { animation ->
                var value = animation?.animatedValue
                if (null != value) {
                    view.setBackgroundColor(value as Int)
                }
            }
            return animator
        }
        return null
    }
}

代码中使用

var changeColorTransition = ChangeColorTransition()
changeColorTransition.duration = 2000
TransitionManager.beginDelayedTransition(root_view, changeColorTransition)
val backDrawable = view1_red.background as ColorDrawable
if (backDrawable.color == Color.RED) {
  view1_red.setBackgroundColor(Color.BLUE)
} else {
  view1_red.setBackgroundColor(Color.RED)
}

gifeditor_20191224_112303.gif

五、Scene的相关内容

1.Scene的创建

sceneRoot是要进行场景变化的根布局,不用非得是整个布局的根布局,只要是包含了场景变化的根布局可以了。 R.layout.scene_layout0R.layout.scene_layout1中的要进行转场动画的控件id一致

  • 通过 Scene.getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) 方法。
var scene0 = Scene.getSceneForLayout(sceneRoot,R.layout.scene_layout0,this)
var scene1 = Scene.getSceneForLayout(sceneRoot,R.layout.scene_layout1,this)
  • 通过Scene()构造函数

val view =  LayoutInflater.from(this).inflate(R.layout.scene_layout0,sceneRoot,false)
val scene0 = Scene(sceneRoot,view)
val view1 =  LayoutInflater.from(this).inflate(R.layout.scene_layout1,sceneRoot,false)
val scene1 = Scene(sceneRoot,view1)

这里有一点需要注意:LayoutInflater.from(this).inflate(R.layout.scene_layout0,sceneRoot,false),最后一个参数要传false,不然一旦你的view添加到sceneRoot中,你去调用TransitionManager.go()传入参数就会报错 IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

----------------------------scene_layout0的布局----------------------------------
<?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/black_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="18dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="48dp"
        android:src="@drawable/shape_black_circle" />

    <ImageView
        android:id="@+id/yellow_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_marginTop="48dp"
        android:layout_marginEnd="40dp"
        android:layout_marginRight="10dp"
        android:src="@drawable/shape_yellow_circle" />

    <ImageView
        android:id="@+id/red_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_below="@+id/black_circle"
        android:layout_alignParentStart="true"
        android:layout_marginStart="13dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="39dp"
        android:src="@drawable/shape_red_circle" />

    <ImageView
        android:id="@+id/blue_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_marginTop="241dp"
        android:layout_marginEnd="45dp"
        android:layout_marginRight="10dp"
        android:src="@drawable/shape_blue_circle" />
</RelativeLayout>

----------------------------scene_layout1的布局----------------------------------
与scene_layout0一样,只是ImageView的位置更换了一下。

两个场景要进行转场变化的控件id是一致的。

我通过实践发现了一个问题:当多个转场控件放到不同的ViewGroup下面,而不是在同一个ViewGroup的布局下面,产生的动画会有不一致的情况。

上面的所有ImageView都放在RelativeLayout的布局下面,与使用Linearlayout为纵向根布局再加上两个子横向Linearlayout,再将ImageView两两放置到子横向Linearlayout中,你会看到位置变化的转场效果可能不是你所期望的。(这里应该是因为不在同一个ViewGroup下导致的)

2. 使用

//场景1:
val transition = TransitionInflater.from(this).inflateTransition(R.transition.explore_transtion)
TransitionManager.go(scene0,transition)

//场景2:
val transition = TransitionInflater.from(this).inflateTransition(R.transition.explore_transtion)
TransitionManager.go(scene1,transition)

gifeditor_20191225_100316.gif

六、Activity间的转场动画

图解.png

1. 基本主要API

  • window.enterTransition: 进入时候的转场效果
  • window.exitTransition: 退出时候的转场效果
  • window.reenterTransition: 重新进入的转场效果
  • window.returnTransition: 回退的时候的转场效果

对应样式下的

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <item name="android:windowEnterTransition"></item>
  <item name="android:windowExitTransition"></item>
  <item name="android:windowReenterTransition"></item>
  <item name="android:windowReturnTransition"></item>
 </style>

2. Android 支持以下进入和退出过渡:

explore : 将视图移入场景中心或从中移出。
slide : 将视图从场景的其中一个边缘移入或移出。
fade : 通过更改视图的不透明度,在场景中添加视图或从中移除视图。
系统支持将任何扩展 Visibility 类的过渡作为进入或退出过渡。

3. 基本使用

onCreate()中设置转场动画

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setUpWindow()
    }
    private fun setUpWindow() {
       window.let {
            it.exitTransition = TransitionInflater.from(this).inflateTransition(R.transition.fade_transtion)
            it.enterTransition = Explode().apply {
                duration = 500
            }
            it.reenterTransition = Explode().apply {
                duration = 500
            }
            it.returnTransition = Slide().apply {
                duration = 500
            }
        }
        }
    }

跳转的时候,startActivity增加bundle

val intent = Intent(this@MainActivity, SampleTranslateActivity::class.java)
val bundle =  ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle()//Androidx提供的类
//val bundle =  ActivityOptions.makeSceneTransitionAnimation(this).toBundle()//不是Andoridx的时候使用ActivityOptions
startActivity(intent,bundle)

gifeditor_20191226_111126.gif
上面的效果存在一些问题,有些动画重叠在一块了。 我们需要设置一下代码让进入退出的动画按序完成而不重叠到一块的时候,

setWindowAllowEnterTransitionOverlap(false)
setWindowAllowReturnTransitionOverlap(false)

或者在Activity或者Application对应的样式下面增加

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

gifeditor_20191226_112449.gif

七、Activity间的共享转场动画

image.png

1.基本API

对应各方法进入时候的转场效果,跟上面的转场动画的api是相对的

  • window.sharedElementEnterTransition
  • window.sharedElementExitTransition
  • window.sharedElementReenterTransition
  • window.sharedElementReturnTransition 对应样式下的
 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowSharedElementEnterTransition"></item>
        <item name="android:windowSharedElementExitTransition"></item>
        <item name="android:windowSharedElementReenterTransition"></item>
        <item name="android:windowSharedElementReturnTransition"></item>
   </style>

2.基本使用

注意:版本要大于android5.0以上的,才有提供共享元素场景动画,用的时候记得做一下版本兼容

    // Check if we're running on Android 5.0 or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // Apply activity transition
    } else {
        // Swap without transition
    }

2.1. 先在xml样式中开启

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
 <item name="android:windowContentTransitions">true</item>
</style>

或者代码中开启

requestWindowFeature(Window.FEATURE_CONTENT_TRANSITIONS)

2.2. 定义两个布局都要设置android:transitionName 跳转布局一

    <ImageView
        android:id="@+id/image_blue"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:src="@drawable/shape_blue_circle"
        android:transitionName="blue_name"
        />
    <TextView
        android:id="@+id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:transitionName="textName"
        android:text="这个是我等下转场假装变大的数据~~~~"
        />

布局二

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="68dp"
        android:src="@drawable/shape_blue_circle"
        android:transitionName="blue_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.497"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="68dp"
        android:text="TextView"
        android:transitionName="textName"
        android:textSize="23sp"
        android:textColor="@color/black"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

2.3 在两个Activity中分别设置共享元素的转场动画

window.sharedElementEnterTransition = ChangeBounds()
window.sharedElementExitTransition = ChangeBounds()

2.3 跳转开始

val intent = Intent(this@MainActivity, ShareElementActivity2::class.java)
// 构造多个Pair 一个Pair对应一个共享元素 
val pair = Pair(image_blue as View, image_blue.transitionName)
val pair1 = Pair(text1 as View, text1.transitionName)
// 将多个共享元素传入
val options = ActivityOptions.makeSceneTransitionAnimation(
  this@MainActivity,
  pair, pair1
)
startActivity(intent, options.toBundle())

gifeditor_20191226_142635.gif

限制(选自Android官方文档)

  • Android 版本在 4.0(API Level 14)4.4.2(API Level 19) 使用 Android Support Library’s

  • 应用于 SurfaceView 的动画可能无法正确显示。 SurfaceView 实例是从非界面线程更新的,因此这些更新与其他视图的动画可能不同步。

  • 当应用于 TextureView 时,某些特定过渡类型可能无法产生所需的动画效果。

  • 扩展 AdapterView 的类(例如 ListView)会以与过渡框架不兼容的方式管理它们的子视图。如果您尝试为基于 AdapterView 的视图添加动画效果,则设备显示屏可能会挂起。

  • 如果您尝试使用动画调整 TextView 的大小,则文本会在该对象完全调整过大小之前弹出到新位置。为了避免出现此问题,请勿为调整包含文本的视图的大小添加动画效果。

本章的源码:

github.com/lovebluedan…

感谢:

Android官方文档
github.com/lgvalle/Mat…
github.com/codepath/an…