我们如何为 Android 开发 Guillotine 菜单动画

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

你一定读过我们的 故事 ,讲述了我们的设计师 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 开发人员]