汉堡按钮在 UI 设计中早已不是什么新鲜玩意儿了,但是某天我突然在 dribbble 上看到了这个酷炫的动画效果,效果真是棒棒哒!于是我决定把它在代码里实现一下。
先来看下 CreativeDash 团队做出来的原始动画效果:
我们可以看到 (看得我眼睛都花了),汉堡按钮 (也就是三条横线的那个)的上下两条线分别绕着自身最优的端点旋转了45°,变成了按钮里的 X ,中间的那个线则摇身一变,成了外面的圈圈。这个效果可以用 CAShapeLayer
实现,但是首先,我需要为这三条直线分别创建一个 CGPath
。
我们可以这样手动创建一个短短的直线:
let shortStroke: CGPath = {
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 2, 2)
CGPathAddLineToPoint(path, nil, 28, 2)
return path
}()
不过对于中间那个线,因为它有比较复杂的运动轨迹,所以我用 Sketch 创建了一条路径,把动画融入了线条中:
接下来我把图片导出成了 SVG 格式,然后通过 PaintCode 1 把图片转换成代码片段,导入项目中。接下来我重写了导入的代码片段,并且创建了想要的 CGPath 对象:
let outline: CGPath = {
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 10, 27)
CGPathAddCurveToPoint(path, nil, 12.00, 27.00, 28.02, 27.00, 40, 27)
CGPathAddCurveToPoint(path, nil, 55.92, 27.00, 50.47, 2.00, 27, 2)
CGPathAddCurveToPoint(path, nil, 13.16, 2.00, 2.00, 13.16, 2, 27)
CGPathAddCurveToPoint(path, nil, 2.00, 40.84, 13.16, 52.00, 27, 52)
CGPathAddCurveToPoint(path, nil, 40.84, 52.00, 52.00, 40.84, 52, 27)
CGPathAddCurveToPoint(path, nil, 52.00, 13.16, 42.39, 2.00, 27, 2)
CGPathAddCurveToPoint(path, nil, 13.16, 2.00, 2.00, 13.16, 2, 27)
return path
}()
可能有第三方的类库可以让你直接把 SVG 转换成 CGPath,但是对于这种简单的图像,没必要杀鸡用牛刀。
在我自定义的 UIButton
的子类里,我添加了三个 CAShapeLayer
属性并且把他们和前面定义的路径关联了起来:
self.top.path = shortStroke
self.middle.path = outline
self.bottom.path = shortStroke
然后设置一下相关的属性:
layer.fillColor = nil
layer.strokeColor = UIColor.whiteColor().CGColor
layer.lineWidth = 4
layer.miterLimit = 4
layer.lineCap = kCALineCapRound
layer.masksToBounds = true
为了正确的计算出边框 (bounds
) ,我需要对这些线条的大小进行计算。谢天谢地,用CGPathCreateCopyByStrokingPath
这个函数可以创建一条和原来线条一样的路径,所以它的 bounds
和参数里那个CAShapeLayer
类型的内容完全一致:
let boundingPath = CGPathCreateCopyByStrokingPath(layer.path, nil, 4, kCGLineCapRound, kCGLineJoinMiter, 4)
layer.bounds = CGPathGetPathBoundingBox(boundingPath)
因为汉堡按钮的上下两个线条接下来会绕着最右点旋转,所以在布局图层的时候,我们需要相应的设置它的锚点 (anchorPoint
):
self.top.anchorPoint = CGPointMake(28.0 / 30.0, 0.5)
self.top.position = CGPointMake(40, 18)
self.middle.position = CGPointMake(27, 27)
self.middle.strokeStart = hamburgerStrokeStart
self.middle.strokeEnd = hamburgerStrokeEnd
self.bottom.anchorPoint = CGPointMake(28.0 / 30.0, 0.5)
self.bottom.position = CGPointMake(40, 36)
接下来就是动画的部分了。当按钮的状态改变的时候,三条直线应该会动态的移动到新的位置。对于那两个外围的直线而言,实现起来很容易。比如最上面的那条线,我先把它往里移动 4 points 让它位于中心位置,然后再逆时针旋转45度,形成了半个 X :
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, -4, 0, 0)
transform = CATransform3DRotate(transform, -M_PI_4, 0, 0, 1)
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
animation.toValue = NSValue(CATransform3D: transform)
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.8, 0.75, 1.85)
animation.beginTime = CACurrentMediaTime() + 0.25
self.top.addAnimation(animation, forKey: "transform")
self.top.transform = transform
最下面的那条线的部分交给读者朋友们作为练习。
接下来,中间的那条直线似乎有点棘手。为了实现理想的效果,我们需要给 CAShapeLayer
的起点和终点分别设置动画效果。
首先我需要知道这两个属性在两种状态下的值分别是多少。注意,就算是在汉堡状态下,直线也没有从 O 开始。路径稍微的从左边延伸,我们可以稍候应用 timing
函数实现一个酷炫的效果:
let menuStrokeStart: CGFloat = 0.325
let menuStrokeEnd: CGFloat = 0.9
let hamburgerStrokeStart: CGFloat = 0.028
let hamburgerStrokeEnd: CGFloat = 0.111
接下来我们只需要创建动画并加到图层里即可:
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
strokeStart.fromValue = hamburgerStrokeStart
strokeStart.toValue = menuStrokeStart
strokeStart.duration = 0.5
strokeStart.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.4, 0.5, 1)
self.middle.addAnimation(strokeStart, forKey: "strokeStart")
self.middle.strokeStart = menuStrokeStart
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
strokeEnd.fromValue = hamburgerStrokeEnd
strokeEnd.toValue = menuStrokeEnd
strokeEnd.duration = 0.6
strokeEnd.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.4, 0.5, 1)
self.middle.addAnimation(strokeEnd, forKey: "strokeEnd")
self.middle.strokeEnd = menuStrokeEnd
把前面的工作整合一下,你可以看到如下的结果:
哈哈哈哈成功实现了,洒家真是太高兴了。你可以在这里下载源码。当然你也可以加我的小鸟。玩的开心~
原文地址