targetContentOffsetForProposedContentoffset:withScrollingVelocity без подкласса UICollectionViewFlowLayout
у меня есть очень простой collectionView в моем приложении (только один ряд квадратных миниатюр изображений).
Я хотел бы перехватить прокрутку, чтобы смещение всегда оставляло полное изображение с левой стороны. На данный момент он прокручивается туда, где и оставит обрезанные изображения.
в любом случае, я знаю, мне нужно использовать функцию
- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity
сделать это, но я просто использую стандартный UICollectionViewFlowLayout. Я не разделяю его на подклассы.
есть ли способ перехватывая это без подклассов UICollectionViewFlowLayout?
спасибо
16 ответов:
ОК, ответ Нет, нет никакого способа сделать это без подкласса UICollectionViewFlowLayout.
однако, подклассы это невероятно легко для тех, кто читает это в будущем.
сначала я настроил вызов подкласса
MyCollectionViewFlowLayoutа затем в interface builder я изменил макет представления коллекции на пользовательский и выбрал свой подкласс flow layout.потому что ты делаешь это таким образом, вы можете не указывать размеры деталей и т. д... в ИБ так MyCollectionViewFlowLayout.у меня есть это...
- (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 на левом краю.
Это все, что мне нужно делать. Мне не нужно было устанавливать макет потока в коде вообще.
решение Дэна является ошибочным. Он не обрабатывает пользователя щелкая хорошо. Случаи, когда пользователь быстро щелкает и прокручивает не так много, имеют сбои анимации.
моя предлагаемая альтернативная реализация имеет ту же разбиение на страницы, что и предлагалось ранее, но обрабатывает пользовательское переключение между страницами.
#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; }
после долгого тестирования я нашел решение для привязки к центру с пользовательской шириной ячейки (каждая ячейка имеет diff. ширина), которая фиксирует мерцание. Не стесняйтесь улучшать скрипт.
- (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; }
пока ответ была большая помощь для меня, есть заметное мерцание, когда вы быстро проведите на небольшом расстоянии. Гораздо проще воспроизвести его на устройстве.
я обнаружил, что это всегда происходит, когда
collectionView.contentOffset.x - proposedContentOffset.xиvelocity.xразные поет.мое решение состояло в том, чтобы гарантировать, что
proposedContentOffsetбольшеcontentOffset.xесли скорость положительна, и меньше, если она отрицательная. Это в C#, но должно быть довольно просто перевести на 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; } }
Я просто хочу поставить здесь быструю версию принятого ответа.
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.
смотрите это ответ Дана Абрамова вот Swift версия
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; }или суть здесь https://gist.github.com/katopz/8b04c783387f0c345cd9
вот мое быстрое решение на горизонтальной прокрутке коллекции. Это просто, сладко и избегает любого мерцания.
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 }
для тех, кто ищет решение...
- не дает сбоя, когда пользователь выполняет короткую быструю прокрутку (т. е. он учитывает положительные и отрицательные скорости прокрутки)
- принимает
collectionView.contentInset(и safeArea на iPhone X) в рассмотрение- только считает thoes ячейки видны в точке прокрутки (для peformance)
- использует хорошо названные переменные и комментарии
- Это Swift 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 } else { return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.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 } } }
небольшая проблема, с которой я столкнулся при использовании 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); }надеюсь, это сэкономит кому-то немного времени и головная боль
Я предпочитаю, чтобы пользователь пролистывал несколько страниц. Так вот моя версия
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; }
вот моя реализация в Swift 4.1 на вертикальный подкачка на основе ячеек (как отмечено в комментариях, не забудьте установить по умолчанию подкачку false!):
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: Вот это горизонтальный версия (не проверяли его тщательно, поэтому, пожалуйста, простите любые ошибки):
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 newHorizontalOffset. let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - self.collectionView!.contentInset.left return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y) }
Fogmeisters ответ работал для меня, если я не прокрутил до конца строки. Мои клетки не помещаются аккуратно на экране, поэтому он прокручивается до конца и отпрыгивает назад рывком, чтобы последняя ячейка всегда перекрывала правый край экрана.
чтобы предотвратить это, добавьте следующую строку кода в начале метода targetcontentoffset
if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right) return proposedContentOffset;
@André Abreu ' s Code
версия 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) } }
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 }
более короткое решение (при условии, что вы кэшируете атрибуты макета):
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 { .frame.intersection(proposedEndFrame).width < .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 { .indexPath == indexPath } } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return cache.filter { .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 { .frame.intersection(proposedEndFrame).width < .frame.intersection(proposedEndFrame).width }! return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0) } }
для тех, кто ищет решение в 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) } }
Comments