侧边栏动画

1,846 阅读6分钟

侧边菜单栏动画效果实现

2017/2/15 22:22 下午 comments

代码地址

SideMenuAnimation

用到的技术

  • CAGradientLayer
  • UITapGestureRecognizer
  • 关键帧动画
  • CATransform3DMakeRotation

怎么实现

定义协议

首先我们定义一个SideMenuAnimationProtocol的协议,用来获取需要执行动画的视图以及它们的父视图。

protocol SideMenuAnimationProtocol {
    func animationViews() -> [UIView]
    func parentView() -> UIView
}

搭建UI结构

在我们的例子中,我们有三个UIViewController,一个是最顶层的,叫RootViewController,第二个是SideMenuViewController,第三个是UINavigationControllerSideMenuViewControllerUINavigationController都是以 childViewController 的形式添加到RootViewController中的。这样我们就有了一个带侧边栏菜单的视图的基本模型了。

SideMenuViewController中,我们需要实现SideMenuAnimationProtocol协议,在animationViews的实现中,我们直接返回了UITableViewvisibleCells对象,这是用来执行动画的关键。

准备工作

我们定义了一个SideMenuAnimation的类用来实现真正的动画。为了使动画看起来更真实,我们会添加一些辅助的视图来达到我们要的效果。

首先我们有一个叫dimmingView的视图,是一个半透明的遮罩,当菜单打开的时候,用来覆盖在右边的视图上面,起到一个遮挡的效果。同时dimmingView也处理一个UITapGestureRecognizer事件,当用户点击的时候用来关闭菜单。dimmingView是被添加到RootViewController的视图里的。

我们还有一个叫shadowView的视图,是用来粘在SideMenuViewController的视图边上的,它是一个渐变的半透明图层,起到一个阴影的效果。这里我们使用了CAGradientLayer来实现颜色的渐变。shadowView也是添加到了根视图里面。

默认情况下,我们假设菜单是打开的,这时,dimmingView是覆盖在右边的视图上,它们俩的frame是一样的,shadowView是粘在SideMenuViewController的视图右边。

动画实现

因为菜单默认是打开的,所以我们先说说关闭动画。

通过前面的Gif图我们可以看到,动画可以分成以下两部分:

  • 左侧的菜单一个个沿着 Y 轴旋转出去
  • dimmingViewshadowView慢慢往左移动并且变成透明
改变anchorPoint

由于菜单的动画是沿着视图左侧的边旋转,我们需要调整layeranchorPoint。默认的anchorPoint值是(0.5, 0.5),我们需要将值改为(0.0, 0.5)才能符合我们的要求。当改变layeranchorPoint之后,视图的 frame也会跟着变化,所以我们同时还需要更改视图的frame来使视图待在原来的位置。具体原因可以参考 这里

self.animationViews = self.delegate.animationViews()
    
for view in self.animationViews {
    view.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
    view.frame = view.frame.offsetBy(dx: -0.5 * view.frame.width, dy: 0)
}
动画选择

由于左侧菜单视图的动画不是同一时间开始,每一个视图开始动画的时间都不一样,所以关键帧动画是一个比较好的选择,我们可以在不同的时机将动画添加进去。

简单动画

首先我们将dimmingViewshadowView和右侧视图的移动以及透明等简单动画放到一个关键帧里添加进去。这个动画的持续时间与主动画是一样长的,所以relativeDuration的值为0.1

UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
  self.dimmingView.center = self.dimmingView.center.offsetBy(dx: -self.viewWidth(), dy: 0)
  self.shadowView.center = self.shadowView.center.offsetBy(dx: -self.viewWidth(), dy: 0)
  self.rightView.center = self.rightView.center.offsetBy(dx: -self.viewWidth(), dy: 0)
                
  self.dimmingView.alpha = 0.0
  self.shadowView.alpha = 0.0
})
菜单动画

每个菜单执行的动画都是一样的,只是它们开始的时间不一样而已,菜单的动画如下:

CATransform3DMakeRotation(CGFloat(M_PI_2), 0.0, 1.0, 0.0)

上面的代码表示这是一个 3D 旋转动画,并且是沿着 Y 粥的。虽然我们知道需要旋转的角度是 90 度,但是我们如何知道是正的还是负的呢,这里教给大家一个技巧。从 Y 轴的正方向看过去,如果是顺时针则角度是正的,如果是逆时针,那么角度就是负的

接下来只需要将每个菜单视图的动画按顺序添加到关键帧里就可以了。那么视图动画在什么时候开始才是一个合适的值呢?

注意:在关键帧动画里用到的时间都是相对时间

计算动画时间

每个视图的动画的持续时间是一样的,只是开始时间不一样而已,当我们知道了动画的持续时间,我们就只需要计算动画的开始时间就可以了。

我们先定义一个叫interval的常量,把它的值设为0.6。这个值的意思是下一个动画会在上一个动画执行到 60% 的时候开始,我们可以通过改变这个值来使我们的动画看起来更舒服,0.6是我测试之后得出来的一个比较合适的值。

private func animationTime() -> TimeInterval {
     return 1.0 / (TimeInterval(self.animationViews.count - 1) * interval + 1)
}

假设动画时间是X,需要执行动画的总视图数为C,总时间是1。从第二个动画开始,每个动画都会有一段时间是与上一个动画重叠的,当我们把所有动画的时间相加并且减掉重叠部分就能得到总时间。

C * X - (C - 1) * (1 - interval) * X = 1

计算等式中X的值得到的结果就是上面代码中返回的值。

计算动画开始时间
startTime = (TimeInterval)(index) * interval * animationTime

通过上面的公式可以计算每个动画开始的时间。

执行动画
UIView.addKeyframe(withRelativeStartTime: startTime,
                               relativeDuration: animationTime,
                               animations: {
    view.layer.transform = transform
})

上面就是所有关于关闭菜单的动画的实现过程,打开菜单的动画相对来说就是一个逆的过程了。关于打开菜单的动画有两个地方需要注意:

  • 菜单的动画虽然是沿 Y 轴逆时针旋转 90 度,但是不要忘记在关闭动画的时候我们已经给了视图一个CATransform3DMakeRotation的变换,在打开的时候我们只需要将变换变味CATransform3DIdentity就可以了,如果按照上面说的方法逆时针旋转的话却达不到我们要的效果。
  • 由于在打开菜单的时候,第一个菜单会先停止动画,这时候dimmingViewshadowView等视图的简单动画还未结束,为了使动画看起来更漂亮,需要将这些视图的动画的持续时间缩短至与菜单的耽搁动画时间一样。

后记

在动画结束后还有一些细节的处理没有在文章中说明,开发者更具自己的需求可以在动画结束厚做一些相关的逻辑。在动画结束时最好能添加一个回掉给调用者,SideMenuAnimation里最好只处理与自己相关的动画逻辑,外部的逻辑还是需要调用者自己来完成,这样可以起到一个更好的封装效果。