Подтвердить что ты не робот

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

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

Спасибо.

4b9b3361

Ответ 1

В то время как изначально я использовал Objective-C, я с тех пор переключился так быстро, и исходного принятого ответа не хватило.

Я закончил создание подкласса 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

Ответ 2

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

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
}

Ответ 3

SWIFT 3 версия @Iowa15 ответ

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

Ответ 4

Основываясь на ответе Мете и комментариях Криса Чута,

Вот расширение 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()
    }
}

Ответ 5

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

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

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

Ответ 6

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

func snapToNearestCell(scrollView: UIScrollView) {
     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

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

Ответ 7

Если вы хотите простое собственное поведение без настройки:

collectionView.pagingEnabled = YES;

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

Ответ 8

Получил ответ от SO post здесь и docs здесь

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

MyViewController : SuperViewController<... ,UIScrollViewDelegate>

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

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

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

Далее реализовать метод делегата - (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.

}

Ответ 9

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

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

Ответ 10

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

Свифт 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

PS Убедитесь, что класс принимает протокол UICollectionViewDataSource

Ответ 11

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

Сначала добавьте цель к коллекции уже существующего gestureRecognizer:

[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];
    }

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

Ответ 12

Это из видео 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);
    }

И причина, по которой я столкнулся с этим вопросом, заключалась в том, что я привязался к собственному ощущению, которое я получил от принятого Марком ответа... это я ввел в контроллер viewView.

collectionView.decelerationRate = UIScrollViewDecelerationRateFast;

Ответ 13

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

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

Ответ 14

Свифт 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)
    }

}

Ответ 15

Я пробовал решения @Mark Bourke и @mrcrowley, но они дают почти одинаковые результаты с нежелательными эффектами липкости.

Мне удалось решить проблему с учетом velocity. Вот полный код.

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

    var offsetAdjusment = CGFloat.greatestFiniteMagnitude
    let horizontalCenter = proposedContentOffset.x + (collectionView.bounds.width / 2)

    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 itemHorizontalCenter = layoutAttributes.center.x

        if abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjusment) {
            if abs(velocity.x) < 0.3 { // minimum velocityX to trigger the snapping effect
                offsetAdjusment = itemHorizontalCenter - horizontalCenter
            } else if velocity.x > 0 {
                offsetAdjusment = itemHorizontalCenter - horizontalCenter + layoutAttributes.bounds.width
            } else { // velocity.x < 0
                offsetAdjusment = itemHorizontalCenter - horizontalCenter - layoutAttributes.bounds.width
            }
        }
    })

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

}