UICollectionView с шириной страницы страницы подкачки

У меня есть Collection View, который может отображать около 3,5 ячеек за раз, и я хочу, чтобы он был включен под управлением пейджинга. Но я бы хотел, чтобы он привязывался к каждой ячейке (как это делает приложение App Store), а не прокручивает всю ширину представления. Как я могу это сделать?

Ответ 1

Вы можете привязать к ячейкам, являясь делегатом представления коллекции и реализуя метод:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

Это говорит о том, что пользователь закончил перетаскивание, и он позволяет вам изменить targetContentOffset, чтобы выровнять с вашими ячейками (то есть раунд до ближайшей ячейки). Обратите внимание, что вам нужно быть осторожным в том, как вы изменяете targetContentOffset; в частности, вам необходимо избегать его изменения, чтобы представление нужно прокручивать в противоположном направлении от пройденной скорости, или вы получите анимационные глюки. Вероятно, вы можете найти много примеров этого, если вы используете Google для этого имени метода.

Ответ 2

Другой способ - создать пользовательский UICollectionViewFlowLayout и переопределить метод следующим образом:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset 
                                 withScrollingVelocity:(CGPoint)velocity {

    CGRect cvBounds = self.collectionView.bounds;
    CGFloat halfWidth = cvBounds.size.width * 0.5f;
    CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;

    NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];

    UICollectionViewLayoutAttributes* candidateAttributes;
    for (UICollectionViewLayoutAttributes* attributes in attributesArray) {

        // == Skip comparison with non-cell items (headers and footers) == //
        if (attributes.representedElementCategory != 
            UICollectionElementCategoryCell) {
            continue;
        }

        // == First time in the loop == //
        if(!candidateAttributes) {
            candidateAttributes = attributes;
            continue;
        }

        if (fabsf(attributes.center.x - proposedContentOffsetCenterX) < 
            fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
            candidateAttributes = attributes;
        }
    }

    return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);

}

Если вы ищете Swift решение, ознакомьтесь с Gist

  • примечание: это будет работать, когда мы действительно показываем ячейки предварительного просмотра (даже если у них есть альфа 0.0f). Это связано с тем, что если ячейки предварительного просмотра недоступны во время прокрутки, их атрибуты объекта не будут переданы в цикл...

Ответ 3

Вот моя реализация в Swift 5 для вертикальной подкачки на основе ячеек:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page height used for estimating and calculating paging.
    let pageHeight = self.itemSize.height + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.y * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top

    return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

Некоторые заметки:

  • Не глючит
  • УСТАНОВИТЬ СТРАНИЦУ ЛОЖЬЮ! (иначе это не сработает)
  • Позволяет легко установить свою собственную скорость.
  • Если после этого что-то все еще не работает, проверьте, действительно ли ваш itemSize соответствует размеру элемента, поскольку это часто является проблемой, особенно при использовании collectionView(_:layout:sizeForItemAt:), вместо этого используйте пользовательскую переменную с itemSize.
  • Это лучше всего работает при установке self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Вот горизонтальная версия (не проверила ее полностью, поэтому, пожалуйста, простите все ошибки):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page width used for estimating and calculating paging.
    let pageWidth = self.itemSize.width + self.minimumInteritemSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.x/pageWidth

    // Determine the current page based on velocity.
    let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.x * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    // Calculate newHorizontalOffset.
    let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left

    return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}

Этот код основан на коде, который я использую в своем личном проекте, вы можете проверить его здесь, загрузив его и запустив цель Example.

Ответ 4

Я разработал свое решение, прежде чем смотреть на него. Я также пошел с созданием пользовательского UICollectionViewFlowLayout и переопределить метод targetContentOffset.

Кажется, что это работает отлично для меня (т.е. я получаю такое же поведение, как и в AppStore), хотя у меня гораздо меньше кода. Здесь, не стесняйтесь указать мне какой-либо недостаток, о котором вы можете думать:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    let inset: Int = 10
    let vcBounds = self.collectionView!.bounds
    var candidateContentOffsetX: CGFloat = proposedContentOffset.x

    for attributes in self.layoutAttributesForElements(in: vcBounds)! as [UICollectionViewLayoutAttributes] {

        if vcBounds.origin.x < attributes.center.x {
            candidateContentOffsetX = attributes.frame.origin.x - CGFloat(inset)
            break
        }

    }

    return CGPoint(x: candidateContentOffsetX, y: proposedContentOffset.y)
}

Ответ 5

Решение Майка М., представленное на этом посту, раньше работало для меня, но в моем случае я хотел, чтобы первая ячейка начиналась в середине коллекцииView. Поэтому я использовал метод делегата потока сбора для определения вставки (collectionView:layout:insetForSectionAtIndex:). Это заставило прокрутку между первой ячейкой и секундой застревать и не прокручивать правильно до первой ячейки.

Причиной этого было то, что candidateAttributes.center.x - halfWidth имел отрицательное значение. Решение состояло в том, чтобы получить абсолютное значение, поэтому я добавляю fabs к этой строке return CGPointMake(fabs(candidateAttributes.center.x - halfWidth), offset.y);

По умолчанию необходимо добавить Fabs, чтобы охватить все ситуации.

Ответ 6

Это мое решение. Работает с любой шириной страницы.

Установите self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast, чтобы почувствовать реальный пейджинг.

Решение основано на одном разделе для прокрутки с разбивкой по страницам.

- (CGFloat)pageWidth {

    return self.itemSize.width + self.minimumLineSpacing;
}

- (CGPoint)offsetAtCurrentPage {

    CGFloat width = -self.collectionView.contentInset.left - self.sectionInset.left;
    for (int i = 0; i < self.currentPage; i++)
        width += [self pageWidth];

    return CGPointMake(width, 0);
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {

    return [self offsetAtCurrentPage];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    // To scroll paginated
    /*
    if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
    else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;

    return  [self offsetAtCurrentPage];
    */

    // To scroll and stop always at the center of a page
    CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - self.pageWidth/2, 0, self.pageWidth, self.collectionView.bounds.size.height);
    NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
    __block UICollectionViewLayoutAttributes *proposedAttributes = nil;
    __block CGFloat minDistance = CGFLOAT_MAX;
    [allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;

        if (ABS(distance) < minDistance) {
            proposedAttributes = obj;
            minDistance = distance;
        }
    }];


    // Scroll always
    if (self.currentPage == proposedAttributes.indexPath.row) {
        if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfItemsInSection:0]-1) self.currentPage += 1;
        else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
    }
    else  {
        self.currentPage = proposedAttributes.indexPath.row;
    }

    return  [self offsetAtCurrentPage];
}

Это разбивается на разделы.

- (CGPoint)offsetAtCurrentPage {

    CGFloat width = -self.collectionView.contentInset.leff;
    for (int i = 0; i < self.currentPage; i++)
        width += [self sectionWidth:i];
    return CGPointMake(width, 0);
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    return [self offsetAtCurrentPage];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    // To scroll paginated
    /*
    if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
    else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;

    return  [self offsetAtCurrentPage];
    */

    // To scroll and stop always at the center of a page
    CGRect proposedRect = CGRectMake(proposedContentOffset.x+self.collectionView.bounds.size.width/2 - [self sectionWidth:0]/2, 0, [self sectionWidth:0], self.collectionView.bounds.size.height);
    NSMutableArray <__kindof UICollectionViewLayoutAttributes *> *allAttributes = [[self layoutAttributesForElementsInRect:proposedRect] mutableCopy];
    __block UICollectionViewLayoutAttributes *proposedAttributes = nil;
    __block CGFloat minDistance = CGFLOAT_MAX;
    [allAttributes enumerateObjectsUsingBlock:^(__kindof UICollectionViewLayoutAttributes * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

        CGFloat distance = CGRectGetMidX(proposedRect) - obj.center.x;

        if (ABS(distance) < minDistance) {
            proposedAttributes = obj;
            minDistance = distance;
        }
    }];

    // Scroll always
    if (self.currentPage == proposedAttributes.indexPath.section) {
        if (velocity.x > 0 && self.currentPage < [self.collectionView numberOfSections]-1) self.currentPage += 1;
        else if (velocity.x < 0 && self.currentPage > 0) self.currentPage -= 1;
    }
    else  {
        self.currentPage = proposedAttributes.indexPath.section;
    }

    return  [self offsetAtCurrentPage];
}