本文翻译自:Animate all the things. Transitions in Android
让所有的一切都动起来
谷歌在Material Design有这样一个阐述:动画不再仅是iOS才有。这里有一种新概念叫Material motion。
Motion提供了另外一层含义:让呈现在用户面前的对象没有被破坏连贯性,即使它们改变或者重组。在Material Design中的Motion被用来描述空间的关系、功能和意图。
事实上,创建动画的过程是需要时间的。但只需要调用setVisibily(View.VISIBLE)和将业务逻辑移到你的最棒的新特性中,因为它已经超越了所有的界限。但是记住:每一次你忽略了添加有意义的UI过渡,一个可悲的设计师就将出现在这个世界上。
假使我告诉你动画比你想象的需要的工作更少,你会怎么样?你曾听说过Transitions API吗?是的,它是谷歌对Activity之间奇幻的动画所推出的。不幸的是,这仅适用于5.0以上的系统。所以,没有人实际使用过它。但是想象这种API能够被高效地使用在不同的情况中, 甚至更令人激动的是,能够适用在Android旧版本中。
让我们从一些历史开始
在4.0中引入了对ViewGroup新的animateLayoutChange参数。但是,即使调用getLayoutTransition()和配置一些东西在它里面,它仍然是不稳定的和不足够灵活。所以你用它不能做太多事情。
版本4.4Kitkat带来了场景(Scenes)和过渡(Transitions)观念。场景在技术上是场景根(布局容器)中所有视图的状态。过渡是将被用于视图来执行从一个场景到另一个场景平滑过渡的一系列动画。这些例子会更吸引人吗?当然。
想象有一个按钮
在点击之后,想要一行文本出现在按钮下面。这里是布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/transitions_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DO MAGIC"/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Transitions are awesome!"
android:visibility="gone"/>
</LinearLayout>
在Java代码中有一个点击监听器:
final ViewGroup transitionsContainer = (ViewGroup) view.findViewById(R.id.transitions_container);
final TextView text = (TextView) transitionsContainer.findViewById(R.id.text);
final Button button = (Button) transitionsContainer.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
boolean visible;
@Override
public void onClick(View v) {
TransitionManager.beginDelayedTransition(transitionsContainer);
visible = !visible;
text.setVisibility(visible ? View.VISIBLE : View.GONE);
}
});
运行效果:
还不错。仅仅一行代码就实现了动画。
我们还通过beginDelayedTransition方法的第二个参数来指定明确被运用的过渡类型。
简单过渡类型
- ChangeBounds 改变视图的位置和大小
- Fade 继承自Visibility类,执行最流行的动画-渐入渐出
- TransitionSet 一系列过渡动画集合。它们可以混合一起开始执行,也可以按次序执行,只需要通过调用setOrdering方法来设置。
- AutoTransition 它是一个TransitionSet,包含了Fade out,ChangeBounds和Fade in,且顺序是次序的。首先,视图不存在;其次,场景渐出;然后,改变位置和大小;最后,新的视图渐入。AutoTransition 是默认使用的过渡,不用指定beginDelayedTransition方法的第二个参数。
兼容
每个人都想写一个在每个Android版本上都一致的实现。有希望的是我们仍然使用Transitions API时可以实现它。前不久,我发现了两个十分相似的库:
https://github.com/guerwan/TransitionsBackport
https://github.com/pardom/TransitionSupportLibrary
它们已经不再被维护了,且它们都丢失了一些仍然可以兼容的东西。因此,基于它们我创建了我自己的库,而且增加了很多新的东西来兼容旧的Android版本。从Lollipop到Marshmallow,所有的API改变也已经被合并进来了。
因此,它就是Transitions-Everywhere,可以兼容Android 4.0以上的Transitions API,使用它只需要指定gradle依赖:
dependencies {
compile "com.andkulikov:transitionseverywhere:1.7.0"
}
对于所有相关的类,只需将导入的包android.transition.*
替换为com.transitionseverywhere.* 就可以了。
谷歌刚发布了Transitions框架的支持库(Support Library),我写了一篇关于它的文章article to compare it with my library,请查看。
我们还可以做什么
首先,我们可以在过渡里面修改动画的时间、插值器和启动延迟:
transition.setDuration(300);
transition.setInterpolator(new FastOutSlowInInterpolator());
transition.setStartDelay(200);
接下来看看其它可用的过渡类型。
Slid
与渐变过渡相似,继承自Visibility类。它帮助场景里的视图从一边滑到另外一边。下面是 Slide(Gravity.RIGHT)的例子:
Explode and Propagation
Explode非常像Slid,但是视图在滑动时会用基于过渡中心的一些计算方向(你应该为它提供setEpicenterCallback方法)。
TransitionPropagation为每一个动画绘画者计算启动延迟。例
如,Explode默认使用CircularPropagation。动画延迟取决于视图和中心的距离。要使用它,在Transition中调用setPropagation。
让我们看看,我们有一个GridLayoutManager的RecyclerView,而且我们想要在点击任何一个指定的元素之后删除所有的元素。像这样:
public void onClick(View clickedView) {
// save rect of view in screen coordinates
final Rect viewRect = new Rect();
clickedView.getGlobalVisibleRect(viewRect);
// create Explode transition with epicenter
Transition explode = new Explode()
.setEpicenterCallback(new Transition.EpicenterCallback() {
@Override
public Rect onGetEpicenter(Transition transition) {
return viewRect;
}
});
explode.setDuration(1000);
TransitionManager.beginDelayedTransition(recyclerView, explode);
// remove all views from Recycler View
recyclerView.setAdapter(null);
}
ChangeImageTransform
ChangeImageTransform将会改变图片矩阵。它运用的场景是在当改变ImageView的scaleType时候。在大多数情况下,你想要与ChangeBounds配对使用来改变位置,大小和scaleType。
TransitionManager.beginDelayedTransition(transitionsContainer, new TransitionSet()
.addTransition(new ChangeBounds())
.addTransition(new ChangeImageTransform()));
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) imageView.getLayoutParams();
params.width = expanded ? FrameLayout.LayoutParams.MATCH_PARENT :
FrameLayout.LayoutParams.WRAP_CONTENT;
params.height = expanded ? FrameLayout.LayoutParams.MATCH_PARENT :
FrameLayout.LayoutParams.WRAP_CONTENT;
imageView.setLayoutParams(params);
imageView.setScaleType(expanded ? ImageView.ScaleType.CENTER_CROP :
ImageView.ScaleType.FIT_CENTER);
expanded = !expanded;
Path (Curved) motion
真实世界的力量,如重力,激发元素的运动是沿着弧线而不是直线。
每一个过渡都操作两个坐标(例如:ChangeBounds改变视图的位置),我们可以通过setPathMotion方法来运用曲线运动。
ChangeBounds changeBounds = new ChangeBounds();
ArcMotion arcMotion = new ArcMotion();
arcMotion.setMinimumHorizontalAngle(15f);
arcMotion.setMinimumVerticalAngle(0f);
arcMotion.setMinimumVerticalAngle(90f);
changeBounds.setPathMotion(new ArcMotion());
changeBounds.setDuration(500);
TransitionManager.beginDelayedTransition(transitionsContainer,
changeBounds);
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams();
params.gravity = isReturnAnimation ? (Gravity.LEFT | Gravity.TOP) :
(Gravity.BOTTOM | Gravity.RIGHT);
v.setLayoutParams(params);
isReturnAnimation = !isReturnAnimation;