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 *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;

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

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

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

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

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

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

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

- (void)touchesEnded:(NSSet *)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

你可能感兴趣的:(iOS 自定义图片选择器 3 - 相册列表的实现(UICollectionView))