你一定读过我们的 故事 ,讲述了我们的设计师 Vitaly Rubtsov 和 iOS 开发人员 Maksym Lazebnyi 如何创建一个非常规的顶部栏动画,它得到了一个不祥的名字——断头台菜单(你可以在 Dribbble 和 GitHub 上看到 iOS 动画 )。不久之后,我们的 Android 开发人员 Dmytro Denysenko 接受了在 Android 平台上实现相同动画的挑战(在 GitHub 上查看)。他甚至无法预知他必须面对什么困难,以及他必须潜入多深的深度寻找解决方案。
从哪儿开始?
起初,我想求助于标准解决方案来实现 Android 组件。毕竟,乍一看这似乎是可能的。我计划使用 ObjectAnimation 来实现导航视图的旋转。我还想添加一个默认的 BounceInterpolator ,实现菜单碰撞到屏幕左边框时的回弹效果。然而, BounceInterpolator 似乎使反弹过于强大,就好像它是足球的反弹,而不是金属断头台的反弹。
默认的 BounceInterpolator 不提供任何自定义参数,所以我别无选择,只能编写自己的插值器。它不仅应该添加反弹,而且还可以创建自由落体加速效果,使动画看起来更自然。
我们动画中的 Guillotine 组件由 Guillotine 的旋转、Guillotine 的反弹和操作栏的反弹组成。更重要的是,我使用了两个自定义插值器来实现自由落体加速和反弹的效果。现在是时候引导您完成开发过程了。
我们如何实现 Guillotine 菜单的轮换
我需要做两件事来旋转动画:找到旋转中心,并实现 ObjectAnimation 来进行实际旋转。
在计算旋转中心之前,我必须将布局放在屏幕上。
private void setUpOpeningView(final View openingView) {
if (mActionBarView != null) {
mActionBarView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mActionBarView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mActionBarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
mActionBarView.setPivotX(calculatePivotX(openingView));
mActionBarView.setPivotY(calculatePivotY(openingView));
}
});
}
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2;
}
之后,我只需要添加几行代码:
private void setUpOpeningView(final View openingView) {
if (mActionBarView != null) {
mActionBarView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mActionBarView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mActionBarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
mActionBarView.setPivotX(calculatePivotX(openingView));
mActionBarView.setPivotY(calculatePivotY(openingView));
}
});
}
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2;
}
旋转中心实际上是汉堡包的中心。动画需要两个汉堡包:一个在主操作栏上,另一个在 Guillotine 布局上。为了使动画看起来流畅,两个汉堡包必须相同并使用相同的坐标。为此,我刚刚在工具栏(您看不到)上创建了一个汉堡包,并将其与 Guillotine 菜单汉堡包的中心对齐。
我们如何实现自由落体和反弹
为了实现 iOS 的 Guillotine 菜单动画,我的同事 Maksym Lazebnyi 使用了 默认的 UIDynamicItemBehavior 类,该类是在弹性和阻力属性的帮助下定制的。然而,这在 Android 上并不那么容易。
[标准安卓插值器]
正如我之前提到的,我可以使用默认的 BounceInterpolator 进行布局旋转,但它似乎会产生太软的 反弹(就好像我们的断头台是一个球一样)。这就是我尝试实现自定义插值器的原因。它应该为动画添加加速。
插值率在 0 到 1 之间变化。在我的例子中,旋转角度从 0° 到 90°(顺时针)。这意味着在 0° 角度时,插值率也将为“0”(初始位置),而当角度等于 90° 时,插值率将为“1”(最终位置)。
由于我们的插值具有 二次相关性,因此它允许反弹和自由落体,就像 Vitaly 的动画屏幕截图 一样。我不得不回想起我的高中数学课程来构建自定义插值器。经过短暂的头脑风暴,我拿了一本字帖,画了一张函数图,说明了对象的属性相对于时间的依赖性。
[自定义插值器]
我写了三个符合图表的二次方程。
private void setUpOpeningView(final View openingView) {
if (mActionBarView != null) {
mActionBarView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mActionBarView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mActionBarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
mActionBarView.setPivotX(calculatePivotX(openingView));
mActionBarView.setPivotY(calculatePivotY(openingView));
}
});
}
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2;
}
我们如何实现操作栏的 反弹
现在,当我们的 Guillotine 菜单与屏幕的左边框碰撞时,它可能会掉落并反弹 。但是,我还必须执行一次反弹。当 Guillotine 菜单回到初始状态时,它会与操作栏发生碰撞,产生弹跳效果。为此,我需要另一个插值器。
此处图形以 0° 角开始和终止 ,但二次相关性建立在与前一种情况相同的原则上。
private void setUpOpeningView(final View openingView) {
if (mActionBarView != null) {
mActionBarView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mActionBarView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mActionBarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
mActionBarView.setPivotX(calculatePivotX(openingView));
mActionBarView.setPivotY(calculatePivotY(openingView));
}
});
}
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2;
}
结果,我们得到了三个 ObjectAnimation 实例:Guillotine 的打开和关闭、action bar 的旋转,以及两个插值器:Guillotine 的下落和 action bar 的反弹。之后我需要做的就是将插值设置为适当的动画,在关闭菜单后立即开始操作栏的回弹,并通过点击适当的汉堡包来绑定动画的开始。
private void setUpOpeningView(final View openingView) {
if (mActionBarView != null) {
mActionBarView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mActionBarView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
mActionBarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
mActionBarView.setPivotX(calculatePivotX(openingView));
mActionBarView.setPivotY(calculatePivotY(openingView));
}
});
}
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2
}
private float calculatePivotY(View burger) {
return burger.getTop() + burger.getHeight() / 2;
}
就是这样。制作动画是一个相当大的挑战,但完全值得!现在,我们流畅的 Guillotine 菜单可用于 iOS 和 Android 两个平台。
另请阅读: 我们如何为 Android 创建 FlipViewPager 动画
计划的功能
我打算为 Guillotine 菜单动画添加一些新效果。它们包括滑动过渡、从右到左的布局支持和水平布局方向。请继续关注我们的更新。
您可以在此处找到项目示例及其设计:
[Dmytro Denysenko,Yalantis 的 Android 开发人员]