看了 hamburger-button 这个的实现后,一直很喜欢,在此跟随着How to build a nice Hamburger Button transition in Swift 用 OC 实现一下。
通过看动画图,我们可以大致分析思路:
1、画出怎样的线和圈圈?
2、动画是如何过渡转换的?
当然叉叉是通过两条线转化过来的,此时我们通过源码中提供的数值,它这个数值,是根据是它设计时候的确定的。
一、画三条线
画三条线,实际上是先画两条线 ,再截取一条线,图片和路径如下:
两条线
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 2, 2);
CGPathAddLineToPoint(path, nil, 28, 2);
PS :CGContextRef,CGPath 和 UIBezierPath 本质上都是一样的,都是使用Quartz来绘画,只不过把绘图操作暴露在不同的API层面上。此处用的CGPath。
中间那条线
另一条线是通过截取下面这个圈圈的前半部分确定的
CGMutablePathRef 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);
截取那条线
layer.strokeStart = 0.028f;
layer.strokeEnd = 0.111f;
不过先不纠结这些数值怎么来的,其中提到一点是像那种圆可以通过PaintCode 得出,当然也是可以自己算的。
二、过渡动画
在CABasicAnimation中通过设定起始点,终点,时间,动画会沿着你这设定点进行移动。此处动画的核心转换是 toValue 的变化来达到效果。
两条线变成叉叉,或者叉叉返回两条线
变成 叉叉
CATransform3D translation = CATransform3DMakeTranslation(-4, 0, 0);
topTransform.toValue = [NSValue valueWithCATransform3D: CATransform3DRotate(translation, -0.7853975, 0, 0, 1)];
bottomTransform.toValue = [NSValue valueWithCATransform3D: CATransform3DRotate(translation, 0.7853975, 0, 0, 1)];
叉叉变回来两条线
topTransform.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
bottomTransform.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
单线变成圈圈,圈圈变成线条
变成圈圈
strokeStart.toValue = @(0.325);
strokeEnd.toValue = @( 0.9f);
变成线条
strokeStart.toValue = 0.028f;
strokeEnd.toValue = 0.111f;
然后两者结合起来,就达到了我们的那个效果啦,下面通过具体的实现代码来看。
直接上代码:
#import <UIKit/UIKit.h>
@interface HamburgerButton : UIButton
@property (nonatomic, assign) BOOL showsMenu;
@end
#import "HamburgerButton.h"
static const CGFloat kMenuStrokeStart = 0.325f;
static const CGFloat kMenuStrokeEnd = 0.9f;
static const CGFloat kHamburgerStrokeStart = 0.028f;
static const CGFloat kHamburgerStrokeEnd = 0.111f;
@interface HamburgerButton ()
@property (nonatomic, assign) CGMutablePathRef shortStrokePath;
@property (nonatomic, assign) CGMutablePathRef outlinePath;
@property (nonatomic, strong) CAShapeLayer *topLayer;
@property (nonatomic, strong) CAShapeLayer *middleLayer;
@property (nonatomic, strong) CAShapeLayer *bottomLayer;
@end
@implementation HamburgerButton
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self configLayer];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configLayer];
}
return self;
}
- (void)configLayer {
// init
self.topLayer = [CAShapeLayer layer];
self.topLayer.path = self.shortStrokePath;
self.middleLayer = [CAShapeLayer layer];
self.middleLayer.path = self.outlinePath;
self.bottomLayer = [CAShapeLayer layer];
self.bottomLayer.path = self.shortStrokePath;
// 添加属性
for (CAShapeLayer *layer in @[self.topLayer, self.middleLayer, self.bottomLayer]) {
// 设置基本属性
layer.fillColor = nil;
layer.strokeColor = [UIColor whiteColor].CGColor;
layer.lineWidth = 4.f;
layer.miterLimit = 4.f;
layer.lineCap = kCALineCapRound;
layer.masksToBounds = YES;
// 创建一个共享的路径
CGPathRef strokingPath = CGPathCreateCopyByStrokingPath(layer.path, nil, 4, kCGLineCapRound,kCGLineJoinMiter, 4);
layer.bounds = CGPathGetPathBoundingBox(strokingPath);
layer.actions = @{
@"strokeStart":[NSNull null],
@"strokeEnd":[NSNull null],
@"transform":[NSNull null]
};
// 添加到 layer 上
[self.layer addSublayer:layer];
}
// 确定其位置
self.topLayer.anchorPoint = CGPointMake(28.0/30.0, 0.5);
self.topLayer.position = CGPointMake(40, 18);
self.middleLayer.position = CGPointMake(27, 27);
self.middleLayer.strokeStart = kHamburgerStrokeStart;
self.middleLayer.strokeEnd = kHamburgerStrokeEnd;
self.bottomLayer.anchorPoint = CGPointMake(28.0 / 30.0, 0.5);
self.bottomLayer.position = CGPointMake(40, 36);
}
- (void)setShowsMenu:(BOOL)showsMenu {
// 中间那条线的处理
CABasicAnimation *strokeStart = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
CABasicAnimation *strokeEnd = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
/**
* 注意CAMediaTimingFunction 是一个贝塞尔曲线的控制方法,可以令动画做到先慢後快或先快後慢的结果
*/
if (showsMenu) {
strokeStart.toValue = @(kMenuStrokeStart);
strokeStart.duration = 0.5;
strokeStart.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25 :-0.4 :0.5 :1];
strokeEnd.toValue = @(kMenuStrokeEnd);
strokeEnd.duration = 0.6;
strokeEnd.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25 :-0.4 :0.5 :1];
}else {
strokeStart.toValue = @(kHamburgerStrokeStart);
strokeStart.duration = 0.5;
strokeStart.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25: 0 : 0.5 : 1.2];
strokeStart.beginTime = CACurrentMediaTime() + 0.1;
strokeStart.fillMode = kCAFillModeBackwards;
strokeEnd.toValue = @(kHamburgerStrokeEnd);
strokeEnd.duration = 0.6;
strokeEnd.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25 : 0.3 : 0.5 : 0.9];
}
[self addAnimationWithLayer:self.middleLayer animation:strokeStart];
[self addAnimationWithLayer:self.middleLayer animation:strokeEnd];
// 底部和上部的线
CABasicAnimation *topTransform = [CABasicAnimation animationWithKeyPath:@"transform"];
topTransform.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.5 :-0.8 :0.5 :1.85];
topTransform.duration = 0.4;
topTransform.fillMode = kCAFillModeBackwards;
/**
* CATransform3D 动作效果
*/
CABasicAnimation *bottomTransform = topTransform.copy;
if (showsMenu) {
CATransform3D translation = CATransform3DMakeTranslation(-4, 0, 0);
topTransform.toValue = [NSValue valueWithCATransform3D: CATransform3DRotate(translation, -0.7853975, 0, 0, 1)];
topTransform.beginTime = CACurrentMediaTime() + 0.25;
bottomTransform.toValue = [NSValue valueWithCATransform3D: CATransform3DRotate(translation, 0.7853975, 0, 0, 1)];
bottomTransform.beginTime = CACurrentMediaTime() + 0.25;
} else {
topTransform.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
topTransform.beginTime = CACurrentMediaTime() + 0.05;
bottomTransform.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
bottomTransform.beginTime = CACurrentMediaTime() + 0.05;
}
[self addAnimationWithLayer:self.topLayer animation:topTransform];
[self addAnimationWithLayer:self.bottomLayer animation:bottomTransform];
}
- (void)addAnimationWithLayer:(CAShapeLayer *)layer animation:(CABasicAnimation *)animation{
if (animation.fromValue == nil) {
animation.fromValue = [layer.presentationLayer valueForKeyPath:animation.keyPath];
}
[layer addAnimation:animation forKey:animation.keyPath];
// 记住需要重新设置一下,让其达到实现效果
[layer setValue:animation.toValue forKey:animation.keyPath];
}
- (CGMutablePathRef)shortStrokePath {
if (!_shortStrokePath) {
_shortStrokePath = CGPathCreateMutable();
CGPathMoveToPoint(_shortStrokePath, nil, 2, 2);
CGPathAddLineToPoint(_shortStrokePath, nil, 28, 2);
}
return _shortStrokePath;
}
- (CGMutablePathRef)outlinePath {
if (!_outlinePath) {
_outlinePath = CGPathCreateMutable();
CGPathMoveToPoint(_outlinePath, nil, 10, 27);
CGPathAddCurveToPoint(_outlinePath, nil, 12.00, 27.00, 28.02, 27.00, 40, 27);
CGPathAddCurveToPoint(_outlinePath, nil, 55.92, 27.00, 50.47, 2.00, 27, 2);
CGPathAddCurveToPoint(_outlinePath, nil, 13.16, 2.00, 2.00, 13.16, 2, 27);
CGPathAddCurveToPoint(_outlinePath, nil, 2.00, 40.84, 13.16, 52.00, 27, 52);
CGPathAddCurveToPoint(_outlinePath, nil, 40.84, 52.00, 52.00, 40.84, 52, 27);
CGPathAddCurveToPoint(_outlinePath, nil, 52.00, 13.16, 42.39, 2.00, 27, 2);
CGPathAddCurveToPoint(_outlinePath, nil, 13.16, 2.00, 2.00, 13.16, 2, 27);
}
return _outlinePath;
}
@end
#import "ViewController.h"
#import "HamburgerButton.h"
@interface ViewController ()
@property (nonatomic, strong) HamburgerButton *hamburgerButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithRed:38.0 / 255 green:151.0 / 255 blue:68.0 / 255 alpha:1.0];
self.hamburgerButton.center = self.view.center;
[self.view addSubview:self.hamburgerButton];
}
- (void)hamburgerAction:(HamburgerButton *)button {
button.selected = !button.selected;
button.showsMenu = button.selected;
}
- (HamburgerButton *)hamburgerButton {
if (!_hamburgerButton) {
_hamburgerButton = [[HamburgerButton alloc] initWithFrame:CGRectMake(0, 0, 54, 54)];
[_hamburgerButton addTarget:self action:@selector(hamburgerAction:) forControlEvents:UIControlEventTouchUpInside];
}
return _hamburgerButton;
}
@end
总的来说大致实现是这样的,但里面其实还有很多点可以挖掘。
- 数值(toValue)是如何确定的?
- CAMediaTimingFunction 如何改变动画速度?
- CATransform3D 动作效果如何设置细节的?
这个先记录着,还是继续通过动画一步一步先实现效果,然后再慢慢深入细节点。