UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一)

版本记录

版本号 时间
V1.0 2020.05.04 星期一

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)
33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)

效果展示

首先看一个转盘效果

这个是一个礼物面板,具有转盘的效果,这个是同事实现的自定义UI。

下面我们就看下使用UICollectionView是怎么实现的。


代码实现

下面我们就看一下源码。

首先看下工程文件

下面就是源码了

1. ViewController.m
#import "ViewController.h"
#import "JJGiftCollectionView.h"

#define kCollectionViewHeight   300.0
#define kScreenWidth            [UIScreen mainScreen].bounds.size.width
#define kScreenHeight           [UIScreen mainScreen].bounds.size.height

CGFloat radius_ = 308;

@interface ViewController ()

@property (nonatomic, strong) JJGiftCollectionView *collectionView;


@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self initUI];
}

#pragma mark - Object Private Function

- (void)initUI
{
    self.collectionView.backgroundColor = [UIColor lightGrayColor];
    self.collectionView.contentOffset = CGPointMake(86 * 5, 0.0);
}

#pragma mark - Getter && Setter

- (JJGiftCollectionView *)collectionView
{
    if (!_collectionView) {
        _collectionView = [[JJGiftCollectionView alloc] initWithFrame:CGRectMake(0.0, kScreenHeight - kCollectionViewHeight, kScreenWidth, kCollectionViewHeight)];
        [self.view addSubview:_collectionView];
    }
    return _collectionView;
}

@end
2. JJGiftCollectionView.h
#import 

NS_ASSUME_NONNULL_BEGIN

@interface JJGiftCollectionView : UICollectionView

- (void)scrollToCenter;

@end

NS_ASSUME_NONNULL_END
3. JJGiftCollectionView.m
#import "JJGiftCollectionView.h"
#import "JJCollectionViewLayout.h"
#import "JJCollectionViewCell.h"

@interface JJGiftCollectionView() 

@property (nonatomic, assign) NSInteger selectedGiftIndex;
@property (nonatomic, assign) BOOL isScrolling;

@end

extern CGFloat radius_;

@implementation JJGiftCollectionView{
    NSInteger itemCount_;
}

#pragma mark - Override Base Function

- (instancetype)initWithFrame:(CGRect)frame
{
    JJCollectionViewLayout *layout = [[JJCollectionViewLayout alloc] init];
    layout.cellItemSize = CGSizeMake(86, 100);
    self = [super initWithFrame:frame collectionViewLayout:layout];
    if (self) {
        self.backgroundColor = UIColor.clearColor;
        self.showsVerticalScrollIndicator = NO;
        self.showsHorizontalScrollIndicator = NO;
        self.delegate = self;
        self.dataSource = self;
        [self registerClass:[JJCollectionViewCell class] forCellWithReuseIdentifier:NSStringFromClass(JJCollectionViewCell.class)];
        itemCount_ = 10;
        _selectedGiftIndex = 0;
    }
    return self;
}

#pragma mark - Object Private Function

- (UIColor *)randomColor
{
    CGFloat red = random() % 255 / 255.0;
    CGFloat green = random() % 255 / 255.0;
    CGFloat blue = random() % 255 / 255.0;
    return [UIColor colorWithRed:red green:green blue:blue alpha:1.];
}

- (void)scrollToSelectedIndex:(NSInteger)index
{
    self.selectedGiftIndex = index;
    [self setContentOffset:CGPointMake([self itemPerWidthFromIphone]*index, 0) animated: YES];
}

#pragma mark - Object Public Function

- (void)scrollToCenter
{
    if (itemCount_ == 0) {
        return ;
    }
    [self scrollToSelectedIndex:floorf(itemCount_/2)];
}

- (CGFloat)itemPerWidthFromIphone
{
    CGFloat width = self.contentSize.width;
    NSInteger count = itemCount_;
    CGFloat anglePerItem = atan((width/count) / (radius_+25));
    CGFloat angleAtExtreme = [self numberOfItemsInSection:0] > 0 ? -([self numberOfItemsInSection:0] -1)*anglePerItem : 0;
    CGFloat factor = -angleAtExtreme/(self.contentSize.width - CGRectGetWidth(self.bounds));
    return anglePerItem/factor;
}

#pragma - mark - UICollectionViewDelegate

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    JJCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass(JJCollectionViewCell.class) forIndexPath:indexPath];
    cell.backgroundColor = [self randomColor];
    return cell;
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.row == _selectedGiftIndex || indexPath.row >= itemCount_) {
        return ;
    }
    _selectedGiftIndex = indexPath.row;
    [self scrollToSelectedIndex:_selectedGiftIndex];
}

#pragma mark - ScrollViewDelegate

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    CGFloat perItemWidth = [self itemPerWidthFromIphone];
    CGFloat index = (scrollView.contentOffset.x)/perItemWidth;
    NSInteger selectIndex = round(index);
    NSIndexPath *indexPath = [NSIndexPath indexPathForItem:selectIndex inSection:0];
    [self collectionView:self didSelectItemAtIndexPath:indexPath];
    self.isScrolling = NO;
}

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
    self.isScrolling = YES;
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    self.isScrolling = YES;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    self.isScrolling = YES;
}

@end
4. JJCollectionViewLayout.h
#import 

NS_ASSUME_NONNULL_BEGIN

@interface JJCollectionViewLayout : UICollectionViewLayout

@property (nonatomic, assign) CGSize cellItemSize;// 单元格大小
@property (nonatomic, assign) CGFloat radius; // 圆环半径
@property (nonatomic, strong) NSIndexPath *selectedIndexPath; // 选中的单元格
@property (nonatomic, assign) CGFloat scrollContentOffsetX;

@end

@interface JJCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes

@property (nonatomic, assign) CGPoint anchorPoint; // 锚点
@property (nonatomic, assign) CGFloat angle; // 角度

@end

NS_ASSUME_NONNULL_END
5. JJCollectionViewLayout.m
#import "JJCollectionViewLayout.h"

@interface JJCollectionViewLayout ()

@property (nonatomic, strong) NSMutableArray  *attributeItems;
@property (nonatomic, assign) CGRect changeRect;

@end

extern CGFloat radius_;

@implementation JJCollectionViewLayout

- (void)setRadius:(CGFloat)radius
{
    _radius = radius;
    
    [self invalidateLayout];
}

+ (Class)layoutAttributesClass
{
    return JJCollectionViewLayoutAttributes.class;
}

// 当滑到极端时  第0个item的角度
- (CGFloat)angleAtExtreme
{
    return [self.collectionView numberOfItemsInSection:0] > 0 ? -([self.collectionView numberOfItemsInSection:0] -1)*self.anglePerItem : 0;
}
// 滑动时 第0个角度
- (CGFloat)angle
{
    return (self.angleAtExtreme * self.collectionView.contentOffset.x)/(self.collectionViewContentSize.width - self.collectionView.bounds.size.width);
}

- (CGFloat)anglePerItem
{
    return atan((self.cellItemSize.width) / self.radius);
}

- (CGSize)collectionViewContentSize
{
    return CGSizeMake([self.collectionView numberOfItemsInSection:0] * self.cellItemSize.width, CGRectGetHeight(self.collectionView.bounds));
}

- (void)setCellItemSize:(CGSize)cellItemSize
{
    _cellItemSize = cellItemSize;
}

- (instancetype)init
{
    if (self == [super init]) {
        _radius = radius_ - 50;
        _attributeItems = [[NSMutableArray alloc] init];
        _cellItemSize = CGSizeMake(86, 100);
    }
    return self;
}

- (void)prepareLayout
{
    [super prepareLayout];
    
    CGFloat centerX = self.collectionView.contentOffset.x + CGRectGetWidth(self.collectionView.bounds)/2.0;
    CGFloat anchorPointY = ((self.cellItemSize.height/2.0) + self.radius)/self.cellItemSize.height;
    // 只计算在屏幕中的item 正切
    CGFloat theTan = atan2(CGRectGetWidth(self.collectionView.bounds)/2.0, self.radius + self.cellItemSize.height/2.0 - CGRectGetHeight(self.collectionView.bounds)/2.0);
    NSInteger startIndex = 0;
    NSInteger endIndex = [self.collectionView numberOfItemsInSection:0] - 1;
    if (self.angle < -theTan) {
        startIndex = floor((-theTan - self.angle)/self.anglePerItem);
    }
    endIndex = MIN(endIndex, ceil((theTan - self.angle)/self.anglePerItem));
    if (endIndex < startIndex) {
        endIndex = 0;
        startIndex = 0;
    }
    
    for (NSInteger index = startIndex; index <= endIndex; index ++) {
        JJCollectionViewLayoutAttributes *attributes = [JJCollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
        attributes.size = self.cellItemSize;
        attributes.center = CGPointMake(centerX, CGRectGetMidY(self.collectionView.bounds) - 25);
        attributes.angle = self.angle + (self.anglePerItem * index);
        attributes.anchorPoint = CGPointMake(0.5, anchorPointY);
        [self.attributeItems addObject:attributes];
    }
}
// 固定item在中间位置
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGPoint finalContentOffset = proposedContentOffset;
    CGFloat factor = -self.angleAtExtreme/(self.collectionViewContentSize.width - CGRectGetWidth(self.collectionView.bounds));
    CGFloat proposedAngle = proposedContentOffset.x * factor;
    CGFloat ratio = proposedAngle/self.anglePerItem;
    CGFloat multiplier = 0.0;
    if (velocity.x > 0) {
        multiplier = ceil(ratio);
    } else if (velocity.x < 0) {
        multiplier = floor(ratio);
    } else {
        multiplier = round(ratio);
    }
    finalContentOffset.x = multiplier*self.anglePerItem/factor;
    self.scrollContentOffsetX = finalContentOffset.x;
    return finalContentOffset;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attributeItems;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return self.attributeItems[indexPath.item];
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

@end


@implementation JJCollectionViewLayoutAttributes

#pragma mark - Override Base Function

- (instancetype)init
{
    if (self = [super init]) {
        _anchorPoint = CGPointMake(0.5, 0.5);
        _angle = 0;
    }
    return self;
}

- (id)copyWithZone:(NSZone *)zone
{
    JJCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
    attributes.anchorPoint = self.anchorPoint;
    attributes.angle = self.angle;
    return attributes;
}

#pragma mark - Getter && Setter

- (void)setAnchorPoint:(CGPoint)anchorPoint
{
    _anchorPoint = anchorPoint;
}

- (void)setAngle:(CGFloat)angle
{
    _angle = angle;
    self.zIndex = angle * 1000000;
    self.transform = CGAffineTransformMakeRotation(angle);
}

@end
6. JJCollectionViewCell.h
#import 

NS_ASSUME_NONNULL_BEGIN

@interface JJCollectionViewCell : UICollectionViewCell

@end

NS_ASSUME_NONNULL_END
7. JJCollectionViewCell.m
#import "JJCollectionViewCell.h"
#import "JJCollectionViewLayout.h"

@implementation JJCollectionViewCell

#pragma mark - Override Base Function

- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    [super applyLayoutAttributes:layoutAttributes];
    
    if ([layoutAttributes isKindOfClass:[JJCollectionViewLayoutAttributes class]]) {
        JJCollectionViewLayoutAttributes *attributes = (JJCollectionViewLayoutAttributes *)layoutAttributes;
        self.layer.anchorPoint = attributes.anchorPoint;
        CGFloat y = self.layer.position.y;
        y += (attributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds);
        CGPoint center = CGPointMake(self.layer.position.x, y);
        self.layer.position = center;
        
        self.contentView.transform = CGAffineTransformMakeRotation(-attributes.angle);
    }
}

@end

下面就是实际实现效果,具体每个item里面放什么view就需要自己去定义和实现了。

后记

本篇主要讲述了基于CollectionView转盘效果的实现,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一))