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

更新时间:

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

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3700+ 小伙伴加入学习 ,欢迎点击围观

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

最新发布