诞生于4.4的transition框架为在不同的UI状态之间产生动画效果提供了非常方便的API。5.0中Activity和Fragment 转场变换也是建立在Transitions框架的新特性之上的。
该框架主要基于两个概念:scenes(场景)和transitions(变换)
1. Secene
Transition Framework 核心就是根据Scene
的不同帮助开发者们自动生成动画
。
官方文档
A scene represents the collection of values that various properties in the View hierarchy will have when the scene is applied. A Scene can be configured to automatically run a Transition when it is applied, which will animate the various property changes that take place during the scene change.通俗的解释就是这个类存储着一个根view下的各种view的属性。
创建Scene
创建一个 Scene有两种方法
// sceneRoot是Scene的 Container,也可以说是它的根布局
1. Scene.getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) ;
2. new Scene(ViewGroup sceneRoot, View layout);
例子如下:
protected Scene scene1;
protected Scene scene2;
protected void initScene(@IdRes int sceneRoot, @LayoutRes int scene1_layout, @LayoutRes int scene2_layout) {
ViewGroup sceneRoot= (ViewGroup) findViewById(rootView);
scene1= Scene.getSceneForLayout(sceneRoot,scene1_layout,this);
scene2=Scene.getSceneForLayout(sceneRoot,scene2_layout,this);
TransitionManager.go(scene1); //先把初始状态设置为scene1
}
或者
protected Scene scene1;
protected Scene scene2;
ViewGroup sceneRoot=findViewById(R.id.rootView);
View view1= LayoutInflater.from(this).inflate(R.layout.changeclipbounds_scene,null);
View view2= LayoutInflater.from(this).inflate(R.layout.changeclipbounds_scene,null);
ImageView iv1=view1.findViewById(R.id.imageView);
ImageView iv2=view2.findViewById(R.id.imageView);
iv1.setClipBounds(new Rect(0,0,100,100));
iv2.setClipBounds(new Rect(100,100,200,200));
scene1=new Scene(sceneRoot,view1);
scene2=new Scene(sceneRoot,view2);
TransitionManager.go(scene1);//先把初始状态设置为scene1
sceneRoot 在动画开始时,会将sceneRoot中的所有子View都remove掉,然后在sceneRoot 中加载我们的end Scene。
所以,对于end Scene,如果是通过代码new Scene(mSceneRoot, view)创建的Scene其实对于view是有要求的:view是没有parentview的,不然在addview的时候会报错
验证代码如下:
//测试该段代码
LinearLayout container=new LinearLayout(appContext);
View view= LayoutInflater.from(appContext).inflate(R.layout.layout_temp,null);
FrameLayout frameLayout=new FrameLayout(appContext);
frameLayout.addView(view);
container.addView(view);
//log
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4917)
at android.view.ViewGroup.addView(ViewGroup.java:4748)
例子
//scene1.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/bangtang"
tools:layout_editor_absoluteX="128dp"
tools:layout_editor_absoluteY="58dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="58dp"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/shengdanlaoren"
tools:layout_editor_absoluteX="49dp"
tools:layout_editor_absoluteY="226dp"
app:layout_constraintTop_toTopOf="@+id/imageView3"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="49dp" />
<ImageView
android:id="@+id/imageView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/xueren"
tools:layout_editor_absoluteX="210dp"
tools:layout_editor_absoluteY="226dp"
android:layout_marginTop="40dp"
app:layout_constraintTop_toBottomOf="@+id/imageView1"
android:layout_marginEnd="46dp"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
//scene2.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/shengdanlaoren"
tools:layout_editor_absoluteX="128dp"
tools:layout_editor_absoluteY="58dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="58dp"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/imageView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/xueren"
tools:layout_editor_absoluteX="49dp"
tools:layout_editor_absoluteY="226dp"
app:layout_constraintTop_toTopOf="@+id/imageView1"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="49dp" />
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/bangtang"
tools:layout_editor_absoluteX="210dp"
tools:layout_editor_absoluteY="226dp"
android:layout_marginTop="40dp"
app:layout_constraintTop_toBottomOf="@+id/imageView2"
android:layout_marginEnd="46dp"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
Activity代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base_scene);
Button btn=findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
clickChange();
}
});
ViewGroup sceneRoot= (ViewGroup) findViewById(rootView);
scene1= Scene.getSceneForLayout(sceneRoot,scene1_layout,this);
scene2=Scene.getSceneForLayout(sceneRoot,scene2_layout,this);
TransitionManager.go(scene1);//先把初始状态设置为scene1
}
protected void clickChange(){
TransitionManager.go(isScene1?scene2:scene1,getTransition());
isScene1=!isScene1;
}
Transition getTransition() {
return new ChangeBounds();
}
getTransition方法提供的是两个Scene的切换效果,下面会讲到,所以先不管它。
最后出来的效果是:
根据效果图和Id对比图可以发现切换时是相同id的View之间互相切换,那么如果两个Scene之间View的Id不对等(id不相同 或者 一多一少)呢?
这个时候可以试着运行一下就会发现:仍然可以实现两个场景的切换,但是切换过程没有任何动画效果(即Transition没有起作用),就只是简单的替换。
上面例子getTransition方法返回ChangeBounds(),其实就是一种Transition的实现,下面来详细了解一下:
2. transitions
当一个Scene发生改变时,transition主要负责:
- 捕捉每个View在开始场景和结束场景时的状态。
- 根据两个场景(开始和结束)之间的区别创建一个
Animator
。
2.1 API 21之后,框架层提供的Transitions:
2.1.1 ChangeBounds :检测view的位置边界创建移动和缩放动画
捕获共享元素的layout bound,然后播放layout bound变化动画。ChangeBounds 是共享元素变换中用的最多的,因为前后两个activity中共享元素的大小和位置一般都是不同的。
根据始末位置画出Path
,再根据Path创造Animator
/**
* This transition captures the layout bounds of target views before and after
* the scene change and animates those changes during the transition.
*
* <p>A ChangeBounds transition can be described in a resource file by using the
* tag <code>changeBounds</code>, using its attributes of
* {@link android.R.styleable#ChangeBounds} along with the other standard
* attributes of {@link android.R.styleable#Transition}.</p>
*/
public class ChangeBounds extends Transition {
...
}
2.1.2 ChangeClipBounds :检测view的剪切区域的位置边界,和ChangeBounds类似。不过ChangeBounds针对的是view而ChangeClipBounds针对的是view的剪切区域(setClipBound(Rect rect) 中的rect)。如果没有设置则没有动画效果
捕获共享元素clip bounds,然后播放clip bounds变化动画。
/**
* ChangeClipBounds captures the {@link android.view.View#getClipBounds()} before and after the
* scene change and animates those changes during the transition.
*/
public class ChangeClipBounds extends Transition {
...
@Override
public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
...
Rect start = (Rect) startValues.values.get(PROPNAME_CLIP);
Rect end = (Rect) endValues.values.get(PROPNAME_CLIP);
boolean endIsNull = end == null;
...
endValues.view.setClipBounds(start);
RectEvaluator evaluator = new RectEvaluator(new Rect());
ObjectAnimator animator =
ObjectAnimator.ofObject(endValues.view, "clipBounds", evaluator, start, end);
if (endIsNull) {
final View endView = endValues.view;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
endView.setClipBounds(null);
}
});
}
return animator;
}
}
2.1.3 ChangeImageTransform :检测ImageView(这里是专指ImageView)的尺寸,位置以及ScaleType,并创建相应动画。
捕获共享元素(ImageView)的transform matrices 属性,然后播放ImageViewtransform matrices 属性变化动画。与ChangeBounds相结合,这个变换可以让ImageView在动画中高效实现大小,形状或者ImageView.ScaleType
属性平滑过度。
/**
* This Transition captures an ImageView's matrix before and after the
* scene change and animates it during the transition.
*
* <p>In combination with ChangeBounds, ChangeImageTransform allows ImageViews
* that change size, shape, or {@link android.widget.ImageView.ScaleType} to animate contents
* smoothly.</p>
*/
public class ChangeImageTransform extends Transition {
...
/**
* Creates an Animator for ImageViews moving, changing dimensions, and/or changing
* {@link android.widget.ImageView.ScaleType}.
*
* @param sceneRoot The root of the transition hierarchy.
* @param startValues The values for a specific target in the start scene.
* @param endValues The values for the target in the end scene.
* @return An Animator to move an ImageView or null if the View is not an ImageView,
* the Drawable changed, the View is not VISIBLE, or there was no change.
*/
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
if (startBounds == null || endBounds == null) {
return null;
}
Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
boolean matricesEqual = (startMatrix == null && endMatrix == null) ||
(startMatrix != null && startMatrix.equals(endMatrix));
if (startBounds.equals(endBounds) && matricesEqual) {
return null;
}
ImageView imageView = (ImageView) endValues.view;
Drawable drawable = imageView.getDrawable();
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
ObjectAnimator animator;
if (drawableWidth == 0 || drawableHeight == 0) {
animator = createNullAnimator(imageView);
} else {
if (startMatrix == null) {
startMatrix = Matrix.IDENTITY_MATRIX;
}
if (endMatrix == null) {
endMatrix = Matrix.IDENTITY_MATRIX;
}
ANIMATED_TRANSFORM_PROPERTY.set(imageView, startMatrix);
animator = createMatrixAnimator(imageView, startMatrix, endMatrix);
}
return animator;
}
...
}
2.1.4 ChangeScroll :滑动的属性发生了变化
/**
* This transition captures the scroll properties of targets before and after
* the scene change and animates any changes.
*/
public class ChangeScroll extends Transition {
...
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
TransitionValues endValues) {
if (startValues == null || endValues == null) {
return null;
}
final View view = endValues.view;
int startX = (Integer) startValues.values.get(PROPNAME_SCROLL_X);
int endX = (Integer) endValues.values.get(PROPNAME_SCROLL_X);
int startY = (Integer) startValues.values.get(PROPNAME_SCROLL_Y);
int endY = (Integer) endValues.values.get(PROPNAME_SCROLL_Y);
Animator scrollXAnimator = null;
Animator scrollYAnimator = null;
if (startX != endX) {
view.setScrollX(startX);
scrollXAnimator = ObjectAnimator.ofInt(view, "scrollX", startX, endX);
}
if (startY != endY) {
view.setScrollY(startY);
scrollYAnimator = ObjectAnimator.ofInt(view, "scrollY", startY, endY);
}
return TransitionUtils.mergeAnimators(scrollXAnimator, scrollYAnimator);
}
}
2.1.5 ChangeTransform :检测view的scale和rotation创建缩放和旋转动画
捕获共享元素的缩放(scale)与旋转(rotation)属性 ,然后播放缩放(scale)与旋转(rotation)属性变化动画。
/**
* This Transition captures scale and rotation for Views before and after the
* scene change and animates those changes during the transition.
*
* A change in parent is handled as well by capturing the transforms from
* the parent before and after the scene change and animating those during the
* transition.
*/
public class ChangeTransform extends Transition {
...
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
TransitionValues endValues) {
if (startValues == null || endValues == null ||
!startValues.values.containsKey(PROPNAME_PARENT) ||
!endValues.values.containsKey(PROPNAME_PARENT)) {
return null;
}
ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
boolean handleParentChange = mReparent && !parentsMatch(startParent, endParent);
Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_INTERMEDIATE_MATRIX);
if (startMatrix != null) {
startValues.values.put(PROPNAME_MATRIX, startMatrix);
}
Matrix startParentMatrix = (Matrix)
startValues.values.get(PROPNAME_INTERMEDIATE_PARENT_MATRIX);
if (startParentMatrix != null) {
startValues.values.put(PROPNAME_PARENT_MATRIX, startParentMatrix);
}
// First handle the parent change:
if (handleParentChange) {
setMatricesForParent(startValues, endValues);
}
// Next handle the normal matrix transform:
ObjectAnimator transformAnimator = createTransformAnimator(startValues, endValues,
handleParentChange);
if (handleParentChange && transformAnimator != null && mUseOverlay) {
createGhostView(sceneRoot, startValues, endValues);
}
return transformAnimator;
}
...
/**
* PathAnimatorMatrix allows the translations and the rest of the matrix to be set
* separately. This allows the PathMotion to affect the translations while scale
* and rotation are evaluated separately.
*/
private static class PathAnimatorMatrix {
...
}
}
2.2 如何使用Transition
2.2.1 TransitionManager.go(Scene scene, Transition transition)
直接在切换Scene时,设置效果
2.2.2 根据设置的 transition文件 自动生成Animator效果
在XML中或者在代码中设置,举个例子就是: res->transition文件下创建transition文件
//xxx.xml Fade、Slide、Explode :渐入、滑动、爆炸
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<explode/>
<fade/>
</transitionSet>
//或者直接new Fade()等等
设置 transition文件 自动生成Animator效果,通常在两种情况下使用:
- 切换Activity/Fragment时设置场景切换效果(这个部分在后续的文章会讲到)
- beginDelayedTransition()设置延时动画
beginDelayedTransition
为每一个结束关键帧都专门设置一个xml的Scene布局岂不是很麻烦?如图所示,点击每个图片的切换效果都差不多,那是不是就要设置4个Scene,然后对应的点击某个View就跳转到某个Scene呢?
所以,就有了 :
延时动画
:beginDelayedTransition()
例子:(四个ImageView,点击每一个,那个就会放大,其他三个就会消失)
<?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"
android:id="@+id/rootView"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView1"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="72dp"
android:layout_marginTop="71dp"
app:srcCompat="@drawable/bangtang"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true" />
<ImageView
android:id="@+id/imageView2"
android:layout_width="120dp"
android:layout_height="120dp"
app:srcCompat="@drawable/shengdanshu"
android:layout_alignTop="@+id/imageView1"
android:layout_alignParentEnd="true"
android:layout_marginEnd="30dp" />
<ImageView
android:id="@+id/imageView3"
android:layout_width="120dp"
android:layout_height="120dp"
app:srcCompat="@drawable/xueren"
android:layout_alignTop="@+id/imageView4"
android:layout_alignStart="@+id/imageView1" />
<ImageView
android:id="@+id/imageView4"
android:layout_width="120dp"
android:layout_height="120dp"
app:srcCompat="@drawable/xunlu"
android:layout_marginTop="24dp"
android:layout_below="@+id/imageView2"
android:layout_alignStart="@+id/imageView2" />
</RelativeLayout>
// explode_fade_changebounds.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<explode/>
<fade/>
</transitionSet>
public class BeginDelayedActivity extends AppCompatActivity implements View.OnClickListener{
ImageView iv1,iv2,iv3,iv4;
ViewGroup rootView;
boolean isBig=false;
int primarySize;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_begin_delayed);
rootView=findViewById(R.id.rootView);
iv1=findViewById(R.id.imageView1);
iv2=findViewById(R.id.imageView2);
iv3=findViewById(R.id.imageView3);
iv4=findViewById(R.id.imageView4);
primarySize=iv1.getLayoutParams().width;
iv1.setOnClickListener(this);
iv2.setOnClickListener(this);
iv3.setOnClickListener(this);
iv4.setOnClickListener(this);
}
@Override
public void onClick(View v) {
//start scene 是当前的scene
TransitionManager.beginDelayedTransition(rootView, TransitionInflater.from(this).inflateTransition(R.transition.explode_fade_changebounds));
//next scene 此时通过代码已改变了scene statue
changeScene(v);
}
private void changeScene(View v) {
changeSize(v);
changeVisibility(iv1,iv2,iv3,iv4);
v.setVisibility(View.VISIBLE);
}
private void changeSize(View v) {
isBig=!isBig;
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if(isBig){
layoutParams.width=(int)(1.5*primarySize);
layoutParams.height=(int)(1.5*primarySize);
}else {
layoutParams.width=primarySize;
layoutParams.height=primarySize;
}
v.setLayoutParams(layoutParams);
}
/**
* VISIBLE和INVISIBLE状态切换
* @param ivs
*/
private void changeVisibility(ImageView ... ivs) {
for (View view:ivs){
view.setVisibility(view.getVisibility()==View.VISIBLE?View.INVISIBLE:View.VISIBLE);
}
}
}
我们在这做详细的分析 :
假设最开始每个view都是可见的:
- 当点击事件发生之后调用TransitionManager的beginDelayedTransition()方法,并且传递了mRootView和一个Fade对象最为参数。之后,framework会立即调用transition类的captureStartValues()方法为每个view保存其当前的可见状态(visibility)。
- 当beginDelayedTransition返回之后,在上面的代码中将每个view设置为不可见。
- 在接下来的显示中framework会调用transition类的captureEndValues()方法,记录每个view最新的可见状态。
- 接着,framework调用transition的createAnimator()方法。transition会分析每个view的开始和结束时的数据发现view在开始时是可见的,结束时是不可见的。Fade(transition的子类)会利用这些信息创建一个用于把view的alpha属性变为0的AnimatorSet,并且将此AnimatorSet对象返回。
- framework会运行返回的Animator,导致所有的View都渐渐消失。
这样就达到了:通过属性的改变,就发生动画...达到了代码的精简
3. 总结
- 创建两个Scene(起始关键帧 和 结束关键帧)
- 利用系统内置的或自定义的transitions创建Animator
- 开启动画
这样就是实现了简单的一个动画效果,这个过程我们只关心 开始状态和结束状态,并为状态的变化规定了变化规律(transitions),然后自动帮我们生成效果
由此可见 :transition框架的两个主要优点
第一、Transitions抽象和封装了属性动画,Animator的概念对开发者来说是透明的,因此它极大的精简了代码量。开发者所做的所有事情只是改变一下view前后的状态数据,Transition就会自动的根据状态的区别去生成动画效果。第二、不同场景之间变换的动画效果可以简单的通过使用不同的Transition类来改变
基础的介绍就先讲到这里!!!
上面讲到了基础的 TransitionManager.go()
和 beginDelayedTransition()
开启动画。其实还有一种开启方式更为常见 :setEnterTransition()/setSharedElementEnterTransition()
//当然,这得看下回分解
Transition系列文章
一、初识Transition—实现两个场景的变换
二、番外篇 Transition之ViewOverlay
三、定义 界面指定元素 或界面间共享元素 的转场动画基础
四、Content Transition实现非共享元素转场
五、SharedElementTransition之Activity间的转场
六、SharedElementTransition之Fragment间的转场
七、番外篇- 自定义Visibility
八、5.0以下实现共享转场
本篇参考:
Activity和Fragment Transition介绍
Android 过渡(Transition)动画解析之基础篇
animatedTransitionsLearn-master