Table of Content

UITableView 是 iOS 开发中常用的一个控件,可惜他不支持横向滚动

UICollectionView 似乎支持横向滚动,但是本着闲(ye)着(wu)蛋(xu)疼(yao)的原则,我还是决定手动实现一个横向滚动的 UITableView

实现的过程中发现一些比较好玩的问题,比如

  1. 当回收某个 view 的时候,用 removeFromSuperView,会造成页面卡糊糊的,重用某个 view 的时候,addSubview 方法也是卡糊糊的
  2. 所以我选择用 hidden 属性来代替这两个方法,CPU 占用和内存占用确实有大幅下降,如图
  3. scrollViewDidScroll: 方法随着滑动的速度加快,调用频次是越来越低,如果想根据 contentOffset 来做一些判断,就不太容易了。即使在滑动结束再做一次运算抢救一下,好像还是有点问题,也有可能我代码写的有问题
  4. 相似的,虽然我没实现 delete 和 insert 操作,即使要 delete 某个 cell,尽量用 hidden,不要用 removeFromSuperView
  5. 具体的实现就是在 alloc init 的时候 addSubview 从此之后只用 hidden 属性
  6. 这样可能会造成一些问题,比如如果后面的 cell 的 size 很大,实际上 reusableViewPool 里面有很多 object 是再也没有出头之日了,这个问题的话,UITableView 现在也是这么实现的,如果想测试可以在 cell 里面写上 dealloc 方法看看什么时候调用
  7. UIScrollView 里面默认有两个 UIImageView 这是滚动条的 image,想要去除也很简单,加上 _scrollView.showsVerticalScrollIndicator = NO_scrollView.showsHorizontalScrollIndicator = NO

接下来是实现过程


每个 view 最好本身可以有个属性来记录自己的 index,但是用 tag 来做不太好,万一外面有用到这个 tag,所以最好自定义一个,继承自 UIView,加上 itemIndex 这个变量,当然以后也能扩充更多功能,比如响应点击事件之类的,这次由于时(ye)间(wu)关(wu)系(guan),就没实现这个功能

要展示的 cell 类继承自这个 ItemView 就行了


首先是 .h 文件

Delegate 至少要有这么几个方法吧

@protocol BayCarouselViewDelegate <NSObject, UIScrollViewDelegate>

@optional
// Display customization
- (void)carouselView:(BayCarouselView *)carouselView willDisplayView:(BayCarouselItemView *)view forRowAtIndex:(NSInteger)index;
- (void)carouselView:(BayCarouselView *)carouselView didEndDisplayView:(BayCarouselItemView *)view forRowAtIndex:(NSInteger)index;

- (void)carouselView:(BayCarouselView *)carouselView didSelectRowAtIndex:(NSInteger)index;
- (void)carouselView:(BayCarouselView *)carouselView didDeSelectRowAtIndex:(NSInteger)index;

- (void)carouselView:(BayCarouselView *)carouselView currentIndex:(NSInteger)index;

@end

Datasource 至少要有这么几个方法吧

@protocol BayCarouselViewDataSource <NSObject>

@required

- (NSInteger)numberOfRowInCarouselView:(BayCarouselView *)carouselView;
- (BayCarouselItemView *)carouselView:(BayCarouselView *)carouselView viewForRowAtIndex:(NSInteger)index;
@end

再在这个 view 下面加上如下属性

@property (nonatomic, weak) __nullable id <BayCarouselViewDataSource> dataSource;
@property (nonatomic, weak) __nullable id <BayCarouselViewDelegate> delegate;
@property (nonatomic, assign) CGFloat rowWidth;
@property (nonatomic, assign) BOOL clipsToBounds;
@property (nonatomic, assign) BOOL pagingEnabled;

- (void)reloadData;
- (__kindof BayCarouselItemView *)dequeueReusableView;
- (void)registerClass:(nullable Class)viewClass;
- (void)scrollToIndex:(NSInteger)index animate:(BOOL)animate;
- (void)resetData;

__kindof 是在 WWDC 2015 上提出来的,加上 __kindof 修饰符,就不再需要强制转换类型了,最典型的例子就是 UITableViewCell,而且加上这个修饰符,可以在某些场景下消除一些不必要的警告

另外,为了避免编译器的警告,我们在文件头尾分别加上

NS_ASSUME_NONNULL_BEGIN

...

NS_ASSUME_NONNULL_END  

关于 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END,这一对儿宏也是在 WWDC 2015 提出的,就不再需要开发者写一大堆 nonnull

To ease adoption of the new annotations, you can mark certain regions of your Objective-C header files as audited for nullability. Within these regions, any simple pointer type will be assumed to be nonnull. -- See Nullability and Objective-C - https://developer.apple.com/swift/blog/?id=25


关于 .m 文件就没什么好说的了,大部分 method 都是日常很常用的,核心方法就是在 scrollViewDidScroll: 里面决定谁 hidden 掉

scrollViewDidScroll: 方法实在太长了,我写最后面,感兴趣的就看看

关于 registerClass: 的实现,简单粗暴

- (void)registerClass:(Class)viewClass {
    self.registerClass = viewClass;
}

关于 dequeueReusableView: 的实现

- (BayCarouselItemView *)dequeueReusableView {
    if (self.reusableViewPool.count > 0) {
        BayCarouselItemView *view = [self.reusableViewPool anyObject];
        view.transform = CGAffineTransformIdentity;
        [self.reusableViewPool removeObject:view];
        return view;
    } else {
        BayCarouselItemView *view = [[self.registerClass alloc] init];
        [self.scrollView addSubview:view];
        return view;
    }
}

关于 queueReusableView: 的实现,那就更简单了

- (void)queueReusableView:(BayCarouselItemView *)view {
    [self.reusableViewPool addObject:view];
}

最后是 scrollViewDidScroll: 方法

我用的方法比较简单,如果有更好的算法,欢迎推荐给我,感谢

判断哪个 view 越界,并且将它 hidden 掉

    for (BayCarouselItemView *view in scrollView.subviews) {
        if ([view isKindOfClass:self.registerClass]) {
            if (CGRectGetMaxX(view.frame) < minX - padding * 2 || CGRectGetMinX(view.frame) > maxX + padding * 2) {

                if (!view.hidden) {
                    [self queueReusableView:view];
#ifdef DEBUG
                    //NSLog(@"remove %ld", view.itemIndex);
#endif
                    view.hidden = YES;
                    if ([self.delegate respondsToSelector:@selector(carouselView:didEndDisplayView:forRowAtIndex:)]) {
                        [self.delegate carouselView:self didEndDisplayView:view forRowAtIndex:view.itemIndex];
                    }
                }
            }
        }
    }

判断哪个地方有空缺,并且给它加上去

    NSArray *subviewsArray = [self.scrollView.subviews sortedArrayUsingComparator:^NSComparisonResult(BayCarouselItemView *obj1, BayCarouselItemView *obj2) {

        return [[NSNumber numberWithFloat:obj1.frame.origin.x] compare:[NSNumber numberWithFloat:obj2.frame.origin.x]];
    }];

    BayCarouselItemView *firstView;
    for (NSInteger i = 0; i < subviewsArray.count; i++) {
        if ([subviewsArray[i] isKindOfClass:self.registerClass]) {
            if (!((BayCarouselItemView *)(subviewsArray[i])).hidden) {
                firstView = subviewsArray[i];
                break;
            }
        }
    }

    BayCarouselItemView *lastView;
    for (NSInteger j = subviewsArray.count - 1; j >= 0; j--) {
        if ([subviewsArray[j] isKindOfClass:self.registerClass]) {
            if (!((BayCarouselItemView *)(subviewsArray[j])).hidden) {
                lastView = subviewsArray[j];
                break;
            }
        }
    }

    // 如果符合条件就生成右边的 view
    if (lastView && CGRectGetMaxX(lastView.frame) < maxX + padding - self.cardInset) {

        self.currentIndex = lastView.itemIndex;
        if (lastView.itemIndex + 1 < self.numberOfRows) {
            [self generateViewWithIndex:lastView.itemIndex + 1];
        }
    }

    // 如果符合条件就生成左边的 view
    if (firstView && CGRectGetMinX(firstView.frame) > minX - padding + self.cardInset) {

        self.currentIndex = firstView.itemIndex;
        if (firstView.itemIndex > 0) {
            [self generateViewWithIndex:firstView.itemIndex - 1];
        }
    }

    // 如果一个 subview 都没有,执行类似 reloadData 的操作
    if (!firstView && !lastView) {
        self.currentIndex = scrollView.contentOffset.x / self.rowWidth;
        [self renderCurrentView];
    }

接下来是大家最喜闻乐见的源代码了:点我可见