UIKit框架(三十八) —— 基于CollectionView轉(zhuǎn)盤效果的實(shí)現(xiàn)(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2020.05.04 星期一

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面,用戶交互也是通過UIKit進(jìn)行的。感興趣的參考上面幾篇文章吠冤。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用尼酿、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實(shí)現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場(chǎng)和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場(chǎng)和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定義布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定義布局 (二)
28. UIKit框架(二十八) —— 一個(gè)UISplitViewController的簡(jiǎn)單實(shí)用示例 (一)
29. UIKit框架(二十九) —— 一個(gè)UISplitViewController的簡(jiǎn)單實(shí)用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的簡(jiǎn)單示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的簡(jiǎn)單示例(二)
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的使用(二)

效果展示

首先看一個(gè)轉(zhuǎn)盤效果

這個(gè)是一個(gè)禮物面板收毫,具有轉(zhuǎn)盤的效果攻走,這個(gè)是同事實(shí)現(xiàn)的自定義UI殷勘。

下面我們就看下使用UICollectionView是怎么實(shí)現(xiàn)的。


代碼實(shí)現(xiàn)

下面我們就看一下源碼昔搂。

首先看下工程文件

下面就是源碼了

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 <UIKit/UIKit.h>

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() <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>

@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 <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface JJCollectionViewLayout : UICollectionViewLayout

@property (nonatomic, assign) CGSize cellItemSize;// 單元格大小
@property (nonatomic, assign) CGFloat radius; // 圓環(huán)半徑
@property (nonatomic, strong) NSIndexPath *selectedIndexPath; // 選中的單元格
@property (nonatomic, assign) CGFloat scrollContentOffsetX;

@end

@interface JJCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes

@property (nonatomic, assign) CGPoint anchorPoint; // 錨點(diǎn)
@property (nonatomic, assign) CGFloat angle; // 角度

@end

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

@interface JJCollectionViewLayout ()

@property (nonatomic, strong) NSMutableArray <JJCollectionViewLayoutAttributes *> *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;
}

// 當(dāng)滑到極端時(shí)  第0個(gè)item的角度
- (CGFloat)angleAtExtreme
{
    return [self.collectionView numberOfItemsInSection:0] > 0 ? -([self.collectionView numberOfItemsInSection:0] -1)*self.anglePerItem : 0;
}
// 滑動(dòng)時(shí) 第0個(gè)角度
- (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;
    // 只計(jì)算在屏幕中的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<UICollectionViewLayoutAttributes *> *)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 <UIKit/UIKit.h>

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

下面就是實(shí)際實(shí)現(xiàn)效果玲销,具體每個(gè)item里面放什么view就需要自己去定義和實(shí)現(xiàn)了。

后記

本篇主要講述了基于CollectionView轉(zhuǎn)盤效果的實(shí)現(xiàn)摘符,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贤斜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子逛裤,更是在濱河造成了極大的恐慌瘩绒,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件带族,死亡現(xiàn)場(chǎng)離奇詭異锁荔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蝙砌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門阳堕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人择克,你說我怎么就攤上這事恬总。” “怎么了肚邢?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵壹堰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我骡湖,道長(zhǎng)贱纠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任勺鸦,我火速辦了婚禮并巍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘换途。我一直安慰自己懊渡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布军拟。 她就那樣靜靜地躺著剃执,像睡著了一般。 火紅的嫁衣襯著肌膚如雪懈息。 梳的紋絲不亂的頭發(fā)上肾档,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼怒见。 笑死俗慈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遣耍。 我是一名探鬼主播闺阱,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼舵变!你這毒婦竟也來了酣溃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤纪隙,失蹤者是張志新(化名)和其女友劉穎赊豌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绵咱,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碘饼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了麸拄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片派昧。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖拢切,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秆吵,我是刑警寧澤淮椰,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站纳寂,受9級(jí)特大地震影響主穗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜毙芜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一忽媒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腋粥,春花似錦晦雨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至展辞,卻和暖如春奥邮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工洽腺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脚粟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓蘸朋,卻偏偏與公主長(zhǎng)得像核无,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子度液,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353