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

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

1、简述

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

2、关键概念

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

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

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

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

官方示意图.png

3、关键类TransitionManager

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

image.png

4、Transition相关内容

系统内置transition.png

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

1.transition的建立

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

建立单一转场效果res/transition/slide_transition.xmlapp

<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文件(多转场跟单一转场都是使用同一方法)ide

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

5、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

6、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

7、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…

相关文章
相关标签/搜索