UICollectionView привязка к ячейке при горизонтальной прокрутке
Я знаю, что некоторые люди задавали этот вопрос раньше, но все они были о UITableViews или UIScrollViews, и я не мог заставить принятое решение работать на меня. То, что мне хотелось бы, - это эффект щелчка при прокрутке моего UICollectionView по горизонтали-очень похоже на то, что происходит в iOS AppStore. iOS 9+ - это моя целевая сборка, поэтому, пожалуйста, посмотрите на изменения UIKit, прежде чем ответить на этот вопрос.
Спасибо.
13 ответов:
В то время как первоначально я использовал 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
Для чего это стоит вот простой расчет, который я использую (в 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 }
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() } }
на основе ответа от 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() } }
Получил ответ от SO post здесь и docs здесь
Во-первых, вы можете установить делегат scrollview вашего представления коллекции, сделав ваш класс делегатом scrollview
MyViewController : SuperViewController<... ,UIScrollViewDelegate>Затем установите контроллер вида в качестве делегата
UIScrollView *scrollView = (UIScrollView *)super.self.collectionView; scrollView.delegate = self;Или сделайте это в конструкторе интерфейсов, нажав control + shift на вид коллекции, а затем control + drag или щелкните правой кнопкой мыши перетащить на контроллер вида и выберите делегат. (Вы должны знать, как это сделать). это не работает. UICollectionView-это подкласс UIScrollView, поэтому теперь вы сможете увидеть его в конструкторе интерфейсов, нажав control + shift
Далее реализуем метод делегата
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollViewMyViewController.m ... - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { }Документы утверждают, что:
Параметры
ScrollView / объект scroll-view, замедляющий прокрутку представления содержимого.
Обсуждение вид прокрутки вызывает этот метод, когда прокрутка движение останавливается. Замедляющее свойство UIScrollView контролирует замедление.
Доступность доступна в iOS 2.0 и более поздних версиях.
Затем внутри этого метода проверьте, какая ячейка была ближе всего к центру scrollview, когда он перестал прокручиваться
- (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. }
Вот моя реализация
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Работает как заклинание
Модификация вышеприведенного ответа, которую вы также можете попробовать:
-(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]; } }
Я только что нашел то, что я считаю наилучшим возможным решением этой проблемы:
Сначала добавьте цель в уже существующий gestureRecognizer 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]; }
Если вы хотите простое собственное поведение, без настройки:
collectionView.pagingEnabled = YES;Это работает правильно только в том случае, если размер элементов макета представления коллекции равен только одному размеру, а свойство
UICollectionViewCellclipToBoundsимеет значениеYES.
Привязка к ближайшей ячейке, соблюдая скорость прокрутки.
Работает без каких-либо сбоев.
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) } }
Это из видеоролика WWDC 2012 для решения Objective-C. Я подкласс UICollectionViewFlowLayout и добавил следующее.
И причина, по которой я добрался до этого вопроса, заключалась в том, что я щелкнул с туземным чувством, которое я получил от принятого ответа Марка... это я вставил в контроллер вида collectionView.-(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.decelerationRate = UIScrollViewDecelerationRateFast;
Это решение дает лучшую и более плавную анимацию.
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
Вот версия 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) }
Comments