У меня есть Collection View, который может отображать около 3,5 ячеек за раз, и я хочу, чтобы он был включен под управлением пейджинга. Но я бы хотел, чтобы он привязывался к каждой ячейке (как это делает приложение App Store), а не прокручивает всю ширину представления. Как я могу это сделать?
UICollectionView с шириной страницы страницы подкачки
Ответ 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];
}