类QQ粘性按钮(封装)
那个,先来说说原理吧:
这里原理就是,在界面设置两个控件一个按钮在上面,一个View在下面(同样大小),当我们拖动按钮的时候显示下面的View,view不移动,但是会根据按钮中心点和它的中心点的距离去等比例变化自己的半径,越远半径酒越小,最后就会消失,而我们这里吗最难的就是在变化的过程中去计算并且设置他们两个之间的区域并且填充。这里需要计算六个点的位置(根据勾股定理),然后根据两个控件同一边的位置的两个点去绘制一条曲线。拖动距离到达一定的时候就会使用动画(序列帧)去清楚界面的按钮,随后做了一定的优化,,,好了就这么多,下面来看看具体怎么实现它!
1:创建一个自定义的按钮:这里名为iCocosBadgeView
2:在头文件中创建一个图片数组,这里时为了后面实现拖动后删除动画:
#import <UIKit/UIKit.h> @interface iCocosBadgeView : UIButton<NSCopying> @property (nonatomic, strong) NSArray *images; @end
3:实现文件中实现相应的功能需求
这里时自定义View的基本常识久不多说:
- (void)awakeFromNib { [self setUp]; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setUp]; } return self; }
4:实现按钮的初始化和属性的设置,并且为他添加拖动手势
// 初始化 - (void)setUp { // self.width CGFloat w = self.frame.size.width; // 设置圆角 self.layer.cornerRadius = w * 0.5; // 设置字体 self.titleLabel.font = [UIFont systemFontOfSize:]; // 设置字体颜色 [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // 添加手势 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self addGestureRecognizer:pan]; // 添加小圆,颜色一样,圆角半径,尺寸 // 如果一个类想使用copy,必须要遵守NSCopying UIView *smallCircleView = [self copy]; // 把小圆添加badgeView的父控件 [self.superview insertSubview:smallCircleView belowSubview:self]; }
由于上面直接使用copy复制一份来实现下面的那个View,具体下面那个View我前面已经介绍,所以我们需要让他遵守NSCoping协议,并且实现copyWithZone方法:
- <NSCopying>
- (id)copyWithZone:(NSZone *)zone { UIView *smallCircleView = [[UIView alloc] initWithFrame:self.frame]; smallCircleView.backgroundColor = self.backgroundColor; smallCircleView.layer.cornerRadius = self.layer.cornerRadius; _smallCircleView = smallCircleView; return _smallCircleView; }
5:实现按钮拖动手势方法
在这之前需要定义一个地步View的属性,用来记录用户的一些操作
@property (nonatomic, weak) UIView *smallCircleView;
// 手指拖动的时候调用
- (void)pan:(UIPanGestureRecognizer *)pan { // 获取手指的偏移量 CGPoint transP = [pan translationInView:self]; // 设置形变 // 修改形变不会修改center CGPoint center = self.center; center.x += transP.x; center.y += transP.y; self.center = center; // self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y); // 复位 [pan setTranslation:CGPointZero inView:self]; // 计算两个圆的圆心距离 CGFloat d = [self distanceWithSmallCircleView:_smallCircleView bigCircleView:self]; // 计算小圆的半径 CGFloat smallRadius = self.bounds.size.width * 0.5 - d / 10.0; // 给小圆赋值 _smallCircleView.bounds = CGRectMake(, , smallRadius * , smallRadius * ); // 注意小圆半径一定要改 _smallCircleView.layer.cornerRadius = smallRadius; // 设置不规则的矩形路径 if (_smallCircleView.hidden == NO) {// 小圆显示的时候才需要描述不规则矩形 self.shapeL.path = [self pathWithSmallCircleView:_smallCircleView bigCircleView:self].CGPath; } // 拖动的时候判断下圆心距离是否大于50 ) { // 粘性效果拖没 // 隐藏小圆 _smallCircleView.hidden = YES; // 隐藏不规则的layer // _shapeL.hidden = YES; // 从父层中移除,就有吸附效果 [self.shapeL removeFromSuperlayer]; } // 手指抬起的业务逻辑 if (pan.state == UIGestureRecognizerStateEnded) { ) { // 播放gif动画 // 创建UIImageView UIImageView *imageV = [[UIImageView alloc] initWithFrame:self.bounds]; NSMutableArray *images = [NSMutableArray array]; if (_images == nil) { ; i <= ; i++) { NSString *imageName = [NSString stringWithFormat:@"%d",i]; UIImage *image = [UIImage imageNamed:imageName]; [images addObject:image]; } }else{ images = _images; } imageV.animationImages = images; imageV.animationDuration = ; [imageV startAnimating]; [self addSubview:imageV]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.9 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self removeFromSuperview]; }); }else{ // 两个圆心距离没有超过范围 // 弹簧效果 [UIView animateWithDuration: usingSpringWithDamping: options:UIViewAnimationOptionCurveLinear animations:^{ // badgeView还原到之前的位置,设置中心点为原来位置 self.center = _smallCircleView.center; } completion:^(BOOL finished) { }]; // 小圆重新显示 _smallCircleView.hidden = NO; // 不规则的矩形形状也需要干掉 [self.shapeL removeFromSuperlayer]; } } }
6:下面就是本案例中最难的一部分,其实也不难,只不过涉及到了比较麻烦的计算
先来看图:
是不是感觉一片茫然,好吧哪里来根据下面的代码结合上面的图片,相信你会看懂。
提示一下,这里主要是计算ABCDOP四个点的位置(坐标)然后时候画图技术绘制并且填充这个区域,
// 根据两个控件描述不规则的路径
- (UIBezierPath *)pathWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView { // 小圆,x1,y1,r1 CGFloat x1 = smallCircleView.center.x; CGFloat y1 = smallCircleView.center.y; CGFloat r1 = smallCircleView.bounds.size.width * 0.5; // 大圆,x2,y2,r2 CGFloat x2 = bigCircleView.center.x; CGFloat y2 = bigCircleView.center.y; CGFloat r2 = bigCircleView.bounds.size.width * 0.5; // 计算两个圆心距离 CGFloat d = [self distanceWithSmallCircleView:smallCircleView bigCircleView:bigCircleView]; ) return nil; // cosθ CGFloat cosθ = (y2 - y1) / d; // sinθ CGFloat sinθ = (x2 - x1) / d; CGPoint pointA = CGPointMake(x1 - r1 * cosθ, y1 + r1 * sinθ); CGPoint pointB = CGPointMake(x1 + r1 * cosθ, y1 - r1 * sinθ); CGPoint pointC = CGPointMake(x2 + r2 * cosθ, y2 - r2 * sinθ); CGPoint pointD = CGPointMake(x2 - r2 * cosθ, y2 + r2 * sinθ); CGPoint pointO = CGPointMake(pointA.x + d * 0.5 * sinθ, pointA.y + d * 0.5 * cosθ); CGPoint pointP = CGPointMake(pointB.x + d * 0.5 * sinθ, pointB.y + d * 0.5 * cosθ); // 描述路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 设置起点 [path moveToPoint:pointA]; // AB [path addLineToPoint:pointB]; // BC [path addQuadCurveToPoint:pointC controlPoint:pointP]; // CD [path addLineToPoint:pointD]; // DA [path addQuadCurveToPoint:pointA controlPoint:pointO]; return path; }
由于前面设计到了计算两个控件的中心点之间的距离,所以我们将它抽出来,这样一看就懂,而且方便以后使用
// 获取两个控件之间圆心距离
- (CGFloat)distanceWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView { // 获取x轴偏移量 CGFloat offsetX = bigCircleView.center.x - smallCircleView.center.x; // 获取y轴偏移量 CGFloat offsetY = bigCircleView.center.y - smallCircleView.center.y; // 获取两个圆心的距离 CGFloat d = sqrtf((offsetX * offsetX + offsetY * offsetY)); // sqrtf开根 return d; }
由于前面设置到了拖动上面那个按钮需要实现地步View的半径的变化,并且实现两个控件之间填充控一些对应的,所以这里我们使用的是形变图层:
需要先定义一个图层属性
@property (nonatomic, weak) CAShapeLayer *shapeL;
然后懒加载他:
- (CAShapeLayer *)shapeL { if (_shapeL == nil) { // 创建形状图层 // 利用形状图层 CAShapeLayer *shape = [CAShapeLayer layer]; // 设置填充颜色 shape.fillColor = [UIColor redColor].CGColor; [self.superview.layer insertSublayer:shape atIndex:]; _shapeL = shape; } return _shapeL; }
注意:由于默认系统会讲控制器设置为自动约束,所以一半我们需要取消他:
self.view.translatesAutoresizingMaskIntoConstraints = NO;
下面说说怎么去使用它吧,
1:在界面拖一个按钮设置对应的frame,然后你只需要将对应按钮的class设置为我们自定义的按钮就可以,就这么多:
2:导入我们的按钮类,然后初始化他,并且设置对应的属性:
#import "iCocosBadgeView.h"
初始化控件:
iCocosBadgeView *bage = [[iCocosBadgeView alloc] init]; bage.frame = CGRectMake(, , , ); bage.backgroundColor = [UIColor redColor]; [self.view addSubview:bage];
实现效果:
所有源码:
iCocosBadgeView.h文件的声明
#import <UIKit/UIKit.h> @interface iCocosBadgeView : UIButton<NSCopying> @property (nonatomic, strong) NSArray *images; @end
iCocosBadgeView.m文件的实现
#import "iCocosBadgeView.h" @interface iCocosBadgeView () @property (nonatomic, weak) UIView *smallCircleView; @property (nonatomic, weak) CAShapeLayer *shapeL; @end @implementation iCocosBadgeView - (CAShapeLayer *)shapeL { if (_shapeL == nil) { // 创建形状图层 // 利用形状图层 CAShapeLayer *shape = [CAShapeLayer layer]; // 设置填充颜色 shape.fillColor = [UIColor redColor].CGColor; [self.superview.layer insertSublayer:shape atIndex:]; _shapeL = shape; } return _shapeL; } - (void)awakeFromNib { [self setUp]; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setUp]; } return self; } // 初始化 - (void)setUp { // self.width CGFloat w = self.frame.size.width; // 设置圆角 self.layer.cornerRadius = w * 0.5; // 设置字体 self.titleLabel.font = [UIFont systemFontOfSize:]; // 设置字体颜色 [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // 添加手势 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self addGestureRecognizer:pan]; // 添加小圆,颜色一样,圆角半径,尺寸 // 如果一个类想使用copy,必须要遵守NSCopying UIView *smallCircleView = [self copy]; // 把小圆添加badgeView的父控件 [self.superview insertSubview:smallCircleView belowSubview:self]; } // 只要调用copy就会调用这个方法 - (id)copyWithZone:(NSZone *)zone { UIView *smallCircleView = [[UIView alloc] initWithFrame:self.frame]; smallCircleView.backgroundColor = self.backgroundColor; smallCircleView.layer.cornerRadius = self.layer.cornerRadius; _smallCircleView = smallCircleView; return _smallCircleView; } // 手指拖动的时候调用 - (void)pan:(UIPanGestureRecognizer *)pan { // 获取手指的偏移量 CGPoint transP = [pan translationInView:self]; // 设置形变 // 修改形变不会修改center CGPoint center = self.center; center.x += transP.x; center.y += transP.y; self.center = center; // self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y); // 复位 [pan setTranslation:CGPointZero inView:self]; // 计算两个圆的圆心距离 CGFloat d = [self distanceWithSmallCircleView:_smallCircleView bigCircleView:self]; // 计算小圆的半径 CGFloat smallRadius = self.bounds.size.width * 0.5 - d / 10.0; // 给小圆赋值 _smallCircleView.bounds = CGRectMake(, , smallRadius * , smallRadius * ); // 注意小圆半径一定要改 _smallCircleView.layer.cornerRadius = smallRadius; // 设置不规则的矩形路径 if (_smallCircleView.hidden == NO) {// 小圆显示的时候才需要描述不规则矩形 self.shapeL.path = [self pathWithSmallCircleView:_smallCircleView bigCircleView:self].CGPath; } // 拖动的时候判断下圆心距离是否大于50 ) { // 粘性效果拖没 // 隐藏小圆 _smallCircleView.hidden = YES; // 隐藏不规则的layer // _shapeL.hidden = YES; // 从父层中移除,就有吸附效果 [self.shapeL removeFromSuperlayer]; } // 手指抬起的业务逻辑 if (pan.state == UIGestureRecognizerStateEnded) { ) { // 播放gif动画 // 创建UIImageView UIImageView *imageV = [[UIImageView alloc] initWithFrame:self.bounds]; NSMutableArray *images = [NSMutableArray array]; if (_images == nil) { ; i <= ; i++) { NSString *imageName = [NSString stringWithFormat:@"%d",i]; UIImage *image = [UIImage imageNamed:imageName]; [images addObject:image]; } }else{ images = _images; } imageV.animationImages = images; imageV.animationDuration = ; [imageV startAnimating]; [self addSubview:imageV]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.9 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self removeFromSuperview]; }); }else{ // 两个圆心距离没有超过范围 // 弹簧效果 [UIView animateWithDuration: usingSpringWithDamping: options:UIViewAnimationOptionCurveLinear animations:^{ // badgeView还原到之前的位置,设置中心点为原来位置 self.center = _smallCircleView.center; } completion:^(BOOL finished) { }]; // 小圆重新显示 _smallCircleView.hidden = NO; // 不规则的矩形形状也需要干掉 [self.shapeL removeFromSuperlayer]; } } } // 根据两个控件描述不规则的路径 - (UIBezierPath *)pathWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView { // 小圆,x1,y1,r1 CGFloat x1 = smallCircleView.center.x; CGFloat y1 = smallCircleView.center.y; CGFloat r1 = smallCircleView.bounds.size.width * 0.5; // 大圆,x2,y2,r2 CGFloat x2 = bigCircleView.center.x; CGFloat y2 = bigCircleView.center.y; CGFloat r2 = bigCircleView.bounds.size.width * 0.5; // 计算两个圆心距离 CGFloat d = [self distanceWithSmallCircleView:smallCircleView bigCircleView:bigCircleView]; ) return nil; // cosθ CGFloat cosθ = (y2 - y1) / d; // sinθ CGFloat sinθ = (x2 - x1) / d; CGPoint pointA = CGPointMake(x1 - r1 * cosθ, y1 + r1 * sinθ); CGPoint pointB = CGPointMake(x1 + r1 * cosθ, y1 - r1 * sinθ); CGPoint pointC = CGPointMake(x2 + r2 * cosθ, y2 - r2 * sinθ); CGPoint pointD = CGPointMake(x2 - r2 * cosθ, y2 + r2 * sinθ); CGPoint pointO = CGPointMake(pointA.x + d * 0.5 * sinθ, pointA.y + d * 0.5 * cosθ); CGPoint pointP = CGPointMake(pointB.x + d * 0.5 * sinθ, pointB.y + d * 0.5 * cosθ); // 描述路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 设置起点 [path moveToPoint:pointA]; // AB [path addLineToPoint:pointB]; // BC [path addQuadCurveToPoint:pointC controlPoint:pointP]; // CD [path addLineToPoint:pointD]; // DA [path addQuadCurveToPoint:pointA controlPoint:pointO]; return path; } // 获取两个控件之间圆心距离 - (CGFloat)distanceWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView { // 获取x轴偏移量 CGFloat offsetX = bigCircleView.center.x - smallCircleView.center.x; // 获取y轴偏移量 CGFloat offsetY = bigCircleView.center.y - smallCircleView.center.y; // 获取两个圆心的距离 CGFloat d = sqrtf((offsetX * offsetX + offsetY * offsetY)); // sqrtf开根 return d; } // 目的:取消系统高亮状态做的事情 - (void)setHighlighted:(BOOL)highlighted { } @end