it-swarm.com.ru

UICollectionView привязывается к ячейке при горизонтальной прокрутке

Я знаю, что некоторые люди задавали этот вопрос раньше, но все они были о UITableViews или UIScrollViews, и я не мог заставить принятое решение работать на меня. Я хотел бы получить эффект привязки при горизонтальной прокрутке UICollectionView - очень похоже на то, что происходит в iOS AppStore. iOS 9+ - моя целевая сборка, поэтому, пожалуйста, посмотрите на изменения UIKit, прежде чем ответить на этот вопрос.

Благодарю.

34
Mark Bourke

Хотя изначально я использовал Objective-C, я переключился на Swift, и первоначального принятого ответа не хватило.

Я закончил тем, что создал подкласс UICollectionViewLayout, который обеспечивает лучший (imo) опыт по сравнению с другими функциями, которые изменяют смещение контента или что-то подобное, когда пользователь прекратил прокрутку.

class SnappingCollectionViewLayout: UICollectionViewFlowLayout {

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }

        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left

        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)

        let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)

        layoutAttributesArray?.forEach({ (layoutAttributes) in
            let itemOffset = layoutAttributes.frame.Origin.x
            if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) {
                offsetAdjustment = itemOffset - horizontalOffset
            }
        })

        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}

Для наиболее естественного замедления ощущений в текущем подклассе макета убедитесь, что установлено следующее:

collectionView?.decelerationRate = UIScrollViewDecelerationRateFast

63
Mark Bourke

Для чего стоит вот простой расчет, который я использую (в Swift):

func snapToNearestCell(_ collectionView: UICollectionView) {
    for i in 0..<collectionView.numberOfItems(inSection: 0) {

        let itemWithSpaceWidth = collectionViewFlowLayout.itemSize.width + collectionViewFlowLayout.minimumLineSpacing
        let itemWidth = collectionViewFlowLayout.itemSize.width

        if collectionView.contentOffset.x <= CGFloat(i) * itemWithSpaceWidth + itemWidth / 2 {                
            let indexPath = IndexPath(item: i, section: 0)
            collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
            break
        }
    }
}

Звоните, где вам это нужно. Я звоню в

func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    snapToNearestCell(scrollView)
}

А также

func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    snapToNearestCell(scrollView)
}

Откуда collectionViewFlowLayout может появиться:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // Set up collection view
    collectionViewFlowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
}
19
Nam

Swift 3 версия ответа @ Айова15

func scrollToNearestVisibleCollectionViewCell() {
    let visibleCenterPositionOfScrollView = Float(collectionView.contentOffset.x + (self.collectionView!.bounds.size.width / 2))
    var closestCellIndex = -1
    var closestDistance: Float = .greatestFiniteMagnitude
    for i in 0..<collectionView.visibleCells.count {
        let cell = collectionView.visibleCells[i]
        let cellWidth = cell.bounds.size.width
        let cellCenter = Float(cell.frame.Origin.x + cellWidth / 2)

        // Now calculate closest cell
        let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
        if distance < closestDistance {
            closestDistance = distance
            closestCellIndex = collectionView.indexPath(for: cell)!.row
        }
    }
    if closestCellIndex != -1 {
        self.collectionView!.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
    }
}

Необходимо реализовать в UIScrollViewDelegate:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollToNearestVisibleCollectionViewCell()
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        scrollToNearestVisibleCollectionViewCell()
    }
}
14
Mette

На основе ответа от Mete и комментария от Chris Chute,

Вот расширение Swift 4, которое будет делать то, что хочет OP. Он протестирован на однорядных и двухрядных представлениях вложенных коллекций и работает просто отлично.

extension UICollectionView {
    func scrollToNearestVisibleCollectionViewCell() {
        self.decelerationRate = UIScrollViewDecelerationRateFast
        let visibleCenterPositionOfScrollView = Float(self.contentOffset.x + (self.bounds.size.width / 2))
        var closestCellIndex = -1
        var closestDistance: Float = .greatestFiniteMagnitude
        for i in 0..<self.visibleCells.count {
            let cell = self.visibleCells[i]
            let cellWidth = cell.bounds.size.width
            let cellCenter = Float(cell.frame.Origin.x + cellWidth / 2)

            // Now calculate closest cell
            let distance: Float = fabsf(visibleCenterPositionOfScrollView - cellCenter)
            if distance < closestDistance {
                closestDistance = distance
                closestCellIndex = self.indexPath(for: cell)!.row
            }
        }
        if closestCellIndex != -1 {
            self.scrollToItem(at: IndexPath(row: closestCellIndex, section: 0), at: .centeredHorizontally, animated: true)
        }
    }
}

Вам нужно реализовать протокол UIScrollViewDelegate для представления вашей коллекции, а затем добавить эти два метода:

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    self.collectionView.scrollToNearestVisibleCollectionViewCell()
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        self.collectionView.scrollToNearestVisibleCollectionViewCell()
    }
}
8
Vahid Amiri

Если вам нужно простое поведение без настройки: 

collectionView.pagingEnabled = YES;

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

4
NSDestr0yer

Получил ответ из SO сообщения здесь и документы здесь

Первое, что вы можете сделать, это установить делегирование вашего класса представлением scrollview представления коллекции, сделав ваш класс делегатом scrollview

MyViewController : SuperViewController<... ,UIScrollViewDelegate>

Затем установите контроллер представления в качестве делегата

UIScrollView *scrollView = (UIScrollView *)super.self.collectionView;
scrollView.delegate = self;

Или сделайте это в конструкторе интерфейсов, нажав Ctrl + Shift в представлении коллекции, а затем удерживая нажатой клавишу «Control» + перетаскивание или перетащите правой кнопкой мыши на контроллер представления и выберите делегировать. (Вы должны знать, как это сделать). Это не работает. UICollectionView является подклассом UIScrollView, поэтому теперь вы сможете увидеть его в конструкторе интерфейсов, нажав Ctrl + Shift

Далее реализуем метод делегата - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

MyViewController.m

... 

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{

}

Документы утверждают, что:

Параметры

scrollView | Объект просмотра прокрутки, который замедляет прокрутку содержимого просмотра.

Обсуждение Представление прокрутки вызывает этот метод при прокрутке движение останавливается. Свойство замедления UIScrollView контролирует замедление.

Доступность Доступно в iOS 2.0 и более поздних версиях.

Затем внутри этого метода проверьте, какая ячейка была ближе к центру прокрутки, когда она прекратила прокрутку

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
  //NSLog(@"%f", truncf(scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2)));

float visibleCenterPositionOfScrollView = scrollView.contentOffset.x + (self.pictureCollectionView.bounds.size.width / 2);

//NSLog(@"%f", truncf(visibleCenterPositionOfScrollView / imageArray.count));


NSInteger closestCellIndex;

for (id item in imageArray) {
    // equation to use to figure out closest cell
    // abs(visibleCenter - cellCenterX) <= (cellWidth + cellSpacing/2)

    // Get cell width (and cell too)
    UICollectionViewCell *cell = (UICollectionViewCell *)[self collectionView:self.pictureCollectionView cellForItemAtIndexPath:[NSIndexPath indexPathWithIndex:[imageArray indexOfObject:item]]];
    float cellWidth = cell.bounds.size.width;

    float cellCenter = cell.frame.Origin.x + cellWidth / 2;

    float cellSpacing = [self collectionView:self.pictureCollectionView layout:self.pictureCollectionView.collectionViewLayout minimumInteritemSpacingForSectionAtIndex:[imageArray indexOfObject:item]];

    // Now calculate closest cell

    if (fabsf(visibleCenterPositionOfScrollView - cellCenter) <= (cellWidth + (cellSpacing / 2))) {
        closestCellIndex = [imageArray indexOfObject:item];
        break;
    }
}

if (closestCellIndex != nil) {

[self.pictureCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathWithIndex:closestCellIndex] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];

// This code is untested. Might not work.

}
3
Minebomber

Привязка к ближайшей ячейке, учитывая скорость прокрутки.

Работает без каких-либо глюков.

import UIKit

class SnapCenterLayout: UICollectionViewFlowLayout {
  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) }
    let parent = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)

    let itemSpace = itemSize.width + minimumInteritemSpacing
    var currentItemIdx = round(collectionView.contentOffset.x / itemSpace)

    // Skip to the next cell, if there is residual scrolling velocity left.
    // This helps to prevent glitches
    let vX = velocity.x
    if vX > 0 {
      currentItemIdx += 1
    } else if vX < 0 {
      currentItemIdx -= 1
    }

    let nearestPageOffset = currentItemIdx * itemSpace
    return CGPoint(x: nearestPageOffset,
                   y: parent.y)
  }
}
3
Richard Topchiy

Вот моя реализация

let middlePoint = Int(scrollView.contentOffset.x + UIScreen.main.bounds.width / 2)
    if let indexPath = self.cvCollectionView.indexPathForItem(at: CGPoint(x: middlePoint, y: 0)) {
        self.cvCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    }

Реализуйте ваши прокрутки просмотра делегатов как это

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    self.snapToNearestCell(scrollView: scrollView)
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    self.snapToNearestCell(scrollView: scrollView)
}

Кроме того, для лучшей привязки

self.cvCollectionView.decelerationRate = UIScrollViewDecelerationRateFast

Работает как шарм

3
Sourav Chandra

Я только что нашел то, что я считаю лучшим решением этой проблемы:

Сначала добавьте цель к уже существующему gestRecognizer collectionView:

[self.collectionView.panGestureRecognizer addTarget:self action:@selector(onPan:)];

Пусть селектор указывает на метод, который принимает UIPanGestureRecognizer в качестве параметра:

- (void)onPan:(UIPanGestureRecognizer *)recognizer {};

Затем в этом методе заставьте collectionView прокручивать до соответствующей ячейки, когда жест панорамирования закончился . Я сделал это, получив видимые элементы из представления коллекции и определив, к какому элементу я хочу перейти в зависимости от направления Сковорода.

if (recognizer.state == UIGestureRecognizerStateEnded) {

        // Get the visible items
        NSArray<NSIndexPath *> *indexes = [self.collectionView indexPathsForVisibleItems];
        int index = 0;

        if ([(UIPanGestureRecognizer *)recognizer velocityInView:self.view].x > 0) {
            // Return the smallest index if the user is swiping right
            for (int i = index;i < indexes.count;i++) {
                if (indexes[i].row < indexes[index].row) {
                    index = i;
                }
            }
        } else {
            // Return the biggest index if the user is swiping left
            for (int i = index;i < indexes.count;i++) {
                if (indexes[i].row > indexes[index].row) {
                    index = i;
                }
            }
        }
        // Scroll to the selected item
        [self.collectionView scrollToItemAtIndexPath:indexes[index] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
    }

Имейте в виду, что в моем случае только два элемента могут быть видны одновременно. Я уверен, что этот метод может быть адаптирован для большего количества предметов, однако.

2
Julius

Модификация вышеуказанного ответа, которую вы также можете попробовать:

-(void)scrollToNearestVisibleCollectionViewCell {
    float visibleCenterPositionOfScrollView = _collectionView.contentOffset.x + (self.collectionView.bounds.size.width / 2);

    NSInteger closestCellIndex = -1;
    float closestDistance = FLT_MAX;
    for (int i = 0; i < _collectionView.visibleCells.count; i++) {
        UICollectionViewCell *cell = _collectionView.visibleCells[i];
        float cellWidth = cell.bounds.size.width;

        float cellCenter = cell.frame.Origin.x + cellWidth / 2;

        // Now calculate closest cell
        float distance = fabsf(visibleCenterPositionOfScrollView - cellCenter);
        if (distance < closestDistance) {
            closestDistance = distance;
            closestCellIndex = [_collectionView indexPathForCell:cell].row;
        }
    }

    if (closestCellIndex != -1) {
        [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:closestCellIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
    }
}
2
Iowa15

Это из видео WWDC 2012 года для решения Objective-C. Я вложил в UICollectionViewFlowLayout и добавил следующее.

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    {
        CGFloat offsetAdjustment = MAXFLOAT;
        CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2);

        CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
        NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

        for (UICollectionViewLayoutAttributes *layoutAttributes in array)
        {
            CGFloat itemHorizontalCenter = layoutAttributes.center.x;
            if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment))
            {
                offsetAdjustment = itemHorizontalCenter - horizontalCenter;
            }
        }

        return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
    }

И причина, по которой я добрался до этого вопроса, была для привязки с нативным чувством, которое я получил из принятого ответа Марка ... это я вставил в контроллер представления collectionView.

collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
1
mrcrowley

Это решение дает лучшую и более плавную анимацию.

Swift 3

Чтобы получить первый и последний элемент по центру, добавьте вставки:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {

    return UIEdgeInsetsMake(0, cellWidth/2, 0, cellWidth/2)
}

Затем используйте targetContentOffset в методе scrollViewWillEndDragging, чтобы изменить конечную позицию.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let numOfItems = collectionView(mainCollectionView, numberOfItemsInSection:0)
    let totalContentWidth = scrollView.contentSize.width + mainCollectionViewFlowLayout.minimumInteritemSpacing - cellWidth
    let stopOver = totalContentWidth / CGFloat(numOfItems)

    var targetX = round((scrollView.contentOffset.x + (velocity.x * 300)) / stopOver) * stopOver
    targetX = max(0, min(targetX, scrollView.contentSize.width - scrollView.frame.width))

    targetContentOffset.pointee.x = targetX
}

Возможно, в вашем случае totalContentWidth рассчитывается по-другому, например без minimumInteritemSpacing, поэтому настройте его соответствующим образом . Также вы можете поиграться с 300, используемым в velocity

0
Roland Keesom

Свифт 4.2. Просто. Для фиксированного itemSize. Горизонтальное направление потока.

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
        let floatingPage = targetContentOffset.pointee.x/scrollView.bounds.width
        let rule: FloatingPointRoundingRule = velocity.x > 0 ? .up : .down
        let page = CGFloat(Int(floatingPage.rounded(rule)))
        targetContentOffset.pointee.x = page*(layout.itemSize.width + layout.minimumLineSpacing)
    }

}
0
Javier González Maestro

Вот версия Swift 3.0, которая должна работать как для горизонтального, так и для вертикального направлений, основываясь на предложении Марка выше:

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

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

    let realOffset = CGPoint(
      x: proposedContentOffset.x + collectionView.contentInset.left,
      y: proposedContentOffset.y + collectionView.contentInset.top
    )

    let targetRect = CGRect(Origin: proposedContentOffset, size: collectionView.bounds.size)

    var offset = (scrollDirection == .horizontal)
      ? CGPoint(x: CGFloat.greatestFiniteMagnitude, y:0.0)
      : CGPoint(x:0.0, y:CGFloat.greatestFiniteMagnitude)

    offset = self.layoutAttributesForElements(in: targetRect)?.reduce(offset) {
      (offset, attr) in
      let itemOffset = attr.frame.Origin
      return CGPoint(
        x: abs(itemOffset.x - realOffset.x) < abs(offset.x) ? itemOffset.x - realOffset.x : offset.x,
        y: abs(itemOffset.y - realOffset.y) < abs(offset.y) ? itemOffset.y - realOffset.y : offset.y
      )
    } ?? .zero

    return CGPoint(x: proposedContentOffset.x + offset.x, y: proposedContentOffset.y + offset.y)
  }
0
nikwest