类QQ粘性按钮(封装)
那个,先来说说原理吧:
这里原理就是,在界面设置两个控件一个按钮在上面,一个View在下面(同样大小),当我们拖动按钮的时候显示下面的View,view不移动,但是会根据按钮中心点和它的中心点的距离去等比例变化自己的半径,越远半径酒越小,最后就会消失,而我们这里吗最难的就是在变化的过程中去计算并且设置他们两个之间的区域并且填充。这里需要计算六个点的位置(根据勾股定理),然后根据两个控件同一边的位置的两个点去绘制一条曲线。拖动距离到达一定的时候就会使用动画(序列帧)去清楚界面的按钮,随后做了一定的优化,,,好了就这么多,下面来看看具体怎么实现它!
1:创建一个自定义的按钮:这里名为iCocosBadgeView
2:在头文件中创建一个图片数组,这里时为了后面实现拖动后删除动画:
1 #import <UIKit/UIKit.h> 2 3 @interface iCocosBadgeView : UIButton<NSCopying> 4 5 @property (nonatomic, strong) NSArray *images; 6 7 @end
3:实现文件中实现相应的功能需求
这里时自定义View的基本常识久不多说:
- (void)awakeFromNib { [self setUp]; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setUp]; } return self; }
4:实现按钮的初始化和属性的设置,并且为他添加拖动手势
1 2 3 // 初始化 4 5 - (void)setUp 6 7 { 8 9 // self.width 10 11 CGFloat w = self.frame.size.width; 12 13 // 设置圆角 14 15 self.layer.cornerRadius = w * 0.5; 16 17 // 设置字体 18 19 self.titleLabel.font = [UIFont systemFontOfSize:12]; 20 21 // 设置字体颜色 22 23 [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 24 25 26 27 // 添加手势 28 29 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; 30 31 [self addGestureRecognizer:pan]; 32 33 34 35 // 添加小圆,颜色一样,圆角半径,尺寸 36 37 // 如果一个类想使用copy,必须要遵守NSCopying 38 39 UIView *smallCircleView = [self copy]; 40 41 42 43 // 把小圆添加badgeView的父控件 44 45 [self.superview insertSubview:smallCircleView belowSubview:self]; 46 47 }
由于上面直接使用copy复制一份来实现下面的那个View,具体下面那个View我前面已经介绍,所以我们需要让他遵守NSCoping协议,并且实现copyWithZone方法:
- <NSCopying>
1 - (id)copyWithZone:(NSZone *)zone 2 3 { 4 5 UIView *smallCircleView = [[UIView alloc] initWithFrame:self.frame]; 6 7 smallCircleView.backgroundColor = self.backgroundColor; 8 9 smallCircleView.layer.cornerRadius = self.layer.cornerRadius; 10 11 _smallCircleView = smallCircleView; 12 13 14 15 return _smallCircleView; 16 17 }
5:实现按钮拖动手势方法
在这之前需要定义一个地步View的属性,用来记录用户的一些操作
@property (nonatomic, weak) UIView *smallCircleView;
// 手指拖动的时候调用
1 - (void)pan:(UIPanGestureRecognizer *)pan 2 3 { 4 5 // 获取手指的偏移量 6 7 CGPoint transP = [pan translationInView:self]; 8 9 // 设置形变 10 11 // 修改形变不会修改center 12 13 CGPoint center = self.center; 14 15 center.x += transP.x; 16 17 center.y += transP.y; 18 19 self.center = center; 20 21 // self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y); 22 23 // 复位 24 25 [pan setTranslation:CGPointZero inView:self]; 26 27 28 29 // 计算两个圆的圆心距离 30 31 CGFloat d = [self distanceWithSmallCircleView:_smallCircleView bigCircleView:self]; 32 33 // 计算小圆的半径 34 35 CGFloat smallRadius = self.bounds.size.width * 0.5 - d / 10.0; 36 37 // 给小圆赋值 38 39 _smallCircleView.bounds = CGRectMake(0, 0, smallRadius * 2, smallRadius * 2); 40 41 // 注意小圆半径一定要改 42 43 _smallCircleView.layer.cornerRadius = smallRadius; 44 45 // 设置不规则的矩形路径 46 47 if (_smallCircleView.hidden == NO) {// 小圆显示的时候才需要描述不规则矩形 48 49 50 51 self.shapeL.path = [self pathWithSmallCircleView:_smallCircleView bigCircleView:self].CGPath; 52 53 } 54 55 // 拖动的时候判断下圆心距离是否大于50 56 57 if (d > 60) { // 粘性效果拖没 58 59 // 隐藏小圆 60 61 _smallCircleView.hidden = YES; 62 63 64 65 // 隐藏不规则的layer 66 67 // _shapeL.hidden = YES; 68 69 // 从父层中移除,就有吸附效果 70 71 [self.shapeL removeFromSuperlayer]; 72 73 } 74 // 手指抬起的业务逻辑 75 76 if (pan.state == UIGestureRecognizerStateEnded) { 77 78 79 if (d > 60) { 80 81 // 播放gif动画 82 83 // 创建UIImageView 84 85 UIImageView *imageV = [[UIImageView alloc] initWithFrame:self.bounds]; 86 87 NSMutableArray *images = [NSMutableArray array]; 88 89 if (_images == nil) { 90 91 for (int i = 1; i <= 8; i++) { 92 93 NSString *imageName = [NSString stringWithFormat:@"%d",i]; 94 95 UIImage *image = [UIImage imageNamed:imageName]; 96 97 [images addObject:image]; 98 99 } 100 101 }else{ 102 103 images = _images; 104 105 } 106 107 imageV.animationImages = images; 108 109 110 imageV.animationDuration = 1; 111 112 113 [imageV startAnimating]; 114 115 116 [self addSubview:imageV]; 117 118 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.9 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 119 120 [self removeFromSuperview]; 121 122 }); 123 }else{ // 两个圆心距离没有超过范围 124 // 弹簧效果 125 126 [UIView animateWithDuration:0.25 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{ 127 128 // badgeView还原到之前的位置,设置中心点为原来位置 129 130 self.center = _smallCircleView.center; 131 132 } completion:^(BOOL finished) { 133 }]; 134 135 // 小圆重新显示 136 137 _smallCircleView.hidden = NO; 138 139 // 不规则的矩形形状也需要干掉 140 141 [self.shapeL removeFromSuperlayer]; 142 } 143 } 144 }
6:下面就是本案例中最难的一部分,其实也不难,只不过涉及到了比较麻烦的计算
先来看图:
是不是感觉一片茫然,好吧哪里来根据下面的代码结合上面的图片,相信你会看懂。
提示一下,这里主要是计算ABCDOP四个点的位置(坐标)然后时候画图技术绘制并且填充这个区域,
// 根据两个控件描述不规则的路径
1 - (UIBezierPath *)pathWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView 2 3 { 4 5 // 小圆,x1,y1,r1 6 7 CGFloat x1 = smallCircleView.center.x; 8 9 CGFloat y1 = smallCircleView.center.y; 10 11 CGFloat r1 = smallCircleView.bounds.size.width * 0.5; 12 13 14 15 // 大圆,x2,y2,r2 16 17 CGFloat x2 = bigCircleView.center.x; 18 19 CGFloat y2 = bigCircleView.center.y; 20 21 CGFloat r2 = bigCircleView.bounds.size.width * 0.5; 22 23 24 25 // 计算两个圆心距离 26 27 CGFloat d = [self distanceWithSmallCircleView:smallCircleView bigCircleView:bigCircleView]; 28 29 30 31 if (d <= 0) return nil; 32 33 34 35 // cosθ 36 37 CGFloat cosθ = (y2 - y1) / d; 38 39 40 41 // sinθ 42 43 CGFloat sinθ = (x2 - x1) / d; 44 45 46 47 CGPoint pointA = CGPointMake(x1 - r1 * cosθ, y1 + r1 * sinθ); 48 49 CGPoint pointB = CGPointMake(x1 + r1 * cosθ, y1 - r1 * sinθ); 50 51 CGPoint pointC = CGPointMake(x2 + r2 * cosθ, y2 - r2 * sinθ); 52 53 CGPoint pointD = CGPointMake(x2 - r2 * cosθ, y2 + r2 * sinθ); 54 55 CGPoint pointO = CGPointMake(pointA.x + d * 0.5 * sinθ, pointA.y + d * 0.5 * cosθ); 56 57 CGPoint pointP = CGPointMake(pointB.x + d * 0.5 * sinθ, pointB.y + d * 0.5 * cosθ); 58 59 60 61 // 描述路径 62 63 UIBezierPath *path = [UIBezierPath bezierPath]; 64 65 66 67 // 设置起点 68 69 [path moveToPoint:pointA]; 70 71 72 73 // AB 74 75 [path addLineToPoint:pointB]; 76 77 78 79 // BC 80 81 [path addQuadCurveToPoint:pointC controlPoint:pointP]; 82 83 84 85 // CD 86 87 [path addLineToPoint:pointD]; 88 89 90 91 // DA 92 93 [path addQuadCurveToPoint:pointA controlPoint:pointO]; 94 95 96 97 return path; 98 99 } 100 101
由于前面设计到了计算两个控件的中心点之间的距离,所以我们将它抽出来,这样一看就懂,而且方便以后使用
// 获取两个控件之间圆心距离
1 - (CGFloat)distanceWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView 2 3 { 4 5 // 获取x轴偏移量 6 7 CGFloat offsetX = bigCircleView.center.x - smallCircleView.center.x; 8 9 10 11 // 获取y轴偏移量 12 13 CGFloat offsetY = bigCircleView.center.y - smallCircleView.center.y; 14 15 16 17 // 获取两个圆心的距离 18 19 CGFloat d = sqrtf((offsetX * offsetX + offsetY * offsetY)); 20 21 22 23 // sqrtf开根 24 25 return d; 26 27 }
由于前面设置到了拖动上面那个按钮需要实现地步View的半径的变化,并且实现两个控件之间填充控一些对应的,所以这里我们使用的是形变图层:
需要先定义一个图层属性
@property (nonatomic, weak) CAShapeLayer *shapeL;
然后懒加载他:
1 - (CAShapeLayer *)shapeL 2 3 { 4 5 if (_shapeL == nil) { 6 7 // 创建形状图层 8 9 // 利用形状图层 10 11 CAShapeLayer *shape = [CAShapeLayer layer]; 12 13 14 15 // 设置填充颜色 16 17 shape.fillColor = [UIColor redColor].CGColor; 18 19 20 21 [self.superview.layer insertSublayer:shape atIndex:0]; 22 23 24 25 _shapeL = shape; 26 27 } 28 29 return _shapeL; 30 31 } 32 33 34 35
注意:由于默认系统会讲控制器设置为自动约束,所以一半我们需要取消他:
self.view.translatesAutoresizingMaskIntoConstraints = NO;
下面说说怎么去使用它吧,
1:在界面拖一个按钮设置对应的frame,然后你只需要将对应按钮的class设置为我们自定义的按钮就可以,就这么多:
2:导入我们的按钮类,然后初始化他,并且设置对应的属性:
#import "iCocosBadgeView.h"
初始化控件:
1 iCocosBadgeView *bage = [[iCocosBadgeView alloc] init]; 2 3 bage.frame = CGRectMake(100, 100, 10, 10); 4 5 bage.backgroundColor = [UIColor redColor]; 6 7 [self.view addSubview:bage];
实现效果:
所有源码:
iCocosBadgeView.h文件的声明
1 #import <UIKit/UIKit.h> 2 3 @interface iCocosBadgeView : UIButton<NSCopying> 4 5 @property (nonatomic, strong) NSArray *images; 6 7 @end
iCocosBadgeView.m文件的实现
1 #import "iCocosBadgeView.h" 2 3 4 5 @interface iCocosBadgeView () 6 7 8 9 @property (nonatomic, weak) UIView *smallCircleView; 10 11 12 13 @property (nonatomic, weak) CAShapeLayer *shapeL; 14 15 16 17 @end 18 19 20 21 @implementation iCocosBadgeView 22 23 - (CAShapeLayer *)shapeL 24 25 { 26 27 if (_shapeL == nil) { 28 29 // 创建形状图层 30 31 // 利用形状图层 32 33 CAShapeLayer *shape = [CAShapeLayer layer]; 34 35 36 37 // 设置填充颜色 38 39 shape.fillColor = [UIColor redColor].CGColor; 40 41 42 43 [self.superview.layer insertSublayer:shape atIndex:0]; 44 45 46 47 _shapeL = shape; 48 49 } 50 51 return _shapeL; 52 53 } 54 55 - (void)awakeFromNib 56 57 { 58 59 [self setUp]; 60 61 } 62 63 64 65 - (instancetype)initWithFrame:(CGRect)frame 66 67 { 68 69 if (self = [super initWithFrame:frame]) { 70 71 72 73 [self setUp]; 74 75 } 76 77 return self; 78 79 } 80 81 82 83 // 初始化 84 85 - (void)setUp 86 87 { 88 89 // self.width 90 91 CGFloat w = self.frame.size.width; 92 93 94 95 96 97 // 设置圆角 98 99 self.layer.cornerRadius = w * 0.5; 100 101 102 103 // 设置字体 104 105 self.titleLabel.font = [UIFont systemFontOfSize:12]; 106 107 108 109 // 设置字体颜色 110 111 [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 112 113 114 115 // 添加手势 116 117 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; 118 119 [self addGestureRecognizer:pan]; 120 121 122 123 // 添加小圆,颜色一样,圆角半径,尺寸 124 125 // 如果一个类想使用copy,必须要遵守NSCopying 126 127 UIView *smallCircleView = [self copy]; 128 129 130 131 132 133 // 把小圆添加badgeView的父控件 134 135 [self.superview insertSubview:smallCircleView belowSubview:self]; 136 137 138 139 140 141 } 142 143 // 只要调用copy就会调用这个方法 144 145 - (id)copyWithZone:(NSZone *)zone 146 147 { 148 149 UIView *smallCircleView = [[UIView alloc] initWithFrame:self.frame]; 150 151 smallCircleView.backgroundColor = self.backgroundColor; 152 153 smallCircleView.layer.cornerRadius = self.layer.cornerRadius; 154 155 _smallCircleView = smallCircleView; 156 157 158 159 return _smallCircleView; 160 161 } 162 163 164 165 166 167 168 169 170 171 // 手指拖动的时候调用 172 173 - (void)pan:(UIPanGestureRecognizer *)pan 174 175 { 176 177 // 获取手指的偏移量 178 179 CGPoint transP = [pan translationInView:self]; 180 181 182 183 // 设置形变 184 185 // 修改形变不会修改center 186 187 CGPoint center = self.center; 188 189 center.x += transP.x; 190 191 center.y += transP.y; 192 193 self.center = center; 194 195 // self.transform = CGAffineTransformTranslate(self.transform, transP.x, transP.y); 196 197 198 199 // 复位 200 201 [pan setTranslation:CGPointZero inView:self]; 202 203 204 205 // 计算两个圆的圆心距离 206 207 CGFloat d = [self distanceWithSmallCircleView:_smallCircleView bigCircleView:self]; 208 209 210 211 // 计算小圆的半径 212 213 CGFloat smallRadius = self.bounds.size.width * 0.5 - d / 10.0; 214 215 216 217 // 给小圆赋值 218 219 _smallCircleView.bounds = CGRectMake(0, 0, smallRadius * 2, smallRadius * 2); 220 221 // 注意小圆半径一定要改 222 223 _smallCircleView.layer.cornerRadius = smallRadius; 224 225 226 227 228 229 // 设置不规则的矩形路径 230 231 if (_smallCircleView.hidden == NO) {// 小圆显示的时候才需要描述不规则矩形 232 233 234 235 self.shapeL.path = [self pathWithSmallCircleView:_smallCircleView bigCircleView:self].CGPath; 236 237 } 238 239 240 241 // 拖动的时候判断下圆心距离是否大于50 242 243 if (d > 60) { // 粘性效果拖没 244 245 // 隐藏小圆 246 247 _smallCircleView.hidden = YES; 248 249 250 251 // 隐藏不规则的layer 252 253 // _shapeL.hidden = YES; 254 255 // 从父层中移除,就有吸附效果 256 257 [self.shapeL removeFromSuperlayer]; 258 259 } 260 261 262 263 // 手指抬起的业务逻辑 264 265 if (pan.state == UIGestureRecognizerStateEnded) { 266 267 268 269 if (d > 60) { 270 271 // 播放gif动画 272 273 274 275 // 创建UIImageView 276 277 UIImageView *imageV = [[UIImageView alloc] initWithFrame:self.bounds]; 278 279 NSMutableArray *images = [NSMutableArray array]; 280 281 if (_images == nil) { 282 283 284 285 for (int i = 1; i <= 8; i++) { 286 287 NSString *imageName = [NSString stringWithFormat:@"%d",i]; 288 289 UIImage *image = [UIImage imageNamed:imageName]; 290 291 [images addObject:image]; 292 293 } 294 295 }else{ 296 297 images = _images; 298 299 } 300 301 302 303 304 305 imageV.animationImages = images; 306 307 308 309 imageV.animationDuration = 1; 310 311 312 313 [imageV startAnimating]; 314 315 316 317 [self addSubview:imageV]; 318 319 320 321 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.9 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 322 323 [self removeFromSuperview]; 324 325 }); 326 327 328 329 330 331 }else{ // 两个圆心距离没有超过范围 332 333 334 335 // 弹簧效果 336 337 [UIView animateWithDuration:0.25 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{ 338 339 // badgeView还原到之前的位置,设置中心点为原来位置 340 341 self.center = _smallCircleView.center; 342 343 344 345 } completion:^(BOOL finished) { 346 347 348 349 }]; 350 351 352 353 // 小圆重新显示 354 355 _smallCircleView.hidden = NO; 356 357 358 359 // 不规则的矩形形状也需要干掉 360 361 [self.shapeL removeFromSuperlayer]; 362 363 364 365 366 367 } 368 369 370 371 } 372 373 374 375 } 376 377 378 379 // 根据两个控件描述不规则的路径 380 381 - (UIBezierPath *)pathWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView 382 383 { 384 385 // 小圆,x1,y1,r1 386 387 CGFloat x1 = smallCircleView.center.x; 388 389 CGFloat y1 = smallCircleView.center.y; 390 391 CGFloat r1 = smallCircleView.bounds.size.width * 0.5; 392 393 394 395 // 大圆,x2,y2,r2 396 397 CGFloat x2 = bigCircleView.center.x; 398 399 CGFloat y2 = bigCircleView.center.y; 400 401 CGFloat r2 = bigCircleView.bounds.size.width * 0.5; 402 403 404 405 // 计算两个圆心距离 406 407 CGFloat d = [self distanceWithSmallCircleView:smallCircleView bigCircleView:bigCircleView]; 408 409 410 411 if (d <= 0) return nil; 412 413 414 415 // cosθ 416 417 CGFloat cosθ = (y2 - y1) / d; 418 419 420 421 // sinθ 422 423 CGFloat sinθ = (x2 - x1) / d; 424 425 426 427 CGPoint pointA = CGPointMake(x1 - r1 * cosθ, y1 + r1 * sinθ); 428 429 CGPoint pointB = CGPointMake(x1 + r1 * cosθ, y1 - r1 * sinθ); 430 431 CGPoint pointC = CGPointMake(x2 + r2 * cosθ, y2 - r2 * sinθ); 432 433 CGPoint pointD = CGPointMake(x2 - r2 * cosθ, y2 + r2 * sinθ); 434 435 CGPoint pointO = CGPointMake(pointA.x + d * 0.5 * sinθ, pointA.y + d * 0.5 * cosθ); 436 437 CGPoint pointP = CGPointMake(pointB.x + d * 0.5 * sinθ, pointB.y + d * 0.5 * cosθ); 438 439 440 441 // 描述路径 442 443 UIBezierPath *path = [UIBezierPath bezierPath]; 444 445 446 447 // 设置起点 448 449 [path moveToPoint:pointA]; 450 451 452 453 // AB 454 455 [path addLineToPoint:pointB]; 456 457 458 459 // BC 460 461 [path addQuadCurveToPoint:pointC controlPoint:pointP]; 462 463 464 465 // CD 466 467 [path addLineToPoint:pointD]; 468 469 470 471 // DA 472 473 [path addQuadCurveToPoint:pointA controlPoint:pointO]; 474 475 476 477 return path; 478 479 } 480 481 482 483 // 获取两个控件之间圆心距离 484 485 - (CGFloat)distanceWithSmallCircleView:(UIView *)smallCircleView bigCircleView:(UIView *)bigCircleView 486 487 { 488 489 // 获取x轴偏移量 490 491 CGFloat offsetX = bigCircleView.center.x - smallCircleView.center.x; 492 493 494 495 // 获取y轴偏移量 496 497 CGFloat offsetY = bigCircleView.center.y - smallCircleView.center.y; 498 499 500 501 // 获取两个圆心的距离 502 503 CGFloat d = sqrtf((offsetX * offsetX + offsetY * offsetY)); 504 505 506 507 // sqrtf开根 508 509 return d; 510 511 } 512 513 514 515 // 目的:取消系统高亮状态做的事情 516 517 - (void)setHighlighted:(BOOL)highlighted 518 519 { 520 521 522 523 } 524 525 @end 526 527