动画系列文章
带你走一波Android自定义Animator属性动画相关事项(一)
一、简述
Transition
可以简单理解为一个过渡框架方便在开始场景到结束场景(不局限于Activity
跟Fragment
等页面跳转过程,页面中的控件的变化过程也是场景)设置转场动画(例如,淡入/淡出视图或更改视图尺寸)的一个API。
在Andorid 4.4.2
引入的Transition
框架,Andorid 5.0
以上的版本跳转过渡则建立在该功能上。
二、关键概念
有两个关键概念:场景scene
跟转场transition
。
-
scene
:定义给定应用程序的UI。 -
transition
:定义两个场景之间的动态变化。
当
scene
开始时,Transition
有两个主要职责:
- 在开始和结束的
scene
捕捉每个视图的状态。- 创建一个
Animator
根据视图,将动画的差异从一个场景到另一个。
三、关键类TransitionManager
将
Scene
和Transition
联系起来,提供了几个设置场景跟转场的设置方法。
四、Transition相关内容
系统有实现了部分的转场动画的类,自己根据需求去处理,我这里就简单演示一下里面的几个类,其它的大家自己去试试
1.transition
的创建
1.1. 使用布局的方式:在res
下创建transition
目录,接着创建.xml
文件
创建单一转场效果res/transition/slide_transition.xml
创建 多转场res/transition/mulity_transition
载入.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
}
}
效果图:
这里你可以略清楚转场动画的用意,就是你指定两个场景 比如例子中的开始是
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)
}
}
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动画
主要三个方法,跟属性定义。
属性定义:官网提醒我们避免跟其他的属性名同名,建议我们命名规则:
package_name:transition_class:property_name
三个方法:
captureStartValues()
、captureEndValues()
、createAnimator()
captureStartValues(transitionValues: TransitionValues)
开始场景会多次调用,在这里你调用transitionValues.values[你定义的属性名]
并将此时属性的值赋值给它captureEndValues(transitionValues: TransitionValues)
结束场景会多次调用,在这里你调用transitionValues.values[你定义的属性名]
并将此时属性的值赋值给它createAnimator( sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues? ): Animator?
重点是这个函数,我们在这里根据开始的场景跟结束的场景值,定义对应的属性动画,并通过监听属性动画addUpdateListener
的方法,进行对应的属性改变。
- 补充说明:
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)
}
五、Scene的相关内容
1.Scene
的创建
sceneRoot
是要进行场景变化的根布局,不用非得是整个布局的根布局,只要是包含了场景变化的根布局可以了。
R.layout.scene_layout0
与R.layout.scene_layout1
中的要进行转场动画的控件id
一致
1.1.通过 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)
1.2 通过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的布局----------------------------------
----------------------------scene_layout1的布局----------------------------------
与scene_layout0一样,只是ImageView的位置更换了一下。
两个场景要进行转场变化的控件
id
是一致的。我通过实践发现了一个问题:当多个转场控件放到不同的
ViewGroup
下面,而不是在同一个ViewGroup
的布局下面,产生的动画会有不一致的情况。上面的所有
ImageView
都放在RelativeLayout
的布局下面,与使用Linearlayout
为纵向根布局再加上两个子横向Linearlayout
,再将ImageView
两两放置到子横向Linearlayout
中,你会看到位置变化的转场效果可能不是你所期望的。(这里应该是因为不在同一个ViewGroup
下导致的)
1.3 使用
//场景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)
六、Activity间的转场动画
1. 基本主要API
-
window.enterTransition
: 进入时候的转场效果 -
window.exitTransition
: 退出时候的转场效果 -
window.reenterTransition
: 重新进入的转场效果 -
window.returnTransition
: 回退的时候的转场效果
对应样式下的
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)
上面的效果存在一些问题,有些动画重叠在一块了。
我们需要设置一下代码让进入退出的动画按序完成而不重叠到一块的时候,
setWindowAllowEnterTransitionOverlap(false)
setWindowAllowReturnTransitionOverlap(false)
或者在Activity
或者Application
对应的样式下面增加
- false
- false
七、Activity间的共享转场动画
1.基本API
对应各方法进入时候的转场效果,跟上面的转场动画的api是相对的
window.sharedElementEnterTransition
window.sharedElementExitTransition
window.sharedElementReenterTransition
-
window.sharedElementReturnTransition
对应样式下的
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
样式中开启
或者代码中开启
requestWindowFeature(Window.FEATURE_CONTENT_TRANSITIONS)
2.2. 定义两个布局都要设置android:transitionName
跳转布局一
布局二
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())
限制(选自Android官方文档)
Android
版本在4.0(API Level 14)
到4.4.2(API Level 19)
使用Android Support Library’s
应用于
SurfaceView
的动画可能无法正确显示。SurfaceView
实例是从非界面线程更新的,因此这些更新与其他视图的动画可能不同步。当应用于
TextureView
时,某些特定过渡类型可能无法产生所需的动画效果。扩展
AdapterView
的类(例如ListView
)会以与过渡框架不兼容的方式管理它们的子视图。如果您尝试为基于AdapterView
的视图添加动画效果,则设备显示屏可能会挂起。如果您尝试使用动画调整
TextView
的大小,则文本会在该对象完全调整过大小之前弹出到新位置。为了避免出现此问题,请勿为调整包含文本的视图的大小添加动画效果。
本章的源码:
https://github.com/lovebluedan/AnimatorPro.git
感谢:
Android官方文档
https://github.com/lgvalle/Material-Animations
https://github.com/codepath/android_guides/wiki/Shared-Element-Activity-Transition