UICollectionview scroll horizontal and vertical

2019-05-28 01:44发布

问题:

I have to build a UICollectionView scrollable horizontal and vertical, I know that the grid layout scrolls along one axis only, either horizontally or vertically, so I have read some posts and I have tried different solutions but the most simple is to put the UICollectionview inside a UIScrollView. In this way the CollectionView scroll vertically and the UIScrollView horizontally. The problem is that the vertical scroll is difficult, not fluid and often is stop until you tap again and drag again. Can you suggest a solution? Thanks

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;

And I have implemented the method:

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

回答1:

The way to do this is to create a custom UICollectionViewLayout subclass.

I had to do this recently.

Let me go get the files... One sec...

First of all, you can't use a subclass of UICollectionViewFlowLayout easily for this. Flow layout is designed to fit the content in one direction and scroll in the other direction. This isn't what you want.

It isn't very difficult though to create a custom layout to do this for you.

Header File

@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

Implementation

#import "GridCollectionViewLayout.h"

@interface GridCollectionViewLayout ()

@property (nonatomic, strong) NSDictionary *layoutInfo;

@end

@implementation GridCollectionViewLayout

Create inits for code and interface builder...

- (id)init
{
    self = [super init];
    if (self) {
        [self setup];
    }

    return self;
}

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

    return self;
}

Setup defaults property values...

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

This was used because I was changing between different layouts but it shows what is needed to set the layout..

- (void)configureCollectionViewForLayout:(UICollectionView *)collectionView
{
    collectionView.alwaysBounceHorizontal = YES;

    [collectionView setCollectionViewLayout:self animated:NO];
}

Required method. This iterates the items and creates frames CGRect for each one. Saving them into a dictionary.

- (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;
}

This is a convenience method for quickly getting a frame at a given index.

- (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);
}

Required method to calculate the content size. This just multiplies the number of sections or items by the size and spacing properties. This is what allows scrolling in both directions because the content size can be bigger than the collection view's width AND 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);
}

Required methods. These tell the collection view where each item needs to be placed.

- (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

Of course, the layout in this case is specific to my individual problem.

The layout worked by having each section be a column and the items in each section were the rows. So something like this...

xy = item y in section x

00 10 20 30 ...
01 11 21 31 ...
02 12 22 32 ...
.  .  .  .
.  .  .  .
.  .  .  .

Obviously there can be an unlimited number of sections or items in sections so I had to have scrolling in both directions.

Once you have created your layout class you just need to set it as the layout for your collection view. You can do this in code collectionView.collectionViewLayout = myLayout or you can do it in Interface Builder with the "layout" property on the collection view.



回答2:

I'd like to introduce a different approach to creating a UICollectionView that scrolls in one direction while displaying cells containing a CollectionView that scrolls in the opposite direction. By implementing this collection view, setting the scrollDirection on the UICollectionViewFlowLayout instance used for the collection view in question this solution provides a seamless response to the user's interaction.

The solution subclasses the UICollectionView, and adds a delay gesture recognizer that intercepts the user's touches, delays them for a split seconds to figure out which direction the user is intending to scroll, then cancelling panningRecognizer on the collection view that don't scroll in that specific direction.

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
    }
}