除了常规的动画(帧动画、补间动画、属性动画)等作用于单个控件的动画,Android 还提供了一种类似的动画的功能,用于在两个不同的布局切换时提供过渡动画效果—-Transitions Framework。
该框架帮助在布局改变的时候增加动画效果,它会在布局发生改变的时候应用一个或多个动画效果于布局中的每个控件。框架具有以下特点:
框架由Scenes、Transitions和TransitionManager构成。关系图如下:
Scene保存了布局的状态,包括所有的控件和控件的属性。布局可以是一个简单的视图控件或者复杂的视图树和子布局。保存了这个布局状态到Scene后,我们就可以从另一个场景变化到该场景。Android 提供了一个类Scene
来代表场景。
从一个场景到另一个场景的变换中会有动画效果,这些动画信息就保存在Transition
对象中。要运行动画,我们要使用TransitionManager
实例来应用Transition
Transitions的生命周期和Activity的生命周期类似,它代表了变化过程的状态。在一些重要的状态,框架会触发回调方法,以便我们实现一些操作逻辑。
该框架存在一定的限制。如下:
SurfaceView
,因为SurfaceView
的绘制在非主线程中执行,会导致同步问题。TextureView
AdapterView
的控件,比如ListView
。因为它们是用一种不兼容的方式来管理他们的item的。上面简单了介绍了整个框架的构成,下面就实战一下
<RelativeLayout android:id="@+id/container" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<View android:id="@+id/transition_square" android:layout_width="@dimen/square_size_normal" android:layout_height="@dimen/square_size_normal" android:background="#990000" android:gravity="center"/>
<ImageView android:id="@+id/transition_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/transition_square" android:src="@drawable/ic_launcher"/>
<ImageView android:id="@+id/transition_oval" android:layout_width="32dp" android:layout_height="32dp" android:layout_below="@id/transition_image" android:src="@drawable/oval"/>
</RelativeLayout>
R.layout.scene2
<RelativeLayout android:id="@+id/container" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<View android:id="@+id/transition_square" android:layout_width="@dimen/square_size_normal" android:layout_height="@dimen/square_size_normal" android:layout_alignParentBottom="true" android:background="#990000" android:gravity="center"/>
<ImageView android:id="@+id/transition_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:src="@drawable/ic_launcher"/>
<ImageView android:id="@+id/transition_oval" android:layout_width="32dp" android:layout_height="32dp" android:layout_centerHorizontal="true" android:src="@drawable/oval"/>
</RelativeLayout>
R.layout.scene3
<RelativeLayout android:id="@+id/container" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<View android:id="@+id/transition_square" android:layout_width="@dimen/square_size_normal" android:layout_height="@dimen/square_size_normal" android:layout_centerHorizontal="true" android:background="#990000" android:gravity="center"/>
<ImageView android:id="@+id/transition_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:src="@drawable/ic_launcher"/>
<ImageView android:id="@+id/transition_oval" android:layout_width="32dp" android:layout_height="32dp" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:src="@drawable/oval"/>
<TextView android:id="@+id/transition_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/additional_message" android:textAppearance="?android:attr/textAppearanceLarge"/>
</RelativeLayout>
仔细观察可知,这三个布局只是图标摆放的位置不同,还有就是布局3添加了一个TextView控件。
接下来我们为每个布局创建场景:
mSceneRoot = (ViewGroup) view.findViewById(R.id.scene_root);
//用构造方法创建场景
mScene1 = new Scene(mSceneRoot, (ViewGroup) mSceneRoot.findViewById(R.id.container));
//从布局资源文件创建场景
mScene2 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene2, getActivity());
mScene3 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene3, getActivity());
//针对布局3,我们要自定义一个Transition,使得布局3中的TextView能够单独地淡入淡出
mTransitionManagerForScene3 = TransitionInflater.from(getActivity())
.inflateTransitionManager(R.transition.scene3_transition_manager, mSceneRoot);
看看自定义Transition的资源文件:
R.transition.scene3_transition_manager
<transitionManager xmlns:android="http://schemas.android.com/apk/res/android">
<transition android:toScene="@layout/scene3" android:transition="@transition/changebounds_fadein_together"/>
</transitionManager>
R.transition.changebounds_fadein_together
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<fade android:fadingMode="fade_in">
<targets>
<target android:targetId="@id/transition_title" />
</targets>
</fade>
</transitionSet>
<transitionManager>
标签中可以针对不同的场景定义一系列<transition>
,transition可以是各种transition的集合。
最后就是执行场景变换了,具体看代码:
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.select_scene_1: {
//切换到场景1,使用默认的transition
TransitionManager.go(mScene1);
break;
}
case R.id.select_scene_2: {
//切换到场景2
TransitionManager.go(mScene2);
break;
}
case R.id.select_scene_3: {
//切换到场景3,使用自定义transition
mTransitionManagerForScene3.transitionTo(mScene3);
break;
}
case R.id.select_scene_4: {
//场景4,可以不定义具体的Scene,
//在delayed transition后直接修改控件的属性
//这样也可以形成动画效果
TransitionManager.beginDelayedTransition(mSceneRoot);
View square = mSceneRoot.findViewById(R.id.transition_square);
ViewGroup.LayoutParams params = square.getLayoutParams();
int newSize = getResources().getDimensionPixelSize(R.dimen.square_size_expanded);
params.width = newSize;
params.height = newSize;
square.setLayoutParams(params);
break;
}
}
}
以上就是基本的场景变换的流程。结合效果图,我们发现这种方式可以非常简单地实现布局切换的动画效果,如果用Animation来实现该效果可能会非常复杂。
系统默认提供的Transition有Fade
、ChangeBounds
、Slide
等等。除了这些,我们还可以自己实现一些Transition,达到自己想要的效果。下面我们就来实现ChangeColor
的变换,先看效果图:
这里也定义了3种场景布局:
R.layout.scene1
<LinearLayout android:id="@+id/container" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" tools:context="com.example.android.customtransition.CustomTransitionFragment">
<View android:id="@+id/view_1" android:layout_width="64dp" android:layout_height="64dp" android:layout_margin="8dp" android:background="#f00"/>
<View android:id="@+id/view_2" android:layout_width="64dp" android:layout_height="64dp" android:layout_margin="8dp" android:background="#0f0"/>
<View android:id="@+id/view_3" android:layout_width="64dp" android:layout_height="64dp" android:layout_margin="8dp" android:background="#00f"/>
</LinearLayout>
其他两个布局只是颜色不同而已,就不贴代码了。
重点看看自定义Transition的代码:
ChangeColor.java
public class ChangeColor extends Transition {
/** 用于存储颜色值的KEY*/
private static final String PROPNAME_BACKGROUND = "customtransition:change_color:background";
// BEGIN_INCLUDE (capture_values)
/** * 获取场景的控件的背景色 */
private void captureValues(TransitionValues values) {
// 获取控件的属性,备用
values.values.put(PROPNAME_BACKGROUND, values.view.getBackground());
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
//后去开始的背景色
captureValues(transitionValues);
}
//获取结束的场景的对应的背景色
@Override
public void captureEndValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
//创建动画器用于应用变换
@Override
public Animator createAnimator(ViewGroup sceneRoot,
TransitionValues startValues, TransitionValues endValues) {
if (null == startValues || null == endValues) {
return null;
}
final View view = endValues.view;
Drawable startBackground = (Drawable) startValues.values.get(PROPNAME_BACKGROUND);
Drawable endBackground = (Drawable) endValues.values.get(PROPNAME_BACKGROUND);
if (startBackground instanceof ColorDrawable && endBackground instanceof ColorDrawable) {
ColorDrawable startColor = (ColorDrawable) startBackground;
ColorDrawable endColor = (ColorDrawable) endBackground;
if (startColor.getColor() != endColor.getColor()) {
ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(),
startColor.getColor(), endColor.getColor());
animator.setDuration(3000);
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;
}
}
首先我们要创建一个继承Transition
的抽象类,然后要实现3个方法:
public void captureStartValues(TransitionValues transitionValues):获取初始场景的属性,在这个方法中我们可以取得我们关注的控件的属性值,用于以后的变化。
public void captureEndValues(TransitionValues transitionValues):获取结束场景的属性,这个是我们最终要显示的值。
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues):创建动画器,这个方法的最关键的,在这个方法里面我们可以定义任何我们想要的动画生成器,然后返回给TransitionFramework使用,从而达到我们想要的动画效果。
最后就是将该Transition应用到变换中,直接调用如下代码即可:
mTransition = new ChangeColor();
TransitionManager.go(mScenes[mCurrentScene], mTransition);
将Transition应用的Activity切换中可以实现酷炫的效果,但是这个要在api>=21的系统上才可以看到效果。同样,我们先看看效果:
从效果图上看,从列表到详情的切换非常自然优雅,好像是在同一Activity当中,但其实这是两个不同的Activity。看一下具体的实现过程:
两个Activity的布局,一个是GridView列表,一个是ImageView加TextView,没啥特别的。我们主要关注Activity的切换过程。
列表Activity的操作
在点击列表中的一项时触发`onItemClick()方法,该方法的实现代码如下:
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Item item = (Item) adapterView.getItemAtPosition(position);
// 正常创建Intent
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra(DetailActivity.EXTRA_PARAM_ID, item.getId());
//关键代码,创建场景变化动画,ActivityOptionsCompat可以兼容api > 4的系统
//但是在api<21的系统中和正常发起Activity一样,没有特殊的效果
ActivityOptionsCompat activityOptions = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
//创建要变换的初始控件和目标名称的映射
//该映射会在被调起的Activity中用到
new Pair<View, String>(view.findViewById(R.id.imageview_item),
DetailActivity.VIEW_NAME_HEADER_IMAGE),
new Pair<View, String>(view.findViewById(R.id.textview_name),
DetailActivity.VIEW_NAME_HEADER_TITLE));
ActivityCompat.startActivity(this, intent, activityOptions.toBundle());
}
详情Activity的操作
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.details);
mItem = Item.getItem(getIntent().getIntExtra(EXTRA_PARAM_ID, 0));
mHeaderImageView = (ImageView) findViewById(R.id.imageview_header);
mHeaderTitle = (TextView) findViewById(R.id.textview_title);
//设置要变换到的控件和名称的映射
//通过名称,我们可以和列表Activity的要变换的空间对应起来
//从而对其使用动画效果
ViewCompat.setTransitionName(mHeaderImageView, VIEW_NAME_HEADER_IMAGE);
ViewCompat.setTransitionName(mHeaderTitle, VIEW_NAME_HEADER_TITLE);
loadItem();
}
这样,我们就完成了Activity的切换的场景变换。除此之外,我们还可以监听这种变化的开始和结束,从而执行一些额外的操作。
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private boolean addTransitionListener() {
//获取变换对象
final Transition transition = getWindow().getSharedElementEnterTransition();
if (transition != null) {
// 添加监听器
transition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionEnd(Transition transition) {
// 下载高清图片
loadFullSizeImage();
// 取消监听
transition.removeListener(this);
}
@Override
public void onTransitionStart(Transition transition) {
// No-op
}
@Override
public void onTransitionCancel(Transition transition) {
transition.removeListener(this);
}
@Override
public void onTransitionPause(Transition transition) {
// No-op
}
@Override
public void onTransitionResume(Transition transition) {
// No-op
}
});
return true;
}
return false;
}
到此为止,场景变换的基本知识就讲完了,有了这些基本知识,我们可以举一反三,设计出非常好的场景变换效果,提升APP使用体验。