材料设计其实在上一篇图片和色彩之后,应该是已经结束了。但是后来又发现忘了动画,这是让材料设计焕发光彩的重点啊。所以又翻开了文档和 google,开始了检索和阅读。
谷歌在 api 19 中添加了 android.transition 这个包,用于优化安卓动画的体验。但是事实上,api 19 中 transition 中的类寥寥无几,大部分的类都是在 api 21 之后新添加的。那么有人会说了,现在的工程最少也要兼容到 api 19 吧?这样的类,并没有什么使用价值。
话糙理不糙,我还是先放上 github 上有几千 star 的兼容库链接:这是链接。
这位前辈是真厉害,据说谷歌工程师解决部分 transition 包中 bug 的解决方案都是直接从这里来的。虽然有现成的库可以使用,但是原来的东西还是需要会用。所以来一起看看吧。
今天,主要看一下这里最基础的 TransitionManager
google 文档开头就提出了三个类,TransitionManager 、Transition、 Scene。
根据英文名称不难看出各个类的基本作用:
- TransitionManager:动画的管理类,其中封装了 Transition 和 Scene
- Scene:场景,它记录了 ViewTree 的某个时刻的关键帧,它通常作为动画的起始帧和最终帧使用
- Transition:过渡,它代表了这个动画的过渡方式,包括渐变透明(Fade)、滑动(Slide) 等等
介绍就到这里,如果需要更多参考信息,可以移步 google 文档。
一、TransitionManager API
去除参数,只看方法名一共有以下几种:
方法名 | 作用 | 备注 |
---|---|---|
beginDelayedTransition | 以当前帧为起始帧,直到下一次绘制后为结束帧,补齐中间的过渡动画 | Convenience method to animate to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame. |
endTransitions | 结束所有过渡动画 | Ends all pending and ongoing transitions on the specified scene root. |
go | 以当前帧为起始帧,传入参数为结束帧,补齐中间的过渡动画 | Convenience method to simply change to the given scene using the given transition. |
setTransition | 根据传入参数确认起始帧和结束帧,补齐中间的过渡动画 | Sets a specific transition to occur when the given pair of scenes is exited/entered. |
transitionTo | 以当前帧为起始帧,传入参数为结束帧,补齐中间的过渡动画 | using the appropriate transition for this particular scene change (as specified to the TransitionManager, or the default if no such transition exists) |
beginDelayedTransition 是一种特别方便,又好用的方法。
查看源码可以看到这个方法将会发生变化的 ViewGroup 缓存起来,并给它添加了 再次绘制的监听器:
public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
//debug 模式日志
if (Transition.DBG) {
Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
sceneRoot + ", " + transition);
}
//rootView 缓存
sPendingTransitions.add(sceneRoot);
if (transition == null) {
transition = sDefaultTransition;
}
//过渡方式
final Transition transitionClone = transition.clone();
//设置切换到当前场景的过渡
sceneChangeSetup(sceneRoot, transitionClone);
//设置 rootView 为当前场景
Scene.setCurrentScene(sceneRoot, null);
//添加绘制监听,在合适时机确认最终帧,并实现过渡
sceneChangeRunTransition(sceneRoot, transitionClone);
}
}
private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
final Transition transition) {
if (transition != null && sceneRoot != null) {
//封装了 OnAttachStateChangeListener 和 OnPreDrawListener
MultiListener listener = new MultiListener(transition, sceneRoot);
sceneRoot.addOnAttachStateChangeListener(listener);
sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
}
}
beginDelayedTransition 可以在代码中直接应用过渡, View 变换大小后,它会在下一次绘制的时候执行过渡,从而使得这个过程不那么突兀。
TransitionManager.beginDelayedTransition(mRootView);
ViewGroup.LayoutParams layoutParams = mSquareView.getLayoutParams();
layoutParams.height = newSize;
layoutParams.width = newSize;
mSquareView.setLayoutParams(layoutParams);
上面的例子是直接使用 LinearLayout 设置背景色获取的方块。当然普通 view 本身的大小变化也可以获得过渡效果,但是如果你使用的是自定义 View 可能你获得的过渡效果和想象的会有所不同。
这里我们用自定义 View 圆点视图为例,为了让效果更明显,我给这个 PointView 的容器添加了背景,获取效果为:
产生这样的效果,是因为默认的 AutoTransition 是 Fade 和 ChangeBound 的组合,其中大小变化由 ChangeBound 完成,它能达到的效果只对 ViewGroup 生效。
TransitionManager 后面的方法都与 Scene 相关,看得出 Scene 也是个很重要的类。因此,接下来让我们了解一下 Scene 的用法。
三、Scene 场景
文档阅读 : 链接
我们通常把 Scene 译为场景,这个翻译其实还是挺好的。一个过渡动画其实就是从一个场景到另一个场景的过渡,这里的两个场景我们取名为 起始帧 和 结束帧。而过渡动画,就是将场景中所有的子视图从起始帧移动到结束帧的运动效果。
1)创建 Scene 对象
根据文档,我们可以看到,一共有两种方式可以获取到我们需要的 Scene 对象。
//1.构造器
Scene(ViewGroup rootView)// 没有过渡信息,使用时需要自行添加过渡动画处理
Scene(ViewGroup rootView, View layout)//当这个场景进入时,会移除 rootView 中所有的子视图
//2.静态方法
getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)
其中对于构造器创建的 Scene 对象,需要注意的事项都已经备注在上面了,一般情况下,我们还是会使用静态方法获取。
2)一个例子
我们先来看一个例子:
这是我写的两个布局文件 scene1.xml 和 scene2.xml ,这个布局比较简单,所以也就不贴了,需要注意的是,这里只有一层 ViewGroup ,所有 ImageView 都在同一个层级下,且视图的 id 需要一一对应。
按钮代码:
mRootView = ((ViewGroup) findViewById(R.id.activity_scene_rootview));
((RadioGroup) findViewById(R.id.activity_scene_radiogroup)).setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
switch (checkedId) {
case R.id.activity_scene_radiobtn1:
Scene scene1 = Scene.getSceneForLayout(mRootView, R.layout.scene1, SceneActivity.this);
TransitionManager.go(scene1);
break;
case R.id.activity_scene_radiobtn2:
Scene scene2 = Scene.getSceneForLayout(mRootView, R.layout.scene2, SceneActivity.this);
TransitionManager.go(scene2);
break;
}
}
});
来看一下效果:
3)其他方法
在文档中,Scene 有 enter() 、exit() 、setEnterAction() 、 setExitAction()
enter 和 exit 方法就不多说了,在 TransitionManager 中,切换 Scene 的方法中也是调用了它们。关于后面两个方法,我没有找到合适使用他们的场景,但是文档说明中指出,这两个方法用于没有使用布局资源或层次结构定义的场景,或者在这些层次结构更改后需要执行附加步骤的场景。
有点抽象,之后如果有遇到合适的场景,再看吧。
四、Transition
上面是 google 文档的截图,可以看到 Transition 的子类非常丰富。实现不同接口的子类,组合出了多种多样的过渡效果。
1)介绍
Transition 类承载了切换到目标场景所有的动画效果,它的子类可以实现一组动画,也可以自定义实现动画。Transition 有两个核心任务:1.记录特定的属性;2.根据记录属性的变化执行过渡动画。
2)声明一个 TransitionSet 对象
TransitionSet 对象可以在 xml 文件中初始化,路径为:res/transition
例如我们创建一个带有 explode、changeBounds、changeTransform、changeClipBounds、changeImageTransform 的过渡动画。
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<explode/>
<changeBounds/>
<changeTransform/>
<changeClipBounds/>
<changeImageTransform/>
</transitionSet>
相应的,各个 Transition 有多种属性,这里就不再贴出,需要的时候可以浏览文档。
3)利用 TransitionManager 将动画应用到指定的 View 上
4)如果 explode、changeBounds 等自带的 Transition 并没有完成你需要的效果,那么你也可以用 transition 标签来声明:
<transition class="com.arno.CustomTransition"/>
CustomTransition 是一个自定义的动画。自定义动画和自定义 View 很像,它需要继承 Transition 类,并实现三个方法:
// 记录动画起始帧
public void captureStartValues(TransitionValues transitionValues)
// 记录动画终结帧
public void captureEndValues(TransitionValues transitionValues)
// 根据记录的起始帧和终结帧的属性,创建动画
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)
这里,我们简单实现一个改变布局高度的动画。
首先在captureStartValues
方法中获取动画起始高度属性:
@Override
public void captureStartValues(TransitionValues transitionValues) {
if (transitionValues == null) {
return;
}
transitionValues.values.put(VIEW_HEIGHT,transitionValues.view.getHeight());
}
然后在captureEndValues
中获取动画结束高度属性:
@Override
public void captureEndValues(TransitionValues transitionValues) {
if (transitionValues == null) {
return;
}
transitionValues.values.put(VIEW_HEIGHT,transitionValues.view.getHeight());
}
最后在createAnimator
中创建动画:
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
if (startValues == null || endValues == null) { return null;}
final View endView = endValues.view;
final int startHeight = (int) startValues.values.get(VIEW_HEIGHT);
final int endHeight = (int) endValues.values.get(VIEW_HEIGHT);
ValueAnimator sizeAnimator = ValueAnimator.ofInt(startHeight, endHeight);
sizeAnimator.setDuration(500);
sizeAnimator.setInterpolator(new LinearInterpolator());
sizeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int current = (int) valueAnimator.getAnimatedValue();
endView.getLayoutParams().height = current;
endView.requestLayout();
}
});
AnimatorSet set = new AnimatorSet();
set.play(sizeAnimator);
return set;
}
之后会再看看把 Share Element 和 Transition 结合,做页面跳转动画。
以上。