构建一个动画的汉堡按钮

优质
小牛编辑
136浏览
2023-12-01

汉堡按钮和滑出式菜单可能是整个产业中最两极分化的界面元素。苹果的狂热支持者反对汉堡按钮和相应的滑出式菜单,说设计师(以及工程师、产品经理和CEO们)喜欢在那堆积尽可能多的东西,因为你有了很多垂直地空间。

我不能说我不认同,因为用户测试表明用户其实不太使用滑出式菜单,但可能我是一个伪君子,因为我还是在我的iPhone app Interesting中使用了一个汉堡按钮,这样看来我也是一个问题!不论如何,如果你打算使用一个汉堡按钮,你也要让它有趣、讨喜来让人们点击。

所以一个汉堡按钮的基本元素是什么?典型的是有三个水平栏来描绘常规状态,然后如果你想要精致一点的话,你可以在菜单打开时将栏换成X形。当然了,Pop就是用来让用户界面开发师变得精致的,所以为什么不给这个过渡加上一些动画呢?

稍微看一下我们要构建的是什么。

开始时,我们有一个圆形的黑色按钮,里面中间有一个汉堡形的线。当按钮被点击时,它动画到一个稍微小一点的尺寸。但点击结束时,线会动画城红色的X。当点击X状态时,动画会回到原始的颜色和位置。这是一个明显简化的关于发生了什么的解释,让我们来看看代码。

// 将汉堡按钮添加到屏幕上
self.hamburgerButton = [DTCTestButton buttonWithType:UIButtonTypeCustom];
[self.hamburgerButton addTarget:self action:@selector(didTapHamburgerButton:)
    forControlEvents:UIControlEventTouchUpInside];
self.hamburgerButton.backgroundColor = [UIColor blackColor];
[self.hamburgerButton setFrame:CGRectMake(200, 200, 150, 150)];
self.hamburgerButton.layer.cornerRadius = 75;
[self.window addSubview:self.hamburgerButton];

我们将汉堡按钮设为类的@property,这样我们就可以通过self.hamburgerButton来调用它。它使用了我们在之前的例子里创建的同样的按钮子类,这样我们就可以在用户点击时立即获取好的有弹性的感觉。我们还设置按钮在用户松开他们点击按钮的手指时的事件UIControlEventTouchUpInside下调用我们的方法 -didTapHamburgerButton: 。我们还将按钮设为黑色的并且有圆角。

这里是我们目前有的样子。

该把我们的汉堡线作为子视图添加到按钮上了。

CGFloat sectionWidth = 80;
CGFloat sectionHeight = 11;

// 添加上、中、下汉堡线
self.top = [[UIView alloc] initWithFrame:
    CGRectMake(self.hamburgerButton.bounds.size.width/2 - sectionWidth/2,
    40, sectionWidth, sectionHeight)];
self.top.backgroundColor = [UIColor whiteColor];
self.top.userInteractionEnabled = NO;
self.top.layer.cornerRadius = sectionHeight/2;
[self.hamburgerButton addSubview:self.top];

self.middle = [[UIView alloc] initWithFrame:
    CGRectMake(self.hamburgerButton.bounds.size.width/2 - sectionWidth/2,
    69, sectionWidth, sectionHeight)];
self.middle.backgroundColor = [UIColor whiteColor];
self.middle.userInteractionEnabled = NO;
self.middle.layer.cornerRadius = sectionHeight/2;
[self.hamburgerButton addSubview:self.middle];

self.bottom = [[UIView alloc] initWithFrame:
    CGRectMake(self.hamburgerButton.bounds.size.width/2 - sectionWidth/2,
    99, sectionWidth, sectionHeight)];
self.bottom.backgroundColor = [UIColor whiteColor];
self.bottom.userInteractionEnabled = NO;
self.bottom.layer.cornerRadius = sectionHeight/2;
[self.hamburgerButton addSubview:self.bottom];

我设置了一些我们会在这个代码中重复用到的CGFloat的数字变量。我添加了三个UIView对象到主汉堡按钮上,每个都是白色背景的圆角矩形。它们都放置在大汉堡按钮的水平中心,并在垂直方向上分离。这段代码中最有趣的地方在于我设置这些每个视图的UserInteractionEnabled属性为NO。如果我们不对这些视图这样做,如果直接点击按钮,会吞没触摸事件并且不会传递到实际的完整汉堡按钮上。这里是现在看起来的样子。

现在不添加任何代码,因为这个按钮是我们在之前的例子中创建的UIButton子类DTCTestButton类型的,已经有了一些动画了。

我们现在真正想做的是让线动画交叉变成X。所以让我们进入我们的 didTapHamburgerButton: 方法,我们每次点击这个按钮都会调用它,来看一看我们要做什么。

- (void)didTapHamburgerButton:(id)sender {	
    if (self.hamburgerOpen) {
        self.hamburgerOpen = NO;
        // 添加把X变回三条线的动画
    } else {
        self.hamburgerOpen = YES;
        // 添加把三条线变成X的动画
    }
}

我们需要一种方式来记录按钮是否被动画成X了(如果是一个完整的app,也就是滑出式菜单是否被推出了),所以我天界了一个@property(BOOL)hamburgerOpen到类上,这样我们就可以每次都设置它并且知道按钮当前的状态。这是我们在这个方法中做任何事情前都应该先检查的变量,因为它的值会指示我们需要执行何种类型的动画。

让我们从初始状态开始,也就是self.hamburgerOpen是false,并且代码会从上面的else开始执行。在进入实际的代码之前,让我们讨论一个计划来将三条水平线变成红色的X。

  1. 我们要将顶部的线向下旋转到45度角
  2. 我们要将底部的线向上旋转45度角
  3. 我们不需要中间的线所以就直接淡出它
  4. 旋转后的线可能不会很好地交叉,所以我们要动画它们到准确的位置
  5. 将两根交叉的线从白色动画到红色

如果你注意了,可能会意识到我们有很多动画要执行,你是对的。这不是一个不重要的例子,它由多个单独的动画组成,但如大多数动画代码一样,它会一步一步执行。我们一直一次只写一个动画block,除了这次有很多动画!让我们先从淡出中间行开始。

// 淡出中间行
[UIView animateWithDuration:0.2 animations:^{
    self.middle.alpha = 0.0f;
}];

只是一个简单的基于block的UIView动画。这个淡出动画的目标是让中间行消失,所以我们不需要做任何其他的事情。嗷,我应该提一下,我将顶部、中间和底部的线都作为类的@property了,这就是为什么我们可以用self.前缀获取这个变量。

接下来,让我们把省下来的两根线从白色动画成红色。幸运的是,Pop让它变得很简单,你只需要设置toValue的颜色为你最终想要的颜色,它会自动插入中间的颜色。

// 将顶部和顶部线的颜色变为红色
POPSpringAnimation *topColor = [self.top pop_animationForKey:@"topColor"];

if (topColor) {
    topColor.toValue = [UIColor redColor];
} else {
    topColor =
        [POPSpringAnimation animationWithPropertyNamed:kPOPViewBackgroundColor];
    topColor.toValue = [UIColor redColor];
    topColor.springBounciness = 0;
    topColor.springSpeed = 18.0f;
    [self.top pop_addAnimation:topColor forKey:@"topColor"];
}

POPSpringAnimation *bottomColor = [self.bottom pop_animationForKey:@"bottomColor"];

if (bottomColor) {
    bottomColor.toValue = [UIColor redColor];
} else {
    bottomColor =
        [POPSpringAnimation animationWithPropertyNamed:kPOPViewBackgroundColor];
    bottomColor.toValue = [UIColor redColor];
    bottomColor.springBounciness = 0;
    bottomColor.springSpeed = 18.0f;
    [self.bottom pop_addAnimation:bottomColor forKey:@"bottomColor"];
}

就如我们之前的按钮例子一样,当我们重复一个用户动作时,我们需要确保我们的动画时流动的,即使用户疯狂地快速点击按钮并打断我们的动画。从当前值开始动画非常重要,这样一切就是自然的。这就是为什么我在创建并添加新动画前做了一个topColor和bottomColor动画对象是否已经存在的检查。如果它们存在,我们就使用存在的动画并且只设置一个新的toValue,如果不存在,我们就构建一个新的动画对象。还有,我对这个颜色过渡没有使用任何弹性,因为我确实不想颜色动画过度迭代红色然后变成一些奇怪的颜色。

这时候当用户点击按钮时我们还没有X,但已经有了这个可爱的视觉了。

我们现在还剩两个动画,但它们比较大,需要一些思考。我们需要将顶部的线顺时针旋转45度(所以右边向下倾斜),然后我们需要底部的线逆时针旋转45度(所以右边向上倾斜)。逆时针旋转意味着我们需要旋转一个负值,所以是-45度。当然了,动画不会接受度数值,它们需要角度值,45度在角度上是π/4。来做一些旋转动画。

// 旋转顶部的线来构成X
POPSpringAnimation *topRotate =
    [self.top.layer pop_animationForKey:@"topRotate"];

if (topRotate) {
    topRotate.toValue = @(-M_PI/4);
} else {
    topRotate =
        [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
    topRotate.toValue = @(-M_PI/4);
    topRotate.springBounciness = 11;
    topRotate.springSpeed = 18.0f;
    [self.top.layer pop_addAnimation:topRotate forKey:@"topRotate"];
}

// 旋转底部的线来构成X
POPSpringAnimation *bottomRotate =
    [self.bottom.layer pop_animationForKey:@"bottomRotate"];

if (bottomRotate) {
    bottomRotate.toValue = @(M_PI/4);
} else {
    bottomRotate =
        [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
    bottomRotate.toValue = @(M_PI/4);
    bottomRotate.springBounciness = 11;
    bottomRotate.springSpeed = 18.0f;
    [self.bottom.layer pop_addAnimation:bottomRotate forKey:@"bottomRotate"];
}

Pop的旋转动画时在layer上操作的(看到kPOPLayerRotation了没),所以我们将动画添加到支撑这些视图的layer上。

我们向上旋转一根线、向下旋转一根线所以它们应该在中间交叉,对吗?让我们看看我们得到了什么。

额,直观地说,这可能并不是你期待的样子。旋转动画让线条变成这样的原因是没跟线条都是围绕着它们layer的中心旋转的。所以这些视图会像跷跷板一样旋转,而不是我们想要的在中间交叉的样子。我们可以改变layer旋转的锚点,但这有点麻烦,因为这样做会重定位layer并且我们需要调整框架,这纯粹是找麻烦。所以,更简单的做法是,我们可以就将顶部线下移一点,然后将底部的线上移一旦,然后重叠它们。

// 重定位顶部的线到按钮的中间
POPSpringAnimation *topPosition =
    [self.top.layer pop_animationForKey:@"topPosition"];

if (topPosition) {
    topPosition.toValue = @(29);
} else {
    topPosition =
        [POPSpringAnimation animationWithPropertyNamed:kPOPLayerTranslationY];
    topPosition.toValue = @(29);
    topPosition.springBounciness = 0;
    topPosition.springSpeed = 18.0f;
    [self.top.layer pop_addAnimation:topPosition forKey:@"topPosition"];
}

// 重定位底部的线到按钮的中间
POPSpringAnimation *bottomPosition =
    [self.bottom.layer pop_animationForKey:@"bottomPosition"];

if (bottomPosition) {
    bottomPosition.toValue = @(-29);
} else {
    bottomPosition =
        [POPSpringAnimation animationWithPropertyNamed:kPOPLayerTranslationY];
    bottomPosition.toValue = @(-29);
    bottomPosition.springBounciness = 0;
    bottomPosition.springSpeed = 18.0f;
    [self.bottom.layer pop_addAnimation:bottomPosition forKey:@"bottomPosition"];
}

经过一些测试和试错,我决定将顶部的线下移29像素,底部的线上移29像素,这样会让它们重合的最好。你也可以做一些三角几何计算来得出这个值。我们使用kPOPLayerTranslationY动画来让两根线旋转到按钮中间的X。

完成了!很好吧?现在,当你点击按钮,它会将三根线变成两根线,但当用户再次点击时会发生什么呢?这时候,不会发生任何事情,因为我们没有实现任何其他条件分支的逻辑来将X变回三根线。幸运的是,我们可以很简单地复制粘贴动画,但是要将toValue值改为初始值。比如说,我们需要将两根线都旋转回0度,记得要移动29像素,并将它们的颜色改回白色。还有要将中间的线淡入回100%不透明。这样就全部完成了,我们得到了一个漂亮的汉堡按钮。