TargetContentOffsetForProposedContentOffset: withScrollingVelocity без подкласса UICollectionViewFlowLayout
В моем приложении очень простой CollectionView (всего одна квадратная миниатюра).
Я бы хотел перехватить прокрутку, чтобы смещение всегда оставляло полное изображение с левой стороны. На данный момент он прокручивается туда, где и оставит отрезанные изображения.
В любом случае, я знаю, что мне нужно использовать функцию
- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity
но я просто использую стандартный UICollectionViewFlowLayout
. Я не подклассифицирую его.
Есть ли способ перехвата этого без подкласса UICollectionViewFlowLayout
?
Спасибо
Ответы
Ответ 1
ОК, ответ - нет, нет никакого способа сделать это без подкласса UICollectionViewFlowLayout.
Однако подклассификация невероятно проста для тех, кто читает это в будущем.
Сначала я настроил вызов подкласса MyCollectionViewFlowLayout
, а затем в построителе интерфейса я изменил макет вида коллекций на пользовательский и выбрал подкласс подкласса потока.
Поскольку вы делаете это таким образом, вы не можете указывать размеры элементов и т.д. в IB, поэтому в MyCollectionViewFlowLayout.m у меня есть это...
- (void)awakeFromNib
{
self.itemSize = CGSizeMake(75.0, 75.0);
self.minimumInteritemSpacing = 10.0;
self.minimumLineSpacing = 10.0;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
}
Это устанавливает для меня все размеры и направление прокрутки.
Затем...
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalOffset = proposedContentOffset.x + 5;
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
for (UICollectionViewLayoutAttributes *layoutAttributes in array) {
CGFloat itemOffset = layoutAttributes.frame.origin.x;
if (ABS(itemOffset - horizontalOffset) < ABS(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
Это гарантирует, что прокрутка заканчивается с краем 5,0 на левом краю.
Это все, что мне нужно было сделать. Мне не нужно было устанавливать компоновку потока в коде вообще.
Ответ 2
Решение Dan ошибочно. Он не справляется с хорошим щелчком пользователя. Случаи, когда пользователь щелкает быстро, и прокрутка не двигалась так сильно, имеют сбои в анимации.
Моя предлагаемая альтернативная реализация имеет ту же нумерацию, что и предлагалось ранее, но позволяет пользователю перемещаться между страницами.
#pragma mark - Pagination
- (CGFloat)pageWidth {
return self.itemSize.width + self.minimumLineSpacing;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
CGFloat rawPageValue = self.collectionView.contentOffset.x / self.pageWidth;
CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);
BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
BOOL flicked = fabs(velocity.x) > [self flickVelocity];
if (pannedLessThanAPage && flicked) {
proposedContentOffset.x = nextPage * self.pageWidth;
} else {
proposedContentOffset.x = round(rawPageValue) * self.pageWidth;
}
return proposedContentOffset;
}
- (CGFloat)flickVelocity {
return 0.3;
}
Ответ 3
В то время как этот ответ очень помог мне, появляется заметное мерцание, когда вы быстро проходите на небольшом расстоянии. Это намного проще воспроизвести на устройстве.
Я обнаружил, что это всегда происходит, когда collectionView.contentOffset.x - proposedContentOffset.x
и velocity.x
имеют разные позы.
Мое решение состояло в том, чтобы proposedContentOffset
было больше, чем contentOffset.x
, если скорость положительная и меньше, если она отрицательна. Он в С#, но должен быть достаточно простым для перевода в Objective C:
public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
/* Determine closest edge */
float offSetAdjustment = float.MaxValue;
float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));
RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
var array = base.LayoutAttributesForElementsInRect (targetRect);
foreach (var layoutAttributes in array) {
float itemHorizontalCenter = layoutAttributes.Center.X;
if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
offSetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
float nextOffset = proposedContentOffset.X + offSetAdjustment;
/*
* ... unless we end up having positive speed
* while moving left or negative speed while moving right.
* This will cause flicker so we resort to finding next page
* in the direction of velocity and use it.
*/
do {
proposedContentOffset.X = nextOffset;
float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
float velX = scrollingVelocity.X;
// If their signs are same, or if either is zero, go ahead
if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
break;
// Otherwise, look for the closest page in the right direction
nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
} while (IsValidOffset (nextOffset));
return proposedContentOffset;
}
bool IsValidOffset (float offset)
{
return (offset >= MinContentOffset && offset <= MaxContentOffset);
}
Этот код использует MinContentOffset
, MaxContentOffset
и SnapStep
, который должен быть тривиальным для вас. В моем случае они оказались
float MinContentOffset {
get { return -CollectionView.ContentInset.Left; }
}
float MaxContentOffset {
get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}
float SnapStep {
get { return ItemSize.Width + MinimumLineSpacing; }
}
Ответ 4
После длительного тестирования я нашел решение для привязки к центру с пользовательской шириной ячейки (каждая ячейка имеет ширину разницы), которая исправляет мерцание. Не стесняйтесь улучшать script.
- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint) proposedContentOffset withScrollingVelocity: (CGPoint)velocity
{
CGFloat offSetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));
//setting fastPaging property to NO allows to stop at page on screen (I have pages lees, than self.collectionView.bounds.size.width)
CGRect targetRect = CGRectMake(self.fastPaging ? proposedContentOffset.x : self.collectionView.contentOffset.x,
0.0,
self.collectionView.bounds.size.width,
self.collectionView.bounds.size.height);
NSArray *attributes = [self layoutAttributesForElementsInRect:targetRect];
NSPredicate *cellAttributesPredicate = [NSPredicate predicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject,
NSDictionary<NSString *,id> * _Nullable bindings)
{
return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell);
}];
NSArray *cellAttributes = [attributes filteredArrayUsingPredicate: cellAttributesPredicate];
UICollectionViewLayoutAttributes *currentAttributes;
for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
{
currentAttributes = layoutAttributes;
offSetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
CGFloat nextOffset = proposedContentOffset.x + offSetAdjustment;
proposedContentOffset.x = nextOffset;
CGFloat deltaX = proposedContentOffset.x - self.collectionView.contentOffset.x;
CGFloat velX = velocity.x;
// detection form gist.github.com/rkeniger/7687301
// based on http://stackoverflow.com/a/14291208/740949
if (fabs(deltaX) <= FLT_EPSILON || fabs(velX) <= FLT_EPSILON || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0))
{
}
else if (velocity.x > 0.0)
{
// revert the array to get the cells from the right side, fixes not correct center on different size in some usecases
NSArray *revertedArray = [[array reverseObjectEnumerator] allObjects];
BOOL found = YES;
float proposedX = 0.0;
for (UICollectionViewLayoutAttributes *layoutAttributes in revertedArray)
{
if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (itemHorizontalCenter > proposedContentOffset.x) {
found = YES;
proposedX = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
} else {
break;
}
}
}
// dont set on unfound element
if (found) {
proposedContentOffset.x = proposedX;
}
}
else if (velocity.x < 0.0)
{
for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
{
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (itemHorizontalCenter > proposedContentOffset.x)
{
proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
break;
}
}
}
proposedContentOffset.y = 0.0;
return proposedContentOffset;
}
Ответ 5
Свифт версия принятого ответа.
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
Действительно для Swift 3.
Ответ 6
см. этот ответ Дэн Абрамов здесь Быстрая версия
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var _proposedContentOffset = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y)
var offSetAdjustment: CGFloat = CGFloat.max
let horizontalCenter: CGFloat = CGFloat(proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0))
let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
let array: [UICollectionViewLayoutAttributes] = self.layoutAttributesForElementsInRect(targetRect)! as [UICollectionViewLayoutAttributes]
for layoutAttributes: UICollectionViewLayoutAttributes in array {
if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) {
let itemHorizontalCenter: CGFloat = layoutAttributes.center.x
if (abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment)) {
offSetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
}
var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment
repeat {
_proposedContentOffset.x = nextOffset
let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x
let velX = velocity.x
if (deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) {
break
}
if (velocity.x > 0.0) {
nextOffset = nextOffset + self.snapStep()
} else if (velocity.x < 0.0) {
nextOffset = nextOffset - self.snapStep()
}
} while self.isValidOffset(nextOffset)
_proposedContentOffset.y = 0.0
return _proposedContentOffset
}
func isValidOffset(offset: CGFloat) -> Bool {
return (offset >= CGFloat(self.minContentOffset()) && offset <= CGFloat(self.maxContentOffset()))
}
func minContentOffset() -> CGFloat {
return -CGFloat(self.collectionView!.contentInset.left)
}
func maxContentOffset() -> CGFloat {
return CGFloat(self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width)
}
func snapStep() -> CGFloat {
return self.itemSize.width + self.minimumLineSpacing;
}
или gist здесь https://gist.github.com/katopz/8b04c783387f0c345cd9
Ответ 7
Вот моя реализация в 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.
Ответ 8
Для тех, кто ищет решение, которое...
- НЕ ВЫКЛЮЧАЕТСЯ, когда пользователь выполняет короткую быструю прокрутку (то есть учитывает положительную и отрицательную скорости прокрутки)
- учитывает
collectionView.contentInset
(и safeArea на iPhone X) - учитывает только те клетки, которые видны в момент прокрутки (для удобства)
- использует хорошо названные переменные и комментарии
- Свифт 4
тогда, пожалуйста, смотрите ниже...
public class CarouselCollectionViewLayout: UICollectionViewFlowLayout {
override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
// Identify the layoutAttributes of cells in the vicinity of where the scroll view will come to rest
let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
let visibleCellsLayoutAttributes = layoutAttributesForElements(in: targetRect)
// Translate those cell layoutAttributes into potential (candidate) scrollView offsets
let candidateOffsets: [CGFloat]? = visibleCellsLayoutAttributes?.map({ cellLayoutAttributes in
if #available(iOS 11.0, *) {
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left - sectionInset.left
} else {
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - sectionInset.left
}
})
// Now we need to work out which one of the candidate offsets is the best one
let bestCandidateOffset: CGFloat
if velocity.x > 0 {
// If the scroll velocity was POSITIVE, then only consider cells/offsets to the RIGHT of the proposedContentOffset.x
// Of the cells/offsets to the right, the NEAREST is the 'bestCandidate'
// If there is no nearestCandidateOffsetToLeft then we default to the RIGHT-MOST (last) of ALL the candidate cells/offsets
// (this handles the scenario where the user has scrolled beyond the last cell)
let candidateOffsetsToRight = candidateOffsets?.toRight(ofProposedOffset: proposedContentOffset.x)
let nearestCandidateOffsetToRight = candidateOffsetsToRight?.nearest(toProposedOffset: proposedContentOffset.x)
bestCandidateOffset = nearestCandidateOffsetToRight ?? candidateOffsets?.last ?? proposedContentOffset.x
}
else if velocity.x < 0 {
// If the scroll velocity was NEGATIVE, then only consider cells/offsets to the LEFT of the proposedContentOffset.x
// Of the cells/offsets to the left, the NEAREST is the 'bestCandidate'
// If there is no nearestCandidateOffsetToLeft then we default to the LEFT-MOST (first) of ALL the candidate cells/offsets
// (this handles the scenario where the user has scrolled beyond the first cell)
let candidateOffsetsToLeft = candidateOffsets?.toLeft(ofProposedOffset: proposedContentOffset.x)
let nearestCandidateOffsetToLeft = candidateOffsetsToLeft?.nearest(toProposedOffset: proposedContentOffset.x)
bestCandidateOffset = nearestCandidateOffsetToLeft ?? candidateOffsets?.first ?? proposedContentOffset.x
}
else {
// If the scroll velocity was ZERO we consider all 'candidate' cells (regarless of whether they are to the left OR right of the proposedContentOffset.x)
// The cell/offset that is the NEAREST is the 'bestCandidate'
let nearestCandidateOffset = candidateOffsets?.nearest(toProposedOffset: proposedContentOffset.x)
bestCandidateOffset = nearestCandidateOffset ?? proposedContentOffset.x
}
return CGPoint(x: bestCandidateOffset, y: proposedContentOffset.y)
}
}
fileprivate extension Sequence where Iterator.Element == CGFloat {
func toLeft(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {
return filter() { candidateOffset in
return candidateOffset < proposedOffset
}
}
func toRight(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {
return filter() { candidateOffset in
return candidateOffset > proposedOffset
}
}
func nearest(toProposedOffset proposedOffset: CGFloat) -> CGFloat? {
guard let firstCandidateOffset = first(where: { _ in true }) else {
// If there are no elements in the Sequence, return nil
return nil
}
return reduce(firstCandidateOffset) { (bestCandidateOffset: CGFloat, candidateOffset: CGFloat) -> CGFloat in
let candidateOffsetDistanceFromProposed = fabs(candidateOffset - proposedOffset)
let bestCandidateOffsetDistancFromProposed = fabs(bestCandidateOffset - proposedOffset)
if candidateOffsetDistanceFromProposed < bestCandidateOffsetDistancFromProposed {
return candidateOffset
}
return bestCandidateOffset
}
}
}
Ответ 9
Вот мое решение Swift в представлении коллекции с горизонтальной прокруткой. Это просто, мило и избегает любого мерцания.
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return proposedContentOffset }
let currentXOffset = collectionView.contentOffset.x
let nextXOffset = proposedContentOffset.x
let maxIndex = ceil(currentXOffset / pageWidth())
let minIndex = floor(currentXOffset / pageWidth())
var index: CGFloat = 0
if nextXOffset > currentXOffset {
index = maxIndex
} else {
index = minIndex
}
let xOffset = pageWidth() * index
let point = CGPointMake(xOffset, 0)
return point
}
func pageWidth() -> CGFloat {
return itemSize.width + minimumInteritemSpacing
}
Ответ 10
Небольшая проблема, с которой я столкнулся при использовании targetContentOffsetForProposedContentOffset, - проблема, связанная с тем, что последняя ячейка не корректируется в соответствии с новой точкой, которую я вернул.
Я узнал, что возвращаемый CGPoint я имел значение Y больше, чем допустимо, поэтому я использовал следующий код в конце моей реализации targetContentOffsetForProposedContentOffset:
// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
newY = maxY;
}
return CGPointMake(0, newY);
просто для того, чтобы сделать его более ясным, это моя полная реализация макета, которая просто имитирует поведение вертикального пейджинга:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
return [self targetContentOffsetForProposedContentOffset:proposedContentOffset];
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGFloat heightOfPage = self.itemSize.height;
CGFloat heightOfSpacing = self.minimumLineSpacing;
CGFloat numOfPage = lround(proposedContentOffset.y / (heightOfPage + heightOfSpacing));
CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);
// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
newY = maxY;
}
return CGPointMake(0, newY);
}
надеюсь, что это сэкономит время и головную боль.
Ответ 11
Я предпочитаю, чтобы пользователь просматривал несколько страниц. Итак, вот моя версия targetContentOffsetForProposedContentOffset
(на основе ответа DarthMike) для макета вертикальной.
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGFloat approximatePage = self.collectionView.contentOffset.y / self.pageHeight;
CGFloat currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage);
NSInteger flickedPages = ceil(velocity.y / self.flickVelocity);
if (flickedPages) {
proposedContentOffset.y = (currentPage + flickedPages) * self.pageHeight;
} else {
proposedContentOffset.y = currentPage * self.pageHeight;
}
return proposedContentOffset;
}
- (CGFloat)pageHeight {
return self.itemSize.height + self.minimumLineSpacing;
}
- (CGFloat)flickVelocity {
return 1.2;
}
Ответ 12
Ответ Fogmeisters работал у меня, если я не прокрутил до конца строки. Мои ячейки не подходят аккуратно на экране, поэтому он будет прокручиваться до конца и прыгать назад с рывком, чтобы последняя ячейка всегда перекрывала правый край экрана.
Чтобы предотвратить это, добавьте следующую строку кода в начале метода targetcontentoffset
if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
return proposedContentOffset;
Ответ 13
@Андре Абреу Код
версия Swift3
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
Ответ 14
Swift 4
Самое простое решение для просмотра коллекции с ячейками одного размера (горизонтальная прокрутка):
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return proposedContentOffset }
// Calculate width of your page
let pageWidth = calculatedPageWidth()
// Calculate proposed page
let proposedPage = round(proposedContentOffset.x / pageWidth)
// Adjust necessary offset
let xOffset = pageWidth * proposedPage - collectionView.contentInset.left
return CGPoint(x: xOffset, y: 0)
}
func calculatedPageWidth() -> CGFloat {
return itemSize.width + minimumInteritemSpacing
}
Ответ 15
Более короткое решение (при условии, что вы кешируете свои атрибуты макета):
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0)
}
Для этого в контексте:
class Layout : UICollectionViewLayout {
private var cache: [UICollectionViewLayoutAttributes] = []
private static let horizontalPadding: CGFloat = 16
private static let interItemSpacing: CGFloat = 8
override func prepare() {
let (itemWidth, itemHeight) = (collectionView!.bounds.width - 2 * Layout.horizontalPadding, collectionView!.bounds.height)
cache.removeAll()
let count = collectionView!.numberOfItems(inSection: 0)
var x: CGFloat = Layout.horizontalPadding
for item in (0..<count) {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: x, y: 0, width: itemWidth, height: itemHeight)
cache.append(attributes)
x += itemWidth + Layout.interItemSpacing
}
}
override var collectionViewContentSize: CGSize {
let width: CGFloat
if let maxX = cache.last?.frame.maxX {
width = maxX + Layout.horizontalPadding
} else {
width = collectionView!.width
}
return CGSize(width: width, height: collectionView!.height)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache.first { $0.indexPath == indexPath }
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cache.filter { $0.frame.intersects(rect) }
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0)
}
}
Ответ 16
Для тех, кто ищет решение в Swift:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
private let collectionViewHeight: CGFloat = 200.0
private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width
override func awakeFromNib() {
super.awakeFromNib()
self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere])
self.minimumInteritemSpacing = [InsertItemSpacingHere]
self.scrollDirection = .Horizontal
let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2
self.collectionView?.contentInset = UIEdgeInsets(top: 0,
left: inset,
bottom: 0,
right: inset)
}
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.max
let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2)
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight)
var array = super.layoutAttributesForElementsInRect(targetRect)
for layoutAttributes in array! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}