Подкачка UICollectionView по ячейкам, а не экран
у меня есть UICollectionView с горизонтальной прокруткой и всегда есть 2 ячейки бок о бок на весь экран. Мне нужно, чтобы прокрутка остановилась в начале ячейки. При включенной подкачке представление коллекции прокручивает всю страницу, которая состоит из 2 ячеек одновременно, а затем останавливается.
мне нужно включить прокрутку по одной ячейке или прокрутку по нескольким ячейкам с остановкой на краю ячейки.
Я пытался подкласс UICollectionViewFlowLayout и реализовать метод targetContentOffsetForProposedContentOffset, но до сих пор мне удалось только сломать мой вид коллекции, и он перестал прокручиваться. Есть ли простой способ добиться этого и как, или мне действительно нужно реализовать все методы UICollectionViewFlowLayout подкласс? Спасибо.
13 ответов:
Итак, я нашел решение здесь: targetContentOffsetForProposedContentoffset:withScrollingVelocity без подкласса UICollectionViewFlowLayout
Я должен искать
targetContentOffsetForProposedContentOffsetв начале.
просто переопределить метод:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { *targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0 float pageWidth = (float)self.articlesCollectionView.bounds.size.width; int minSpace = 10; int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines if (cellToSwipe < 0) { cellToSwipe = 0; } else if (cellToSwipe >= self.articles.count) { cellToSwipe = self.articles.count - 1; } [self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES]; }
Swift 3 версия ответа Эви:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset let pageWidth:Float = Float(self.view.bounds.width) let minSpace:Float = 10.0 var cellToSwipe:Double = Double(Float((scrollView.contentOffset.x))/Float((pageWidth+minSpace))) + Double(0.5) if cellToSwipe < 0 { cellToSwipe = 0 } else if cellToSwipe >= Double(self.articles.count) { cellToSwipe = Double(self.articles.count) - Double(1) } let indexPath:IndexPath = IndexPath(row: Int(cellToSwipe), section:0) self.collectionView.scrollToItem(at:indexPath, at: UICollectionViewScrollPosition.left, animated: true) }
частично основано на ответе Стивеноджо. Я проверил это с помощью горизонтальной прокрутки и без отскока UICollectionView. cellSize-это размер CollectionViewCell. Вы можете настроить фактор для изменения чувствительности прокрутки.
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset var factor: CGFloat = 0.5 if velocity.x < 0 { factor = -factor } let indexPath = IndexPath(row: (scrollView.contentOffset.x/cellSize.width + factor).int, section: 0) collectionView?.scrollToItem(at: indexPath, at: .left, animated: true) }
Подход 1: Представление Коллекции
flowLayoutиUICollectionViewFlowLayoutсвойстваoverride func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { if let collectionView = collectionView { targetContentOffset.memory = scrollView.contentOffset let pageWidth = CGRectGetWidth(scrollView.frame) + flowLayout.minimumInteritemSpacing var assistanceOffset : CGFloat = pageWidth / 3.0 if velocity.x < 0 { assistanceOffset = -assistanceOffset } let assistedScrollPosition = (scrollView.contentOffset.x + assistanceOffset) / pageWidth var targetIndex = Int(round(assistedScrollPosition)) if targetIndex < 0 { targetIndex = 0 } else if targetIndex >= collectionView.numberOfItemsInSection(0) { targetIndex = collectionView.numberOfItemsInSection(0) - 1 } print("targetIndex = \(targetIndex)") let indexPath = NSIndexPath(forItem: targetIndex, inSection: 0) collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Left, animated: true) } }Подход 2: Контроллер Просмотра Страниц
вы могли бы использовать
UIPageViewControllerесли он соответствует вашим требованиям, каждая страница будет иметь отдельный контроллер вида.
вроде как ответ evya, но немного более плавный, потому что он не устанавливает targetContentOffset в ноль.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if ([scrollView isKindOfClass:[UICollectionView class]]) { UICollectionView* collectionView = (UICollectionView*)scrollView; if ([collectionView.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]]) { UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout; CGFloat pageWidth = layout.itemSize.width + layout.minimumInteritemSpacing; CGFloat usualSideOverhang = (scrollView.bounds.size.width - pageWidth)/2.0; // k*pageWidth - usualSideOverhang = contentOffset for page at index k if k >= 1, 0 if k = 0 // -> (contentOffset + usualSideOverhang)/pageWidth = k at page stops NSInteger targetPage = 0; CGFloat currentOffsetInPages = (scrollView.contentOffset.x + usualSideOverhang)/pageWidth; targetPage = velocity.x < 0 ? floor(currentOffsetInPages) : ceil(currentOffsetInPages); targetPage = MAX(0,MIN(self.projects.count - 1,targetPage)); *targetContentOffset = CGPointMake(MAX(targetPage*pageWidth - usualSideOverhang,0), 0); } } }
вот моя версия этого в Swift 3. Вычислите смещение после завершения прокрутки и отрегулируйте смещение с помощью анимации.
collectionLayoutэтоUICollectionViewFlowLayout()func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let index = scrollView.contentOffset.x / collectionLayout.itemSize.width let fracPart = index.truncatingRemainder(dividingBy: 1) let item= Int(fracPart >= 0.5 ? ceil(index) : floor(index)) let indexPath = IndexPath(item: item, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) }
Это прямой способ сделать это.
случай простой, но, наконец, довольно распространенный ( типичный скроллер миниатюр с фиксированным размером ячейки и фиксированным зазором между ячейками)
var itemCellSize: CGSize = <your cell size> var itemCellsGap: CGFloat = <gap in between> override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let pageWidth = (itemCellSize.width + itemCellsGap) let itemIndex = (targetContentOffset.pointee.x) / pageWidth targetContentOffset.pointee.x = round(itemIndex) * pageWidth - (itemCellsGap / 2) } // CollectionViewFlowLayoutDelegate func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return itemCellSize } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return itemCellsGap }обратите внимание, что нет причин вызывать scrollToOffset или погружаться в макеты. Собственное поведение прокрутки уже делает все.
Ура Всем :)
Горизонтальная Подкачка С Пользовательской Шириной Страницы (Swift 4)
многие решения, представленные здесь, приводят к некоторому странному поведению, которое не похоже на правильно реализованную подкачку.
решение представлено в в этом уроке, однако, кажется, нет никаких проблем. Это просто похоже на прекрасно работающий алгоритм подкачки. Вы можете реализовать его в 5 простых шагов:
- добавьте в свой тип следующее свойство:
private var indexOfCellBeforeDragging = 0- установить
collectionViewdelegateтакой:collectionView.delegate = self- добавить соответствия
UICollectionViewDelegateчерез расширение:extension YourType: UICollectionViewDelegate { }добавьте следующий метод в расширение, реализующее
UICollectionViewDelegateсоответствие и установить значение дляpageWidth:func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { let pageWidth = // The width your page should have (plus a possible margin) let proportionalOffset = collectionView.contentOffset.x / pageWidth indexOfCellBeforeDragging = Int(round(proportionalOffset)) }добавьте следующий метод в расширение, реализующее
UICollectionViewDelegateсоответствие, установите то же значение дляpageWidth(вы также можете хранить это значение в центральном месте) и установите значение дляcollectionViewItemCount:func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { // Stop scrolling targetContentOffset.pointee = scrollView.contentOffset // Calculate conditions let pageWidth = // The width your page should have (plus a possible margin) let collectionViewItemCount = // The number of items in this section let proportionalOffset = collectionView.contentOffset.x / pageWidth let indexOfMajorCell = Int(round(proportionalOffset)) let swipeVelocityThreshold: CGFloat = 0.5 let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < collectionViewItemCount && velocity.x > swipeVelocityThreshold let hasEnoughVelocityToSlideToThePreviousCell = indexOfCellBeforeDragging - 1 >= 0 && velocity.x < -swipeVelocityThreshold let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell) if didUseSwipeToSkipCell { // Animate so that swipe is just continued let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1) let toValue = pageWidth * CGFloat(snapToIndex) UIView.animate( withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: { scrollView.contentOffset = CGPoint(x: toValue, y: 0) scrollView.layoutIfNeeded() }, completion: nil) } else { // Pop back (against velocity) let indexPath = IndexPath(row: indexOfMajorCell, section: 0) collectionView.scrollToItem(at: indexPath, at: .left, animated: true) } }
также вы можете создать поддельный вид прокрутки для обработки прокрутки.
горизонтальный или вертикальный
// === Defaults === let bannerSize = CGSize(width: 280, height: 170) let pageWidth: CGFloat = 290 // ^ + paging let insetLeft: CGFloat = 20 let insetRight: CGFloat = 20 // ================ var pageScrollView: UIScrollView! override func viewDidLoad() { super.viewDidLoad() // Create fake scrollview to properly handle paging pageScrollView = UIScrollView(frame: CGRect(origin: .zero, size: CGSize(width: pageWidth, height: 100))) pageScrollView.isPagingEnabled = true pageScrollView.alwaysBounceHorizontal = true pageScrollView.showsVerticalScrollIndicator = false pageScrollView.showsHorizontalScrollIndicator = false pageScrollView.delegate = self pageScrollView.isHidden = true view.insertSubview(pageScrollView, belowSubview: collectionView) // Set desired gesture recognizers to the collection view for gr in pageScrollView.gestureRecognizers! { collectionView.addGestureRecognizer(gr) } } func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == pageScrollView { // Return scrolling back to the collection view collectionView.contentOffset.x = pageScrollView.contentOffset.x } } func refreshData() { ... refreshScroll() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() refreshScroll() } /// Refresh fake scrolling view content size if content changes func refreshScroll() { let w = collectionView.width - bannerSize.width - insetLeft - insetRight pageScrollView.contentSize = CGSize(width: pageWidth * CGFloat(banners.count) - w, height: 100) }
Да, вот моя реализация в Swift 4.1 на вертикальный подкачка по ячейкам:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { // 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 = self.collectionView!.contentOffset.y/pageHeight // Determine the current page based on velocity. let currentPage = (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) - self.collectionView!.contentInset.top return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset) }это не должно глюк и позволяет легко установить свой собственный flickvelocity.
edit: Вот это горизонтальный версия (не проверяли его тщательно, поэтому, пожалуйста, простите любые ошибки):
internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { // 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 = self.collectionView!.contentOffset.x/pageWidth // Determine the current page based on velocity. let currentPage = (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 newVerticalOffset. let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - self.collectionView!.contentInset.left return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y) }
хорошо, поэтому предлагаемые ответы не работали для меня, потому что я хотел прокручивать разделы вместо этого и, таким образом, иметь переменные размеры страниц ширины
Я сделал это (только по вертикали):
var pagesSizes = [CGSize]() func scrollViewDidScroll(_ scrollView: UIScrollView) { defer { lastOffsetY = scrollView.contentOffset.y } if collectionView.isDecelerating { var currentPage = 0 var currentPageBottom = CGFloat(0) for pagesSize in pagesSizes { currentPageBottom += pagesSize.height if currentPageBottom > collectionView!.contentOffset.y { break } currentPage += 1 } if collectionView.contentOffset.y > currentPageBottom - pagesSizes[currentPage].height, collectionView.contentOffset.y + collectionView.frame.height < currentPageBottom { return // 100% of view within bounds } if lastOffsetY < collectionView.contentOffset.y { if currentPage + 1 != pagesSizes.count { collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom), animated: true) } } else { collectionView.setContentOffset(CGPoint(x: 0, y: currentPageBottom - pagesSizes[currentPage].height), animated: true) } } }в этом случае я вычисляю каждый размер страницы заранее, используя высоту раздела + Верхний колонтитул + нижний колонтитул, и сохраняю его в массиве. Вот это
pagesSizesmember
вот мой способ сделать это с помощью
UICollectionViewFlowLayoutпереопределитьtargetContentOffset:(хотя, в конце концов, я не использую это и вместо этого использую UIPageViewController.)
/** A UICollectionViewFlowLayout with... - paged horizontal scrolling - itemSize is the same as the collectionView bounds.size */ class PagedFlowLayout: UICollectionViewFlowLayout { override init() { super.init() self.scrollDirection = .horizontal self.minimumLineSpacing = 8 // line spacing is the horizontal spacing in horizontal scrollDirection self.minimumInteritemSpacing = 0 if #available(iOS 11.0, *) { self.sectionInsetReference = .fromSafeArea // for iPhone X } } required init?(coder aDecoder: NSCoder) { fatalError("not implemented") } // Note: Setting `minimumInteritemSpacing` here will be too late. Don't do it here. override func prepare() { super.prepare() guard let collectionView = collectionView else { return } collectionView.decelerationRate = UIScrollViewDecelerationRateFast // mostly you want it fast! let insetedBounds = UIEdgeInsetsInsetRect(collectionView.bounds, self.sectionInset) self.itemSize = insetedBounds.size } // Table: Possible cases of targetContentOffset calculation // ------------------------- // start | | // near | velocity | end // page | | page // ------------------------- // 0 | forward | 1 // 0 | still | 0 // 0 | backward | 0 // 1 | forward | 1 // 1 | still | 1 // 1 | backward | 0 // ------------------------- override func targetContentOffset( //swiftlint:disable:this cyclomatic_complexity forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { guard let collectionView = collectionView else { return proposedContentOffset } let pageWidth = itemSize.width + minimumLineSpacing let currentPage: CGFloat = collectionView.contentOffset.x / pageWidth let nearestPage: CGFloat = round(currentPage) let isNearPreviousPage = nearestPage < currentPage var pageDiff: CGFloat = 0 let velocityThreshold: CGFloat = 0.5 // can customize this threshold if isNearPreviousPage { if velocity.x > velocityThreshold { pageDiff = 1 } } else { if velocity.x < -velocityThreshold { pageDiff = -1 } } let x = (nearestPage + pageDiff) * pageWidth let cappedX = max(0, x) // cap to avoid targeting beyond content //print("x:", x, "velocity:", velocity) return CGPoint(x: cappedX, y: proposedContentOffset.y) } }
Comments