【问题标题】:UICollectionview scroll horizontal and verticalUICollectionview 水平和垂直滚动
【发布时间】:2015-05-31 21:42:25
【问题描述】:

我必须构建一个可水平和垂直滚动的 UICollectionView,我知道网格布局只能沿一个轴水平或垂直滚动​​,所以我阅读了一些帖子并尝试了不同的解决方案,但最简单的是将UIScrollView 中的 UICollectionview。这样,CollectionView 垂直滚动,UIScrollView 水平滚动。 问题是垂直滚动很困难,不流畅,并且经常停止,直到您再次点击并再次拖动。 你能提出一个解决方案吗?谢谢

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
UIScrollView *backgroundScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
backgroundScroll.scrollEnabled = YES;     
[self.view addSubview:backgroundScroll];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(10, 15, 1020, [UIScreen mainScreen].bounds.size.height - 35) collectionViewLayout:layout];
[backgroundScroll addSubview:_collectionView];
_collectionView.contentInset = UIEdgeInsetsMake(0, 0, 50, 0);         
_collectionView.scrollEnabled = YES;

并且我已经实现了方法:

- (void)viewDidLayoutSubviews {
    backgroundScroll.contentSize = self.collectionView.frame.size;
}

【问题讨论】:

    标签: ios objective-c uiscrollview uicollectionview


    【解决方案1】:

    这样做的方法是创建一个自定义的UICollectionViewLayout 子类。

    我最近不得不这样做。

    让我去拿文件……一秒钟……

    首先,您不能轻易使用UICollectionViewFlowLayout 的子类。流式布局旨在使内容适应一个方向并在另一个方向滚动。这不是你想要的。

    创建一个自定义布局来为您做到这一点并不难。

    头文件

    @interface GridCollectionViewLayout : UICollectionViewLayout
    
    // properties to configure the size and spacing of the grid
    @property (nonatomic) CGSize itemSize;
    @property (nonatomic) CGFloat itemSpacing;
    
    // this method was used because I was switching between layouts    
    - (void)configureCollectionViewForLayout:(UICollectionView *)collectionView;
    
    @end
    

    实施

    #import "GridCollectionViewLayout.h"
    
    @interface GridCollectionViewLayout ()
    
    @property (nonatomic, strong) NSDictionary *layoutInfo;
    
    @end
    
    @implementation GridCollectionViewLayout
    

    为代码和界面构建器创建初始化...

    - (id)init
    {
        self = [super init];
        if (self) {
            [self setup];
        }
    
        return self;
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super init];
        if (self) {
            [self setup];
        }
    
        return self;
    }
    

    设置默认属性值...

    - (void)setup
    {
        self.itemSize = CGSizeMake(50.0, 50.0);
        self.itemSpacing = 10.0;
    }
    

    使用它是因为我在不同的布局之间进行更改,但它显示了设置布局所需的内容..

    - (void)configureCollectionViewForLayout:(UICollectionView *)collectionView
    {
        collectionView.alwaysBounceHorizontal = YES;
    
        [collectionView setCollectionViewLayout:self animated:NO];
    }
    

    必需的方法。这会迭代项目并为每个项目创建框架CGRect。将它们保存到字典中。

    - (void)prepareLayout
    {
        NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
    
        NSInteger sectionCount = [self.collectionView numberOfSections];
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
    
        for (NSInteger section = 0; section < sectionCount; section++) {
            NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
    
            for (NSInteger item = 0; item < itemCount; item++) {
                indexPath = [NSIndexPath indexPathForItem:item inSection:section];
    
                UICollectionViewLayoutAttributes *itemAttributes =
                [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
                itemAttributes.frame = [self frameForAssessmentAtIndexPath:indexPath];
    
                cellLayoutInfo[indexPath] = itemAttributes;
            }
        }
    
        self.layoutInfo = cellLayoutInfo;
    }
    

    这是一种在给定索引处快速获取帧的便捷方法。

    - (CGRect)frameForIndexPath:(NSIndexPath *)indexPath
    {
        NSInteger column = indexPath.section;
        NSInteger row = indexPath.item;
    
        CGFloat originX = column * (self.itemSize.width + self.itemSpacing);
        CGFloat originY = row * (self.itemSize.height + self.itemSpacing);
    
        return CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height);
    }
    

    计算内容大小所需的方法。这只是将部分或项目的数量乘以大小和间距属性。这是允许双向滚动的原因,因为内容大小可以大于集合视图的宽度和高度。

    - (CGSize)collectionViewContentSize
    {
        NSInteger sectionCount = [self.collectionView numberOfSections];
    
        if (sectionCount == 0) {
            return CGSizeZero;
        }
    
        NSInteger itemCount = [self.collectionView numberOfItemsInSection:0];
    
        CGFloat width = (self.itemSize.width + self.itemSpacing) * sectionCount - self.itemSpacing;
        CGFloat height = (self.itemSize.height + self.itemSpacing) * itemCount - self.itemSpacing;
    
        return CGSizeMake(width, height);
    }
    

    必需的方法。这些告诉集合视图每个项目需要放置在哪里。

    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        return self.layoutInfo[indexPath];
    }
    
    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
    {
        NSMutableArray *allAttributes = [NSMutableArray array];
    
        [self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attributes, BOOL *stop) {
            if (CGRectIntersectsRect(attributes.frame, rect)) {
                [allAttributes addObject:attributes];
            }
        }];
    
        return allAttributes;
    }
    
    @end
    

    当然,这种情况下的布局是针对我个人问题的。

    布局的工作方式是让每个部分都是一列,每个部分中的项目都是行。所以像这样......

    xy = item y in section x
    
    00 10 20 30 ...
    01 11 21 31 ...
    02 12 22 32 ...
    .  .  .  .
    .  .  .  .
    .  .  .  .
    

    显然,可以有无限数量的部分或部分中的项目,因此我必须双向滚动。

    一旦你创建了你的布局类,你只需要将它设置为你的集合视图的布局。您可以在代码 collectionView.collectionViewLayout = myLayout 中执行此操作,也可以在 Interface Builder 中使用集合视图上的“布局”属性执行此操作。

    【讨论】:

    • @Anna565 已编辑。有相当多的代码需要设置,但这是一个相当简单的示例,因为每个项目都是相同的大小等...排列在一个完美的网格中。
    • 非常感谢您的宝贵时间,但最后我使用了 UiScrolllView 并解决了滚动问题。我使用了 UICollectionView 的垂直滚动和 UIScrollingView 的水平滚动来禁用垂直滚动。我用过这个:stackoverflow.com/questions/5095713/…
    • 这里有一个官方示例文档:"Custom Layouts: A Worked Example",希望对其他人也有帮助。
    【解决方案2】:

    我想介绍一种不同的方法来创建一个在一个方向上滚动的 UICollectionView,同时显示包含一个在相反方向滚动的 CollectionView 的单元格。通过实现此集合视图,在用于相关集合视图的 UICollectionViewFlowLayout 实例上设置 scrollDirection,此解决方案提供对用户交互的无缝响应。

    该解决方案子类化 UICollectionView,并添加一个延迟手势识别器来拦截用户的触摸,将它们延迟几秒钟以确定用户打算滚动的方向,然后取消集合视图上的 panningRecognizer 不朝那个特定方向滚动。

    import Foundation
    import UIKit
    
    class UIDirectionAbidingCollectionView : UICollectionView {
    
        override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
            super.init(frame: frame, collectionViewLayout: layout)
            setupDelayRecognizer()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupDelayRecognizer()
        }
    
        func setupDelayRecognizer() {
            addGestureRecognizer(delayPanGestureRecognizer)
    
            // Delay the touches on the default recognizer on the collection view 
    
            panGestureRecognizer.delaysTouchesBegan = true
        }
    
        // This gesture recognizer controls the response to the user's touches
        // by cancelling by failing panGesture recognizer on the collection view
        // that scrolls in the opposite direction.
    
        lazy var delayPanGestureRecognizer: UIPanGestureRecognizer = {
            var recognizer = UIPanGestureRecognizer()
            recognizer.delegate = self
            return recognizer
        }()
    }
    
    extension UIDirectionAbidingCollectionView: UIGestureRecognizerDelegate {
    
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    
            // Ensure that the delay recognizer needs to fails for the 
            // default panning recognizers to receives the touches
    
            if (gestureRecognizer == delayPanGestureRecognizer &&
                otherGestureRecognizer == panGestureRecognizer) 
            {
                return true
            }
    
            return false
        }
    
        override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    
            // If the recognizer in question is our delay recognizer
            // lets check to see if it should begin receiving touches
    
            if gestureRecognizer == delayPanGestureRecognizer {
    
                // First retrieve the direction our flowlayout intends to scroll
    
                if let flowLayout = self.collectionViewLayout as? UICollectionViewFlowLayout {
    
    
                    let scrollDirection = flowLayout.scrollDirection
    
                    // Retrieve the translation of the delayPanningRecognizer 
    
                    let translation = delayPanGestureRecognizer.translation(in: self)
    
                    // Calculate the magnitude of change for the y and x axis 
    
                    let xTransaltionValue = (translation.x * translation.x)
                    let yTransaltionValue = (translation.y * translation.y)
    
                    if scrollDirection == .vertical && xTransaltionValue > yTransaltionValue {
    
                       // If the scroll direction of the flowlayout is vertical,
                       // and the magnitude in the horizontal direction
                       // is greater than the horizontal, begin receiving touches.
                       // Since the delay recognizer doesn't fail, the vertical
                       // panning recognizer will fail to start on the collection view
    
                       return true
                    }
                    else if scrollDirection == .horizontal && xTransaltionValue < yTransaltionValue {
    
                        // If the scroll direction of the flowlayout is horizontal,
                        // and the magnitude in the vertical direction
                        // is greater than the horizontal, begin receiving touches.
                        // Since the delay recognizer doesn't fail, the horizontal
                        // panning recognizer will fail to start on the collection view
    
                        return true
                    }
                    else {
    
                        // Fail the delay recognizer, and allows the collection 
                        // view to continue as usual
                        return false
                    }
                }
            }
    
            return true
        }
    }
    

    【讨论】: