iOS 自定义图片选择器 3 - 相册列表的实现(UICollectionView)

写在前面
笔者按照Instagram的图片选取器写了个小Demo,
该系列文章是以实现Demo目的来逐个介绍使用到的东西,若有不正确的地方还望指出来,共同学习。
地址:https://github.com/BigBigPo/RJPhotoPicker


UICollectionView,自从推出一来就受到广大的iOS开发者对其赞不绝口,其高度的灵活性使得其自身的可定制化程度极高。开发者们用其做出了诸多绚丽的效果。

这要归功于UICollectionViewLayout,它CollectionView进行自定义布局的重要基石,只有掌握了它才能说自己掌握了CollectionView,Layout到底有多强大,笔者会单独新开一篇来介绍。本节主要还是以达到我们系列文章所要实现的Demo效果为目的,对CollectionView有一个简单的介绍。

好啦,我们先看一下我们要仿照的Instagram的图片选择器


Insshow.gif

上一节我们实现了上方“展示区”的效果,十分简单,核心就是UIScrollView的缩放与图片的布局更新。这一节我们要实现下方的列表。

下方的列表可以直接想到UICollectionView,它对于这种类型的列表再适合不过了,实现起来非常简单,好在Instagram的列表部分并不复杂,我们不需要自己去自定义collectionViewLayout,使用系统提供的UICollectionViewFlowLayout就可以轻松实现这种流式布局的效果。

至于UICollectionView与UITableView的关系······
UICollectionView完全可以实现UITableView的效果,之前看到有大牛发现了UIKit框架中更新了UICollectionViewTableLayout这样的东西(似乎是这个名字,不知真假。)【iOS14中已添加,详见Lists】,两者的关系就不言而喻了。

1. UICollectionView的基本使用

UICollectionView的使用上与UITableVIew极其类似,两者都是继承自UIScrollView,具备ScrollView的所有特性,UICollectionView的初始化是这样的

    _collectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
    [_collectionView setDelegate:self];
    [_collectionView setDataSource:self];
    [_collectionView registerNib:[UINib nibWithNibName:@"RJPhotoCell" bundle:nil] forCellWithReuseIdentifier:RJPhotoPickerCellID];

初始化时附带了layout,布局,决定了UICollectionView会以何种方式展示,如行间距,item的大小,间隔大小等等,这些会在谈layout的文章中再展开,我们的相册选择器直接用系统提供的流式布局UICollectionViewFlowLayout,不用我们自己操心如何去写布局的代码。

    UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc] init];
    layout.itemSize = cellSize;
    layout.minimumInteritemSpacing = 1;
    layout.minimumLineSpacing = 1;

我们这里只设置了间距与大小,这些就足够了,当然,这些配置也
可以留到layout的代理里面设置,但我们的选择器并没有复杂的布局,专门写在代理里有点奇怪。

顺带提一下,UICollectionViewFlowLayout最重要的属性 scrollDirection没有在这里进行设置,其包含两种流式布局的方向:

typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {
    UICollectionViewScrollDirectionVertical,
    UICollectionViewScrollDirectionHorizontal
};

其默认情况下就是垂直方向布局(UICollectionViewScrollDirectionVertical),也就是我们需要的样子。

常用代理方法
//section的数量
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;
//对应section的item数量
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
//cell的代理,只要是继承自UICollectionViewCell的都可以
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
//header与footer,kind是header与footer的类型区别
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;

//item的点击事件
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
//当item即将显示的时候回触发该代理
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0);

是不是很眼熟,怎么跟UITableView的代理那么相似?是的,他们在设计上是一样的,只是UICollectionView更加的灵活。这里就不再对代理进行赘述了,用法跟UITableView是一样的,更多的的代理可以自行查看,对于我们要实现的demo,有这些就足够了。

掌握了这些基本的知识点就完全可以用UICollectionView实现一个流式布局的列表。

2. 列表与展示区域的联动

ins的展示区域与列表是有联动效果的,具体如下:


(1) 当向上滑动列表时,上方展示区域会上移至只留下40px左右的大小。且该效果需要在上滑列表直至手指触碰到展示区域边界才会开始触发。

(2)当下滑动列表时,展示区域会在列表滑动至顶端时开始跟随列表滑动,直至占据一半的屏幕为止。

(3)展示区域的下方越有40px高度的区域有手势效果,可以将处于隐藏状态的展示区域拖动显示出来。


前两点我们会放在列表(UICollectionView上做),我们要去获取什么数据来知道列表滚动了?并且能够拿到对应的数值?

这个时候会首先想到UICollectionView是继承自UIScrollView的,想要知道是否滑动,并且要拿到滑动的数值,UIScrollView是提供的有代理的:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;  

该代理只要有滑动的事件发生都会被触发,我们可以根据拿到scrollView的contentOffset来获取滑动的数据。然后根据得到的数值来推断出展示区所应该在的位置。

但是

UIScrollView提供的代理(didScroll)是在滑动发生后才会触发,是已经滑动了,这个时候我们若根据此数据来决定上方展示区域的位置,而我们的联动是及时的,若在scrollView后再根据拿到的数据来处理,则会造成显示异常,例如,效果(1)中需要展示区域联动时,展示区域会发生轻微的抖动(这个肯定不能忍)。

联动效果,并不仅仅需要滑动数值,还需要更加详尽的滑动状态,如:滑动的开始与结束,滑动距离,方向等等,以此来触发是否需要计算上方展示区域的位置。若只是根据UIScrollView的代理来获取的确是有点复杂,而且也会存在刚提到的【滑动数据获取的时机】问题,我们需要及时的获取到滑动的数据。

这个时候笔者想到的是这几个方法:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

在这几个方法这里来获取数据,时机肯定是没问题了,但笔者担心在此处若操作不当会引发其他地方的手势冲突。但我们并没有对视图进行任何操作,我们只是获取到其数据即可,不存在手势冲突的可能性。

抱着试试看的心态,笔者生成了一个UICollectionView的子类,在子类中实现这些方法,以此来获取所有我们需要的信息,笔者采用一个Block来进行数据的统一回调:

//数据的回调,滑动开始的y坐标点(x的坐标对于该demo效果没有意义),Y轴的移动距离,以及动作完成的标识。
typedef void(^ScrollToTopMoreBlock)(CGFloat startY, CGFloat moveY, BOOL isEnd);

而整个子类的功能,都只是在围绕获取滑动的数据来进行的:

- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view {
    //滑动开始
    UITouch * touch = [touches anyObject];
    _touchPoint = [touch locationInView:self];
    _isTouch = YES;
    return YES;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //滑动开始
    [super touchesBegan:touches withEvent:event];
    UITouch * touch = [touches anyObject];
    _isTouch = YES;
    _touchPoint = [touch locationInView:self];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    //滑动中,获取滑动的距离,并回调
    UITouch * touch = [touches anyObject];
    _isTouch = YES;
    CGPoint movePoint = [touch locationInView:self];
    if (_moveBlock) {
            _moveBlock(_touchPoint.y, movePoint.y - _touchPoint.y, NO);
        }
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    //滑动结束
    [self endScrollTopEventWithTouch:[touches anyObject]];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    //滑动结束
    [self endScrollTopEventWithTouch:[touches anyObject]];
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        gestureRecognizer.cancelsTouchesInView = NO;
    }
    return YES;
}

- (void)setScrollBlock:(void(^)(CGFloat startY, CGFloat moveY, BOOL isEnd))block {
    _moveBlock = block;
}

- (void)endScrollTopEventWithTouch:(UITouch *)touch {
    if (_moveBlock) {
        _isTouch = YES;
        CGPoint movePoint = [touch locationInView:self];
        _moveBlock(_touchPoint.y, movePoint.y - _touchPoint.y, YES);
    }
}

有了这些实时的数据,我们就可以完成我们的联动效果。联动的逻辑稍稍有点复杂,这里就不列举出来了···可以先自行实现以下看看,若没有什么思路,可以参考下笔者的方式。笔者的方式稍显笨拙,并不适宜在此处展开。感兴趣的朋友可以看看Demo。

其他 CollectionView 相关内容:

1. iOS 自定义图片选择器 3 - 相册列表的实现
2. UICollectionView自定义布局基础
3. UICollectionView自定义拖动重排
4. iOS13 中的 CompositionalLayout 与 DiffableDataSource
5. iOS14 中的UICollectionViewListCell、UIContentConfiguration 以及 UIConfigurationState

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容