iOS开发-CAShapeLayer与UIBezierPath实现微信首页的下拉菜单效果
之前开发中遇到需要使用实现微信首页的下拉菜单效果。用到了CAShapeLayer与UIBezierPath绘制菜单外框。
一、效果图
二、CAShapeLayer与UIBezierPath
2.1、CAShapeLayer是什么?
CAShapeLayer继承自CALayer,可使用CALayer的所有属性
CAShapeLayer需要和UIBezierPath绘制图形。
创建shapeLayer
// 创建 shapeLayer
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc]init];
[self.view.layer addSublayer:shapeLayer];
shapeLayer.path = path.CGPath;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.lineWidth = 5;
2.2、UIBezierPath是什么?
UIBezierPath即贝塞尔曲线
UIBezierPath 类允许你在自定义的 View 中绘制和渲染由直线和曲线组成的路径
+ (instancetype)bezierPath;
+ (instancetype)bezierPathWithRect:(CGRect)rect;
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; // rounds all corners with the same horizontal and vertical radius
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
三、绘制箭头
在弹出的菜单弹出时候会显示箭头,这里使用UIBezierPath和CAShapeLayer绘制箭头。
#pragma mark - DrawShapeLayer
- (void)drawShapeLayer {
CGFloat startPointX = self.contentStartPoint.x;
CGFloat startPointY = self.contentStartPoint.y;
CGFloat width = CGRectGetWidth(self.contentBGImageView.frame);
CGFloat height = CGRectGetHeight(self.contentBGImageView.frame);
self.path = [UIBezierPath bezierPath]; // 创建路径
[self.path moveToPoint:CGPointMake(startPointX, startPointY)]; // 设置起始点
[self.path addLineToPoint:CGPointMake(startPointX + 5.0, kAnchorHeight)];
[self.path addLineToPoint:CGPointMake(width - 5.0, kAnchorHeight)];
[self.path addArcWithCenter:CGPointMake(width - 5.0, kAnchorHeight + 5.0) radius:5 startAngle:KCP*3/2 endAngle:2*KCP clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(width, height - 5.0)];
[self.path addArcWithCenter:CGPointMake(width - 5.0, height - 5.0) radius:5 startAngle:0 endAngle:KCP/2 clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(5, height)];
[self.path addArcWithCenter:CGPointMake(5.0, height-5.0) radius:5 startAngle:KCP/2 endAngle:KCP clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(0.0, kAnchorHeight + 5.0)];
[self.path addArcWithCenter:CGPointMake(5, kAnchorHeight + 5) radius:5 startAngle:KCP endAngle:KCP*3/2 clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(startPointX - 5.0, kAnchorHeight)];
[self.path addLineToPoint:CGPointMake(startPointX, startPointY)];
self.path.lineWidth = 2.f;
self.path.lineCapStyle = kCGLineCapRound;
self.path.lineJoinStyle = kCGLineCapRound;
[self.path closePath]; // 封闭未形成闭环的路径
UIGraphicsBeginImageContext(self.contentBGImageView.bounds.size);
[self.path stroke];
UIGraphicsEndImageContext();
self.shapeLayer.path = self.path.CGPath;
}
四、点击触摸的mask处理
当下拉菜单显示后,MaskView上实现touchesBegan可以点击触摸可以隐藏菜单,
INNoteZoneOptionMaskView.h
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@protocol INNoteZoneOptionMaskViewDelegate;
@interface INNoteZoneOptionMaskView : UIView
@property (nonatomic, weak) id<INNoteZoneOptionMaskViewDelegate>maskDelegate;
@end
@protocol INNoteZoneOptionMaskViewDelegate <NSObject>
- (void)optionMaskTouched;
@end
INNoteZoneOptionMaskView.m
#import "INNoteZoneOptionMaskView.h"
@implementation INNoteZoneOptionMaskView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
// 1.自己先处理事件...
if (self.maskDelegate && [self.maskDelegate respondsToSelector:@selector(optionMaskTouched)]) {
[self.maskDelegate optionMaskTouched];
}
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
@end
五、菜单显示
控件显示在keyWindow上
[[UIApplication sharedApplication].keyWindow addSubview:self];
显示动画
- (void)showOption {
self.hidden = NO;
CGPoint anchorPoint = CGPointMake(self.contentStartPoint.x/self.contentBGImageView.frame.size.width, 0.0);
[self resetAnChorPoint:self.contentBGImageView anchorPoint:anchorPoint];
self.contentBGImageView.transform = CGAffineTransformMakeScale(0.1, 0.1);
[UIView animateWithDuration:0.25 animations:^{
self.contentBGImageView.transform = CGAffineTransformMakeScale(1.0, 1.0);
self.contentBGImageView.alpha = 1.0;
} completion:^(BOOL finished) {
}];
}
隐藏菜单
- (void)dismissOption {
[UIView animateWithDuration:0.25 animations:^{
self.contentBGImageView.transform = CGAffineTransformMakeScale(0.1, 0.1);
self.contentBGImageView.alpha = 0.0;
} completion:^(BOOL finished) {
self.hidden = YES;
[self removeFromSuperview];
}];
}
五、菜单完整代码
菜单完整代码如下
INNoteZoneOptionView.h
#import <UIKit/UIKit.h>
/**
元素的item
*/
typedef void(^OptionTouchBlock)(void);
@interface INNoteZoneOptionItem : NSObject
@property (nonatomic, strong) UIImage *iconImage;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, copy) OptionTouchBlock block;
@end
/**
按钮控件
*/
@interface INNoteZoneOptionButton : UIControl
@property (nonatomic, strong) UIImage *iconImage;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) BOOL showLine;
@property (nonatomic, strong) INNoteZoneOptionItem *item;
@end
/**
点击➕号的选项操作
*/
@interface INNoteZoneOptionView : UIView
- (instancetype)initWithFrame:(CGRect)frame anchorPoint:(CGPoint)anchorPoint items:(NSArray *)items;
- (void)showOption;
- (void)dismissOption;
@end
INNoteZoneOptionView.m
#import "INNoteZoneOptionView.h"
#import "UIColor+Addition.h"
#import "UIImageView+WebCache.h"
#import "UIImage+YYAdd.h"
#import "INNoteZoneOptionMaskView.h"
#define KCP 3.1415926
static CGFloat kOpContentWidth = 130.0;
static CGFloat kOpItemHeight = 50.0;
static CGFloat kOpIconSize = 16.0;
static CGFloat kOpMidPadding = 10.0;
static CGFloat kAnchorHeight = 5.0;
static CGFloat kBtnMidPadding = 15.0;
static CGFloat kBtnPadding = 2.0;
static CGFloat kLineHeight = 1.0;
/**
元素的item
*/
@interface INNoteZoneOptionItem ()
@end
@implementation INNoteZoneOptionItem
@end
/**
按钮控件
*/
@interface INNoteZoneOptionButton ()
@property (nonatomic, strong) UIImageView *bgImageView;
@property (nonatomic, strong) UIImageView *iconImageView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIImageView *lineImageView;
@end
@implementation INNoteZoneOptionButton
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.bgImageView];
[self.bgImageView addSubview:self.iconImageView];
[self.bgImageView addSubview:self.titleLabel];
[self.bgImageView addSubview:self.lineImageView];
self.showLine = NO;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.bgImageView.frame = self.bounds;
self.iconImageView.frame = CGRectMake(kBtnMidPadding, (CGRectGetHeight(self.bounds) - kOpIconSize)/2, kOpIconSize, kOpIconSize);
self.titleLabel.frame = CGRectMake(CGRectGetMaxX(self.iconImageView.frame) + kBtnMidPadding, 0.0, CGRectGetWidth(self.bgImageView.frame) - (CGRectGetMaxX(self.iconImageView.frame) + kBtnMidPadding*2), CGRectGetHeight(self.bounds));
self.lineImageView.frame = CGRectMake(0.0, CGRectGetHeight(self.bgImageView.frame) - kLineHeight, CGRectGetWidth(self.bgImageView.frame), kLineHeight);
}
- (void)setIconImage:(UIImage *)iconImage {
_iconImage = iconImage;
self.iconImageView.image = iconImage;
[self setNeedsLayout];
}
- (void)setTitle:(NSString *)title {
_title = (title?title:@"");
self.titleLabel.text = _title;
[self setNeedsLayout];
}
- (void)setShowLine:(BOOL)showLine {
_showLine = showLine;
self.lineImageView.hidden = !showLine;
[self setNeedsLayout];
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
if (highlighted) {
self.bgImageView.backgroundColor = [UIColor colorWithHexString:@"f4f4f4"];
} else {
self.bgImageView.backgroundColor = [UIColor whiteColor];
}
}
#pragma mark - SETTER/GETTER
- (UIImageView *)iconImageView {
if (!_iconImageView) {
_iconImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_iconImageView.contentMode = UIViewContentModeScaleAspectFit;
}
return _iconImageView;
}
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_titleLabel.font = [UIFont boldSystemFontOfSize:14];
_titleLabel.textColor = [UIColor colorWithHexString:@"131619"];
_titleLabel.backgroundColor = [UIColor clearColor];
}
return _titleLabel;
}
- (UIImageView *)bgImageView {
if (!_bgImageView) {
_bgImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_bgImageView.backgroundColor = [UIColor whiteColor];
_bgImageView.layer.cornerRadius = 2.0;
_bgImageView.layer.masksToBounds = YES;
}
return _bgImageView;
}
- (UIImageView *)lineImageView {
if (!_lineImageView) {
_lineImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_lineImageView.backgroundColor = [UIColor colorWithHexString:@"f1f1f1"];
}
return _lineImageView;
}
@end
/**
点击➕号的选项操作
*/
@interface INNoteZoneOptionView ()<INNoteZoneOptionMaskViewDelegate>
@property (nonatomic, strong) INNoteZoneOptionMaskView *maskBGView;
@property (nonatomic, strong) UIImageView *contentBGImageView;
@property (nonatomic, strong) CAShapeLayer *shapeLayer;
@property (nonatomic, strong) UIBezierPath *path;
@property (nonatomic) CGPoint anchorPoint;
@property (nonatomic) CGPoint contentStartPoint;
@property (nonatomic, strong) NSArray *items;
@end
@implementation INNoteZoneOptionView
- (instancetype)initWithFrame:(CGRect)frame anchorPoint:(CGPoint)anchorPoint items:(NSArray *)items {
self = [super initWithFrame:frame];
if (self) {
self.anchorPoint = anchorPoint;
self.items = items;
[self addSubview:self.maskBGView];
[self addSubview:self.contentBGImageView];
[self.contentBGImageView.layer addSublayer:self.shapeLayer];
[[UIApplication sharedApplication].keyWindow addSubview:self];
self.frame = [UIScreen mainScreen].bounds;
self.hidden = YES;
[self layoutOptionSubviews];
[self configContentPoint];
[self drawShapeLayer];
[self setupOptionButtons];
}
return self;
}
- (void)configContentPoint {
CGFloat scale = self.anchorPoint.x/self.frame.size.width;
CGFloat contentWidth = CGRectGetWidth(self.contentBGImageView.frame);
self.contentStartPoint = CGPointMake(scale*contentWidth - kOpMidPadding, 0.0);
}
- (void)layoutOptionSubviews {
self.maskBGView.frame = self.bounds;
self.contentBGImageView.frame = CGRectMake(CGRectGetWidth(self.bounds) - kOpContentWidth - kOpMidPadding, self.anchorPoint.y, kOpContentWidth, self.items.count*kOpItemHeight + kAnchorHeight + 2*kBtnPadding);
self.shapeLayer.frame = self.contentBGImageView.bounds;
}
#pragma mark - Setup Buttons
- (void)setupOptionButtons {
NSInteger index = 0;
for (INNoteZoneOptionItem *item in self.items) {
INNoteZoneOptionButton *button = [[INNoteZoneOptionButton alloc] initWithFrame:CGRectZero];
button.frame = CGRectMake(kBtnPadding, kBtnPadding + kAnchorHeight + index*kOpItemHeight, CGRectGetWidth(self.contentBGImageView.frame) - 2*kBtnPadding, kOpItemHeight);
button.iconImage = item.iconImage;
button.title = item.title;
button.item = item;
[self.contentBGImageView addSubview:button];
[button addTarget:self action:@selector(optionButtonAction:) forControlEvents:UIControlEventTouchUpInside];
button.showLine = YES;
if (index == self.items.count - 1) {
button.showLine = NO;
}
index++;
}
}
- (void)optionButtonAction:(INNoteZoneOptionButton *)button {
NSLog(@"点击栏目");
if (button.item && button.item.block) {
button.item.block();
}
}
#pragma mark - Show AND Dismiss
- (void)showOption {
self.hidden = NO;
CGPoint anchorPoint = CGPointMake(self.contentStartPoint.x/self.contentBGImageView.frame.size.width, 0.0);
[self resetAnChorPoint:self.contentBGImageView anchorPoint:anchorPoint];
self.contentBGImageView.transform = CGAffineTransformMakeScale(0.1, 0.1);
[UIView animateWithDuration:0.25 animations:^{
self.contentBGImageView.transform = CGAffineTransformMakeScale(1.0, 1.0);
self.contentBGImageView.alpha = 1.0;
} completion:^(BOOL finished) {
}];
}
- (void)dismissOption {
[UIView animateWithDuration:0.25 animations:^{
self.contentBGImageView.transform = CGAffineTransformMakeScale(0.1, 0.1);
self.contentBGImageView.alpha = 0.0;
} completion:^(BOOL finished) {
self.hidden = YES;
[self removeFromSuperview];
}];
}
#pragma mark - ResetAnChorPoint
/**
设置动画图钉位置
@param view subView
*/
- (void)resetAnChorPoint:(UIView *)view anchorPoint:(CGPoint)anchorPoint {
CGPoint oldAnchorPoint = view.layer.anchorPoint;
view.layer.anchorPoint = anchorPoint;
[view.layer setPosition:CGPointMake(view.layer.position.x + view.layer.bounds.size.width * (view.layer.anchorPoint.x - oldAnchorPoint.x), view.layer.position.y + view.layer.bounds.size.height * (view.layer.anchorPoint.y - oldAnchorPoint.y))];
}
#pragma mark - INNoteZoneOptionMaskViewDelegate
- (void)optionMaskTouched {
[self dismissOption];
}
#pragma mark - SETTER/GETTER
- (INNoteZoneOptionMaskView *)maskBGView {
if (!_maskBGView) {
_maskBGView = [[INNoteZoneOptionMaskView alloc] initWithFrame:CGRectZero];
_maskBGView.backgroundColor = [UIColor clearColor];
_maskBGView.userInteractionEnabled = YES;
_maskBGView.maskDelegate = self;
}
return _maskBGView;
}
- (UIImageView *)contentBGImageView {
if (!_contentBGImageView) {
_contentBGImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
_contentBGImageView.userInteractionEnabled = YES;
_contentBGImageView.backgroundColor = [UIColor clearColor];
}
return _contentBGImageView;
}
- (CAShapeLayer *)shapeLayer {
if (!_shapeLayer) {
_shapeLayer = [CAShapeLayer layer];
_shapeLayer.fillColor = [UIColor whiteColor].CGColor;
//设置线条的宽度和颜色
_shapeLayer.lineWidth = 1.0f;
_shapeLayer.strokeColor = [UIColor colorWithHexString:@"9bb9ef" alpha:0.55].CGColor;
_shapeLayer.shadowColor = [UIColor colorWithHexString:@"9bb9ef"].CGColor;
_shapeLayer.shadowOffset = CGSizeMake(-3, 3);
_shapeLayer.shadowOpacity = 0.3;
_shapeLayer.shadowRadius = 3.0;
}
return _shapeLayer;
}
#pragma mark - DrawShapeLayer
- (void)drawShapeLayer {
CGFloat startPointX = self.contentStartPoint.x;
CGFloat startPointY = self.contentStartPoint.y;
CGFloat width = CGRectGetWidth(self.contentBGImageView.frame);
CGFloat height = CGRectGetHeight(self.contentBGImageView.frame);
self.path = [UIBezierPath bezierPath]; // 创建路径
[self.path moveToPoint:CGPointMake(startPointX, startPointY)]; // 设置起始点
[self.path addLineToPoint:CGPointMake(startPointX + 5.0, kAnchorHeight)];
[self.path addLineToPoint:CGPointMake(width - 5.0, kAnchorHeight)];
[self.path addArcWithCenter:CGPointMake(width - 5.0, kAnchorHeight + 5.0) radius:5 startAngle:KCP*3/2 endAngle:2*KCP clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(width, height - 5.0)];
[self.path addArcWithCenter:CGPointMake(width - 5.0, height - 5.0) radius:5 startAngle:0 endAngle:KCP/2 clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(5, height)];
[self.path addArcWithCenter:CGPointMake(5.0, height-5.0) radius:5 startAngle:KCP/2 endAngle:KCP clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(0.0, kAnchorHeight + 5.0)];
[self.path addArcWithCenter:CGPointMake(5, kAnchorHeight + 5) radius:5 startAngle:KCP endAngle:KCP*3/2 clockwise:YES]; // 绘制一个圆弧
[self.path addLineToPoint:CGPointMake(startPointX - 5.0, kAnchorHeight)];
[self.path addLineToPoint:CGPointMake(startPointX, startPointY)];
self.path.lineWidth = 2.f;
self.path.lineCapStyle = kCGLineCapRound;
self.path.lineJoinStyle = kCGLineCapRound;
[self.path closePath]; // 封闭未形成闭环的路径
UIGraphicsBeginImageContext(self.contentBGImageView.bounds.size);
[self.path stroke];
UIGraphicsEndImageContext();
self.shapeLayer.path = self.path.CGPath;
}
@end
六、使用显示下拉菜单
使用显示下拉菜单代码,根据anchorPoint确定显示的箭头位置
#pragma mark - INNoteZoneNavbarViewDelegate
- (void)addButtonDidAction:(UIButton *)button {
CGRect btnRect = [button convertRect:button.bounds toView:[UIApplication sharedApplication].keyWindow];
__weak typeof(self) weakSelf = self;
INNoteZoneOptionItem *postItem = [[INNoteZoneOptionItem alloc] init];
postItem.iconImage = [UIImage imageNamed:@"ic_op_editpost"];
postItem.title = @"发布帖子";
postItem.block = ^{
NSLog(@"发布帖子");
};
INNoteZoneOptionItem *scanItem = [[INNoteZoneOptionItem alloc] init];
scanItem.iconImage = [UIImage imageNamed:@"ic_op_scan"];
scanItem.title = @"扫一扫";
scanItem.block = ^{
NSLog(@"扫一扫");
};
INNoteZoneOptionItem *calItem = [[INNoteZoneOptionItem alloc] init];
calItem.iconImage = [UIImage imageNamed:@"ic_op_cal"];
calItem.title = @"日历";
calItem.block = ^{
NSLog(@"日历");
};
INNoteZoneOptionItem *pubWordItem = [[INNoteZoneOptionItem alloc] init];
pubWordItem.iconImage = [UIImage imageNamed:@"ic_op_edit"];
pubWordItem.title = @"发布美句";
pubWordItem.block = ^{
NSLog(@"发布美句");
};
INNoteZoneOptionView *optionView = [[INNoteZoneOptionView alloc] initWithFrame:CGRectZero anchorPoint:CGPointMake(CGRectGetMidX(btnRect), CGRectGetMaxY(btnRect)) items:@[postItem,scanItem,calItem,pubWordItem]];
[optionView showOption];
}
七、小结
iOS开发-CAShapeLayer与UIBezierPath实现微信首页的下拉菜单效果。用到了CAShapeLayer与UIBezierPath绘制菜单外框。
学习记录,每天不停进步。