Transition Animation系列文章:
在Android开发过程中,经常会遇到Activity之间切换的问题,转场动画就是用于布局(界面)变化时的过渡动画,即不同UI状态转换时的动画。
Transition过渡动画(转场动画)是在android4.4.2中引入的,那时只能对整个Activity或Fragment做动画,Google在Android5.0的Material Design中引入更为完整的Transition框架。
Android的过渡动画可分为四个部分:
在具体学习使用过渡动画前,先花点时间捋一下当中的两个重要概念:场景(scenes)
和转换(transitions)
;场景定义了一个确定的UI状态,而转换定义了两个场景切换的动画。
当两个场景切换时,Transition主要有以下两个行为:
下面来根据过渡动画的四个部分分别介绍,这里以Activity为例,Fragment类似。
关于Activity/Fragment切换,这里可以细分为四个动画,例如有两个Activity,分别是A和B:
在Android 5.0(API)中默认提供了三种转换:
分解(Explode)
:从场景中心移入或移出视图滑动(Slide)
:从场景边缘移入或移移出视图淡入淡出(Fade)
:通过调整透明度在场景中增添或移除视图在Android 5.0之前,一般是用overridePendingTransition()
这个方法来实现内容过渡:
* @param enterAnim A resource ID of the animation resource to use for
* the incoming activity. Use 0 for no animation.
* @param exitAnim A resource ID of the animation resource to use for
* the outgoing activity. Use 0 for no animation.
*/
public void overridePendingTransition(int enterAnim, int exitAnim) {
try {
ActivityManager.getService().overridePendingTransition(
mToken, getPackageName(), enterAnim, exitAnim);
} catch (RemoteException e) {
}
}
其中
enterAnim
是进入动画,exitAnim
是退出动画
关于该方法的使用十分简单,先在XML中定义好两个Activity的切换动画,然后:
startActivity(intent);
overridePendingTransition(R.anim.bottom_top_anim, R.anim.alpha_hide);
---
finish();
overridePendingTransition(R.anim.alpha_show, R.anim.top_bottom_anim);
备注:
overridePendingTransition
方法必须在startActivity()
或者finish()
方法后面在Android 5.0 之后,要启用内容过渡动画需要完成下面几个步骤:
在style中设置
<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
- "android:windowContentTransitions"
>true
style>
或者在代码中设置
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
}
<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
- "android:windowEnterTransition"
>@transition/activity_fade
- "android:windowExitTransition">@transition/activity_slide
style>
转换文件存放在res/transiton
目录下
res/transition/activity_fade.xml
<fade xmlns:android="http://schemas.android.com/apk/res/"
android:duration="1000"/>
res/transition/activity_slide.xml
<slide xmlns:android="http://schemas.android.com/apk/res/"
android:duration="1000"/>
(2)除了在style中指定转换,也可以在代码中指定转换
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
setContentView(R.layout.activity_main);
setupWindowAnimations();
TextView textView = (TextView) findViewById(R.id.start_activity);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
}
});
}
private void setupWindowAnimations() {
// inflate from xml
Slide slide = (Slide) TransitionInflater.from(this).inflateTransition(R.transition.activity_slide);
// or create directly
// Slide slide = new Slide();
// slide.setDuration(1000);
// slide.setSlideEdge(Gravity.LEFT);
getWindow().setExitTransition(slide);
getWindow().setReenterTransition(slide);
}
}
SecondActivity.java
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
setContentView(R.layout.activity_second);
setupWindowAnimations();
}
private void setupWindowAnimations() {
// inflate from xml
Fade fade = (Fade) TransitionInflater.from(this).inflateTransition(R.transition.activity_fade);
// or create directly
// Fade fade = new Fade();
// fade.setDuration(1000);
getWindow().setEnterTransition(fade);
}
从上述的代码示例中,总结如下:
转场动画在代码中是通过获取Window对象进行设置的,关于转场动画有四种场景:
void setEnterTransition(Transition transition)
当前界面进入动画void setExitTransition (Transition transition)
当前界面退出动画void setReenterTransition (Transition transition)
下个界面返回当前界面时,当前界面进入动画void setReturnTransition (Transition transition)
返回上个界面时当前界面的退出动画在xml中设置转场动画也有四种场景
android:windowEnterTransition
android:windowExitTransition
android:windowReenterTransition
android:windowReturnTransition
退出activity或调用了finish()
时,记得调用finishAfterTransition()
,保证执行退出动画。
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finishAfterTransition();
finish();
}
});
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
, ActivityOptions
是Android 5.0 后google提供的一种转动动画实现方式,并且提供了兼容包——ActivityOptionsCompat
。关于这部分的研究与使用,可以参考 android动画——过渡动画中ActivityOptions介绍与使用((Transition Animation 系列))前面做转场动画时,默认情况过渡动画会对所有的子View进行遍历加载动画,如果时添加目标则不会进行遍历所有子View,或者可以排除特定的View。
对于目标,一般有三种操作:
添加/排除/删除目标支持以下参数类型:
Transition addTarget (View target)
Transition addTarget (String targetName)
Transition addTarget (Class targetType)
Transition addTarget (int targetId)
相应的对于删除或者排除,则分别是:removeTarget()
与excludeTraget()
使用示例:
private void setupWindowAnimations() {
// inflate from xml
Fade fade = (Fade) TransitionInflater.from(this).inflateTransition(R.transition.activity_fade);
fade.excludeTarget(android.R.id.navigationBarBackground,true);
fade.excludeTarget(android.R.id.statusBarBackground,true);
Slide slide = (Slide) TransitionInflater.from(this).inflateTransition(R.transition.activity_slide);
slide.setSlideEdge(Gravity.LEFT);
slide.setDuration(2000);
slide.excludeTarget(android.R.id.navigationBarBackground, true); // 排除导航栏
slide.excludeTarget(android.R.id.statusBarBackground,true); //排除状态栏
slide.removeTarget(mTextView);
getWindow().setEnterTransition(slide);
getWindow().setReturnTransition(slide);
// getWindow().setReenterTransition(fade);
// getWindow().setExitTransition(slide);
}
在文章开头部分就阐述过,内容过渡动画创建之前,必须要确定视图的状态信息,这个动作需要修改所有过渡视图(transitioning view)的可见性。
startActivity()
时
exit transition
运行时哪些view会退出场景开始状态
INVISIBLE
结束状态
enter transition
运行时的所有过渡视图,设置这些过渡视图的可见性为INVISIBLE
开始状态
VISIBLE
结束状态
总结:
所有的内容转换都需要记录每个过渡视图的开始状态和结束状态。而抽象类Visibility
已经做了这部分内容了,Visibility的子类只需要实现 onAppear()
和 onDisappear()
方法,创建过渡视图进入或退出场景的 Animator。Android 5.0 中Visibility有三个子类 – Fade
、Slide
、Explode
,如果有需要的话也可以自定义Visibility子类。
关于Visibility的三个子类的使用探索,待补充。。。。。。。。。。。。。。。
一般默认情况下,内容过渡动画的进入/返回转换会在退出/重新进入转换结束前一点点开始,产生一个小的重叠来让整体的效果更自然、协调,如果不想产生重叠等效果,可以通过Window/Fragment 的setAllowEnterTransitionOverlap(boolean)
和setAllowReturnTransitionOverlap(boolean)
方法来设置。默认overlap时true
,进入转换会在退出转换开始后尽可能快地开始,如果设置为false
,进入转换转换只能在退出转换结束后开始。这个对Activity和Fragment的共享元素过渡动画同样有效。
private void setupWindowAnimations() {
// inflate from xml
Slide slide = (Slide) TransitionInflater.from(this).inflateTransition(R.transition.activity_slide);
// or create directly
// Slide slide = new Slide();
// slide.setDuration(1000);
slide.setSlideEdge(Gravity.LEFT);
getWindow().setAllowEnterTransitionOverlap(true);
getWindow().setAllowReturnTransitionOverlap(true);
getWindow().setExitTransition(slide);
getWindow().setReenterTransition(slide);
}
除了在代码中设置,还可以在xml中设置:
<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
- "android:windowAllowEnterTransitionOverlap"
>true
- "android:windowAllowReturnTransitionOverlap">true
style>
如果你要实现一个动画,google官方提供的slide、fade等都难以实现你的需求的时候,你可以自己写一个转场动画。
自定义转场动画,只需要继承Visibility
或者Transition
,其中Visibility
是继承自Transibility
。
如果转场动画只是某个View出现或消失,那么可以考虑继承Visibility
,如果是类ShareElement
(下面介绍的共享元素)这样的转场动画,那么就需要继承Transition
。
继承Visibility
需要重写4个方法:
captureStartValues()
:保存计算动画初始状态的一个属性值captureEndValues()
:保存计算动画结束状态的一个属性值onAppear()
:如果是进入动画,即显示某个View会执行这个方法onDisappear()
:如果是退出,即不显示某个View会执行这个方法示例:
public class FABTransition extends Visibility {
private static final String BOTTOM_TRANSITION_Y = "FABTransition:change_transY:transitionY";
private static final String TAG = "FABTransition";
private View fab;
private Context context;
public FABTransition(Context context, View fab) {
this.fab = fab;
this.context = context;
}
/**
* 收集动画的开始信息
* @param transitionValues 只有两个成员变量view和values, view指的是我们要从哪个view上收集信息, values是用来存放我们收集到的信息的
* 比如: 在captureStartValues里, transitionValues.view指的就是我们在开始动画的界面上的那个view,
* 在captureEndValues指的就是在目标界面上的那个view
*/
@Override
public void captureStartValues(TransitionValues transitionValues) {
super.captureStartValues(transitionValues);
int transY= (int) (context.getResources().getDisplayMetrics().density*56*2);
transitionValues.values.put(BOTTOM_TRANSITION_Y,transY);
}
/**
* 收集动画结束的信息
*/
@Override
public void captureEndValues(TransitionValues transitionValues) {
super.captureEndValues(transitionValues);
transitionValues.values.put(BOTTOM_TRANSITION_Y, 0);
}
/**
* 创建一个Animator
*/
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
return super.createAnimator(sceneRoot, startValues, endValues);
}
@Override
public Animator onAppear(ViewGroup sceneRoot, final View view, TransitionValues startValues, TransitionValues endValues) {
if (null == startValues || null == endValues) {
return null;
}
int startY= (int) startValues.values.get(BOTTOM_TRANSITION_Y);
int endY= (int) endValues.values.get(BOTTOM_TRANSITION_Y);
// 在这里去除之前存储的初始值和结束值,然后执行动画
if(view==fab && startY!=endY){
ValueAnimator valueAnimator=ValueAnimator.ofInt(startY,endY);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Object transY= animation.getAnimatedValue();
if(transY!=null){
view.setTranslationY((Integer) transY);
}
}
});
return valueAnimator;
}
return null;
}
@Override
public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, TransitionValues endValues) {
if (null == startValues || null == endValues) {
return null;
}
int startY= (int) endValues.values.get(BOTTOM_TRANSITION_Y);
int endY= (int) startValues.values.get(BOTTOM_TRANSITION_Y);
if(view==fab && startY!=endY){
ValueAnimator valueAnimator=ValueAnimator.ofInt(startY,endY);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Object transY= animation.getAnimatedValue();
if(transY!=null){
view.setTranslationY((Integer) transY);
}
}
});
return valueAnimator;
}
return null;
}
}
使用自定义的FABTransition ,为它添加一个Target:
TransitionSet cotentTransition=new TransitionSet();
Slide slide=new Slide(Gravity.LEFT);
slide.setDuration(500);
slide.excludeTarget(android.R.id.navigationBarBackground, true);
slide.excludeTarget(android.R.id.statusBarBackground, true);
slide.excludeTarget(R.id.appBarLayout, true);
slide.excludeTarget(R.id.fab, true);
cotentTransition.addTransition(slide);
//fab进入动画
FABTransition fabTransition=new FABTransition(this,this);
fabTransition.addTarget(R.id.fab);
fabTransition.setDuration(500);
cotentTransition.addTransition(fabTransition);
getWindow().setEnterTransition(cotentTransition);
不同activity或fragment之间传统的过渡涉及到整个View层级相互独立的进入和退出过渡动画。例如,渐入过渡,滑动过渡,或新引入的爆炸过渡。
然后,大多数情况下页面之间有共用的元素,并且在页面切换时有能力提供这些共享元素分别强调连续性的过渡作为用户操作的App。
shareElement Transition
指的是共享元素从activity/fragment到其他activity/fragment时的动画,其过渡动画在确定开始状态和结束状态是分别在两个页面上,可以实现共享元素从一个页面到另一个页面的动画。
Android 5.0(API 21)默认提供了下面四种共享元素转换:
changeBounds
- 为目标视图的布局布局边界的变化添加动画
changeClipBounds
- 为目标视图的裁剪边界的变化添加动画
changeTransform
- 为目标视图的缩放与旋转变化添加动画
changeImageTransform
- 为目标图像的大小与缩放变化添加动画
与内容过渡动画类似,共享元素过渡动画也分为 sharedElementExitTransition
、sharedElementEnterTransition
、sharedElementReturnTransition
、sharedElementReenterTransition
。
一般启用了共享元素过渡动画,都会启用内容过渡动画。
注意:共享元素过渡需要Android 5.0(API 21)及以上并且将会忽略低版本。使用API 21指定特性之前确保在运行时检查版本。
styles.xml
文件中开启窗口内容过渡<style name="BaseAppTheme" parent="Theme.AppCompat.Light">
- "android:windowContentTransitions"
>true
style>
或者在代码中设置:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
}
这一步与内容过渡动画的第一步是一样的。
使用共享元素的布局中指定共用过渡名称。使用android:transitionName
属性。
activity_main.xml
<TextView
android:id="@+id/start_shared_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transitionName="profile"
android:layout_gravity="center_horizontal"
android:text="start shared activity"
android:textSize="25sp"/>
activity_shared_element.xml
<TextView
android:id="@+id/shared_element_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:transitionName="profile"
android:text="这里是共享元素页面"/>
处理在xml文件文件中通过android:transitionName
属性设置外,还可以在代码中通过ViewCompat.setTranslationName()
方法设置
ViewCompat.setTransitionName(view,"imageView");
使用ActivityOpstions.makeSceneTransitionAnimation()
方法来指定共享元素的 origin view
和transition name
。如果要结束第二个 Activity 时也显示共享元素过渡动画,请调用Activity.finishAfterTransitino()
而非Activity.finish()
。
textView1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SharedElementActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this,textView1, "profile").toBundle());
}
});
如果你想要从源view层级动画多个元素,可以通过在源View和目标view使用不同的过渡名称实现。
textView1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SharedElementActivity.class);
Pair<View, String> p1 = Pair.create((View)textView1, "profile");
Pair<View, String> p2 = Pair.create((View)textView, "test");
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this,p1,p2).toBundle());
}
});
注意:不要使用过多的共享元素。它会使场景有一个粘性直到动画从一个屏幕到另一个屏幕(可能会包含多个共享元素),过多的共享元素会导致用户分心产生糟糕的体验。
ChangeImageTransform changeImageTransform = new ChangeImageTransform();
ChangeBounds changeBounds = new ChangeBounds();
ChangeClipBounds changeClipBounds = new ChangeClipBounds();
TransitionSet transitionSet=new TransitionSet();
transitionSet.addTransition(changeImageTransform);
// transitionSet.addTransition(changeBounds);
transitionSet.addTransition(changeClipBounds);
transitionSet.addTarget(R.id.image);
transitionSet.setDuration(1000);
getWindow().setSharedElementEnterTransition(transitionSet);
// getWindow().setSharedElementReturnTransition(transitionSet);
Transition动画其实就是拿着第一页某个view的信息去第二页的某个view上做的动画, 这样我们在视觉上就会产生一个渐变的错觉。前面我们大致学习了该类动画的使用方法。对于共享动画,google提供了ChangeBounds
, ChangeTransform
, ChangeImageTransform
, 和 ChangeClipBounds
四种动画,基本能够适用于大多数典型场景。你也可以自定义自己的Transition。这里我们开始自己自定义Transition,用于针对不同的view采用不同的动画效果。
自定义Transition,与自定义View等一样,我们需要继承Transition
类,主要重写三个方法:
captureStartValues(TransitionValues transitionValues)
captureEndValues(TransitionValues transitionValues)
createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)
关于这三个方法中的一个参数TransitionValues
:这个类很简单,只有两个成员变量view
和values
。view指的是我们要从哪个view上收集信息, values是用来存放我们收集到的信息的
示例:
public class ColorTransition extends Transition {
private static final String COLOR_BACKGROUND = "ColorTransition:change_color:background";
private int mStartColor;
private int mEndColor;
public ColorTransition(int mStartColor, int mEndColor) {
this.mStartColor = mStartColor;
this.mEndColor = mEndColor;
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
transitionValues.values.put(COLOR_BACKGROUND,mStartColor);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
transitionValues.values.put(COLOR_BACKGROUND,mEndColor);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
if (null == startValues || null == endValues) {
return null;
}
final View view = endValues.view;
int startColor = (int) startValues.values.get(COLOR_BACKGROUND);
int endColor = (int) endValues.values.get(COLOR_BACKGROUND);
if (startColor != endColor) {
ValueAnimator animator = ValueAnimator.ofArgb(startColor, endColor);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Object value = animation.getAnimatedValue();
if (null != value) {
view.setBackgroundColor((Integer) value);
}
}
});
return animator;
}
return null;
}
}
共享元素过的动画的原理与内容过渡动画类似,系统也会在创建共享元素动画前直接修改共享视图的属性。当Activity A启动ActivityB时:
startActivity()
后,B 启动时,窗口的背景是透明的。INVISIBLE
。对比内容过渡动画,内容过渡动画中系统会修改 transition views 的可见性,而共享元素过渡动画中系统会修改 shared element views 的位置、大小和显示。而且我们也可以看出实际上共享元素的 view 其实并没有在 Activity/ Fragment 之间共享,事实上,我们看到的进入或者返回的共享元素过渡动画都是直接在 B 的视图中运行的。
Window中有个关于共享元素的设置setSharedElementsUseOverlay(boolean sharedElementsUseOverlay)
,我们将其设为false,重启App:
默认情况下,共享元素视图是绘制在整个视图结构之上的——窗口的ViewOverlay
层。添加到视图的ViewOverlay层的Drawables或者Views都会在所有其他视图上面绘制,不会被遮挡。而共享元素默认会在 ViewOverlay 层绘制的原因是:共享元素是整个进入转换中的焦点,如果 transitioning view 忽然遮盖了共享元素的话,整体的效果会大打折扣。
虽然共享元素视图默认是绘制在 ViewOverlay 层的,但是也可以在必要情况下使用 Window.setSharedElementsUseOverlay(false)
来禁用。
Transition会获取共享视图的前后状态值来创建动画,如果我们的view(例如图片)是网上下载的,那么很有可能图片的准确大小需要下载下来才能确定,Activity Transition API提供了一对方法暂时推迟过渡,直到我们确切地知道共享元素已经被适当的渲染和放置。在在onCreate中调用postponeEnterTransition()
(API >= 21)或者supportPostponeEnterTransition()
(API < 21)延迟过渡;当图片的状态确定后,调用startPostponedEnterTransition()
(API >= 21)或supportStartPostponedEnterTransition()
(API < 21)恢复过渡,常见处理:
// ... load remote image with Glide/Picasso here
supportPostponeEnterTransition();
ivBackdrop.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
ivBackdrop.getViewTreeObserver().removeOnPreDrawListener(this);
supportStartPostponedEnterTransition();
return true;
}
}
);
这部分内容待更新。。。。。