先來張效果圖:
之前項目中利用UICollectionView實現了一個圓環形分布的菜單,但不能隨著手勢進行旋轉,正好這兩天放假,參考一些大神的文章,搗鼓出了可以進行旋轉的菜單。下面說一下代碼實現。
1.由於UICollectionVIew是依賴於UICollectionViewFlowLayout進行布局,而UICollectionViewFlowLayout 繼承自 UICollectionViewLayout,所以如果我們想進行個人化布局,可以建立一個UICollectionViewLayout的子類來進行個人化布局。
#import <UIKit/UIKit.h>@interface YMLRotationLayout : UICollectionViewLayout/** 按鈕半徑 */@property (assign, nonatomic) CGFloat itemRadius;/** 按鈕中心相對菜單中心旋轉角度*/@property (assign, nonatomic) CGFloat rotationAngle;@end
#import "YMLRotationLayout.h"@implementation YMLRotationLayout{ NSMutableArray * _attributeAttay; CGFloat _rLength; NSInteger _itemCount;}- (void)prepareLayout{ [super prepareLayout]; // 按鈕個數 _itemCount = (int)[self.collectionView numberOfItemsInSection:0]; _attributeAttay = [[NSMutableArray alloc] init]; // 先設定大圓的半徑 取長和寬最短的 CGFloat radius = MIN(self.collectionView.frame.size.width, self.collectionView.frame.size.height) / 2.2; // 圓心位置 CGPoint center = CGPointMake(self.collectionView.frame.size.width / 2.0, self.collectionView.frame.size.height / 2.0); _rLength = _itemRadius; // 設定每個item的大小 for (int idx = 0; idx < _itemCount; idx ++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:0]; UICollectionViewLayoutAttributes * attris = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; // 設定item大小 attris.size = CGSizeMake(_rLength, _rLength); if (_itemCount == 1) { attris.center = self.collectionView.center; } else { // 計算每個item的圓心位置 /* . . . . . r . . ......... */ // 計算每個item中心的座標 // 算出的x,y值還要減去item自身的半徑大小 float x = center.x + cosf(2 * M_PI / _itemCount * idx + _rotationAngle) * (radius - _rLength / 2.0); float y = center.y + sinf(2 * M_PI / _itemCount * idx + _rotationAngle) * (radius - _rLength / 2.0); attris.center = CGPointMake(x, y); } [_attributeAttay addObject:attris]; }}// contentSize 的大小- (CGSize)collectionViewContentSize{ return self.collectionView.frame.size;}// cell / header / footer 的frame數組- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect{ return _attributeAttay;}@end
2.由於需要監聽滑動手勢以調整布局,所以需要重載UICollectionVIew 事件方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ CGPoint centerPoint = self.center; UITouch *touch = touches.anyObject; CGPoint point = [touch locationInView:self]; CGFloat rLength = sqrt((point.x - centerPoint.x) * (point.x - centerPoint.x) + (point.y - centerPoint.y) * (point.y - centerPoint.y)); // 手勢範圍限制 if (!(rLength <= [self.largeRadius floatValue] && rLength >= [self.smallRadius floatValue])) { return; } [[NSNotificationCenter defaultCenter] postNotificationName:@"touchBegin" object:nil userInfo:@{@"x":[NSString stringWithFormat:@"%f",point.x],@"y":[NSString stringWithFormat:@"%f",point.y]}];}- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ CGPoint centerPoint = self.center; UITouch *touch = touches.anyObject; CGPoint point = [touch locationInView:self]; CGFloat rLength = sqrt((point.x - centerPoint.x) * (point.x - centerPoint.x) + (point.y - centerPoint.y) * (point.y - centerPoint.y)); // 手勢範圍限制 if (!(rLength <= [self.largeRadius floatValue] && rLength >= [self.smallRadius floatValue])) { return; } [[NSNotificationCenter defaultCenter] postNotificationName:@"touchMoving" object:nil userInfo:@{@"x":[NSString stringWithFormat:@"%f",point.x],@"y":[NSString stringWithFormat:@"%f",point.y]}];}
3.上面兩步完成之後,再依據滑動前後手勢的位置,計算滑動角度,重新計算每個item的座標,然後重新布局
#pragma mark -- 按鈕滑動,重新布局// 滑動開始- (void)touchBegin:(NSNotification *)sender{ if (!_rotate) return; _centerPoint = self.collectionView.center; NSDictionary *dic = sender.userInfo; CGPoint point = CGPointMake([dic[@"x"] floatValue], [dic[@"y"] floatValue]); _lastPoint = point;}// 正在滑動中- (void)touchMoving:(NSNotification *)sender{ if (!_rotate) return; NSDictionary *dic = sender.userInfo; CGPoint point = CGPointMake([dic[@"x"] floatValue], [dic[@"y"] floatValue]); // 以collectionView center為中心計算滑動角度 CGFloat rads = [self angleBetweenFirstLineStart:_centerPoint firstLineEnd:_lastPoint andSecondLineStart:_centerPoint secondLineEnd:point]; if (_lastPoint.x != _centerPoint.x && point.x != _centerPoint.x) { CGFloat k1 = (_lastPoint.y - _centerPoint.y) / (_lastPoint.x - _centerPoint.x); CGFloat k2 = (point.y - _centerPoint.y) / (point.x - _centerPoint.x); if (k2 > k1) { _totalRads += rads; } else { _totalRads -= rads; } } _layout.rotationAngle = _totalRads; // 重新布局 [_layout invalidateLayout]; // 更新記錄點 _lastPoint = point;}// 兩條直線之間的夾角- (CGFloat)angleBetweenFirstLineStart:(CGPoint)firstLineStart firstLineEnd:(CGPoint)firstLineEnd andSecondLineStart:(CGPoint)secondLineStart secondLineEnd:(CGPoint)secondLineEnd{ CGFloat a1 = firstLineEnd.x - firstLineStart.x; CGFloat b1 = firstLineEnd.y - firstLineStart.y; CGFloat a2 = secondLineEnd.x - secondLineStart.x; CGFloat b2 = secondLineEnd.y - secondLineStart.y; // 夾角餘弦 double cos = (a1 * a2 + b1 * b2) / (sqrt(pow(a1, 2) + pow(b1, 2)) * sqrt(pow(a2, 2) + pow(b2, 2))); // 浮點計算結果可能超過1,需要控制 cos = cos > 1 ? 1 : cos; return acos(cos);}
demo地址:https://github.com/HuberyYang/YMLMenuDemo.git
本文新地址:iOS使用UICollectionView實現可旋轉菜單