-
-
Save mmick66/9812223 to your computer and use it in GitHub Desktop.
#import "UICollectionViewFlowLayoutCenterItem.h" | |
@implementation UICollectionViewFlowLayoutCenterItem | |
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity | |
{ | |
CGSize collectionViewSize = self.collectionView.bounds.size; | |
CGFloat proposedContentOffsetCenterX = proposedContentOffset.x + self.collectionView.bounds.size.width * 0.5f; | |
CGRect proposedRect = self.collectionView.bounds; | |
// Comment out if you want the collectionview simply stop at the center of an item while scrolling freely | |
// proposedRect = CGRectMake(proposedContentOffset.x, 0.0, collectionViewSize.width, collectionViewSize.height); | |
UICollectionViewLayoutAttributes* candidateAttributes; | |
for (UICollectionViewLayoutAttributes* attributes in [self layoutAttributesForElementsInRect:proposedRect]) | |
{ | |
// == 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 - self.collectionView.bounds.size.width * 0.5f, proposedContentOffset.y); | |
} | |
@end |
CGSize collectionViewSize = self.collectionView.bounds.size;
is unused here
collectionViewSize
is used, if you uncomment changing proposedRect
Is there something that I'm missing here? Disabling the "Paged enabled" and still doesn't quite works. Any ideea?
Updated snippets code for both version which seems to work fine for me
Vertical centering
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let rectBounds:CGRect = self.collectionView!.bounds
let halfHeight:CGFloat = rectBounds.size.height * CGFloat(0.45)
let proposedContentOffsetCenterY:CGFloat = proposedContentOffset.y + halfHeight
let attributesArray:NSArray = self.layoutAttributesForElementsInRect(rectBounds)!
var candidateAttributes:UICollectionViewLayoutAttributes?
for layoutAttributes : AnyObject in attributesArray {
if let _layoutAttributes = layoutAttributes as? UICollectionViewLayoutAttributes {
if _layoutAttributes.representedElementCategory != UICollectionElementCategory.Cell {
continue
}
if candidateAttributes == nil {
candidateAttributes = _layoutAttributes
continue
}
if fabsf(Float(_layoutAttributes.center.y) - Float(proposedContentOffsetCenterY)) < fabsf(Float(candidateAttributes!.center.y) - Float(proposedContentOffsetCenterY)) {
candidateAttributes = _layoutAttributes
}
}
}
if attributesArray.count == 0 {
return CGPointMake(proposedContentOffset.x,proposedContentOffset.y - halfHeight * 2)
}
return CGPointMake(proposedContentOffset.x,candidateAttributes!.center.y - halfHeight)
}
Horizontal centering
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let collectionViewSize: CGSize = self.collectionView!.bounds.size
let rectBounds: CGRect = self.collectionView!.bounds
let halfWidth: CGFloat = rectBounds.size.width * CGFloat(0.50)
let proposedContentOffsetCenterX: CGFloat = proposedContentOffset.x + halfWidth
let proposedRect: CGRect = self.collectionView!.bounds
let attributesArray:NSArray = self.layoutAttributesForElementsInRect(proposedRect)!
var candidateAttributes:UICollectionViewLayoutAttributes?
for layoutAttributes : AnyObject in attributesArray {
if let _layoutAttributes = layoutAttributes as? UICollectionViewLayoutAttributes {
if _layoutAttributes.representedElementCategory != UICollectionElementCategory.Cell {
continue
}
if candidateAttributes == nil {
candidateAttributes = _layoutAttributes
continue
}
if fabsf(Float(_layoutAttributes.center.x) - Float(proposedContentOffsetCenterX)) < fabsf(Float(candidateAttributes!.center.x) - Float(proposedContentOffsetCenterX)) {
candidateAttributes = _layoutAttributes
}
}
}
if attributesArray.count == 0 {
return CGPoint(x: proposedContentOffset.x - halfWidth * 2,y: proposedContentOffset.y)
}
return CGPoint(x: candidateAttributes!.center.x - halfWidth, y: proposedContentOffset.y)
}
I'd like to center vertically, not horizontally (based on @mmick66's example)
It worked awesomely. I just added the following right at the top of the function. This makes the usage more clear.
//For this to work paging must be disabled if not the return value will be overriden down the line by the scroll view
if (self.collectionView.pagingEnabled) {
return proposedContentOffset;
}
It wont work for the last cell. Always stays in the middle if moved couple of pixels.
Am I missing something
@mosn
any ideas on how to fix the last cell?
This tutorial might help/
http://blog.karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/
Vertical concise version
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return super.targetContentOffsetForProposedContentOffset(proposedContentOffset) }
let halfHeight = collectionView.bounds.height / 2
let proposedContentOffsetCenterY = proposedContentOffset.y + halfHeight
let layoutAttributes = layoutAttributesForElementsInRect(collectionView.bounds)
let closest = layoutAttributes?.sort { abs($0.center.y - proposedContentOffsetCenterY) < abs($1.center.y - proposedContentOffsetCenterY) }.first ?? UICollectionViewLayoutAttributes()
return CGPoint(x: proposedContentOffset.x, y: closest.center.y - halfHeight)
}
Any luck with Last cell ? or Any idea how to fix the last cell ?
override func collectionViewContentSize() -> CGSize {
if let
collectionView = collectionView,
first = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0)),
last = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: collectionView.numberOfItemsInSection(0) - 1, inSection: 0))
{
sectionInset = UIEdgeInsets(top: (collectionView.bounds.height / 2) - (first.bounds.height / 2), left: 0, bottom: (collectionView.bounds.height / 2) - (last.bounds.height / 2), right: 0)
}
return super.collectionViewContentSize()
}
You can adjust the sectionInset or collectionView.contentInset.
I am aware that there're many questions was already asked for this issue. But most of the questions are outdated or with no answers. The problem with my implementation is not preview, but its pagination speed?
I am able to show previous/next cell in UICollectionView but when I try to scroll it speedily, its scrolling (by skipping) 1 or 2 pages. This happens when I scrolling it at good speed.
For a note, I'm using custom layout.
This is my code:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = proposedContentOffset.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(round(candidateAttributes.center.x - halfWidth), proposedContentOffset.y);
}
Any help would be highly appreciated.
Great Solution!
There's one caveat though.
It's considering items in the opposite direction of the scroll. If the user scroll even just a little bit to the right they still don't want the current item. So here's my collaboration. (It's swift, but shall be easy enough to translate for those who need Obj-C)
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let collectionViewSize = self.collectionView!.bounds.size
let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width * 0.5
let proposedRect = self.collectionView!.bounds
// Comment out if you want the collectionview simply stop at the center of an item while scrolling freely
// proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in self.layoutAttributesForElementsInRect(proposedRect)! {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != .Cell {
continue
}
// Get collectionView current scroll position
let currentOffset = self.collectionView!.contentOffset
// Don't even bother with items on opposite direction
// You'll get at least one, or else the fallback got your back
if (attributes.center.x < (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x > 0) || (attributes.center.x > (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x < 0) {
continue
}
// First good item in the loop
if candidateAttributes == nil {
candidateAttributes = attributes
continue
}
// Save constants to improve readability
let lastCenterOffset = candidateAttributes!.center.x - proposedContentOffsetCenterX
let centerOffset = attributes.center.x - proposedContentOffsetCenterX
if fabsf( Float(centerOffset) ) < fabsf( Float(lastCenterOffset) ) {
candidateAttributes = attributes
}
}
if candidateAttributes != nil {
// Great, we have a candidate
return CGPoint(x: candidateAttributes!.center.x - collectionViewSize.width * 0.5, y: proposedContentOffset.y)
} else {
// Fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
@mmick66 : I used your given solution for collection view.
But its scrolling didn't stop, it scrolls like there is no paging enabled.
Here you go - fixed the last cell issue ( it needed some "<=" & ">=" love) I incorporated what everyone added so this should center the middle element and keep it centered at the ends using insets.
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let collectionView = collectionView,
first = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0)),
last = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: collectionView.numberOfItemsInSection(0) - 1, inSection: 0))
{
sectionInset = UIEdgeInsets(top: 0, left: collectionView.frame.width / 2 - first.bounds.size.width / 2, bottom: 0, right: collectionView.frame.width / 2 - last.bounds.size.width / 2)
}
let collectionViewSize = self.collectionView!.bounds.size
let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width * 0.5
var proposedRect = self.collectionView!.bounds
// comment this out if you don't want it to scroll so quickly
proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in self.layoutAttributesForElementsInRect(proposedRect)! {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != .Cell {
continue
}
// Get collectionView current scroll position
let currentOffset = self.collectionView!.contentOffset
// Don't even bother with items on opposite direction
// You'll get at least one, or else the fallback got your back
if (attributes.center.x <= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x > 0) || (attributes.center.x >= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x < 0) {
continue
}
// First good item in the loop
if candidateAttributes == nil {
candidateAttributes = attributes
continue
}
// Save constants to improve readability
let lastCenterOffset = candidateAttributes!.center.x - proposedContentOffsetCenterX
let centerOffset = attributes.center.x - proposedContentOffsetCenterX
if fabsf( Float(centerOffset) ) < fabsf( Float(lastCenterOffset) ) {
candidateAttributes = attributes
}
}
if candidateAttributes != nil {
// Great, we have a candidate
return CGPoint(x: candidateAttributes!.center.x - collectionViewSize.width * 0.5, y: proposedContentOffset.y)
} else {
// Fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
Is there a way to have this and only move one page at a time like pagingEnabled?
How could i make it circular or infinite with item at centre?
@DavidSchechter did you find any solution for your concern ????
@mcginnik I loved your answer, just converted it to Swift 3. Working perfect for me.
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let collectionView = collectionView,
let first = layoutAttributesForItem(at: IndexPath(row: 0, section: 0)),
let last = layoutAttributesForItem(at: IndexPath(row: collectionView.numberOfItems(inSection: 0) - 1, section: 0))
{
sectionInset = UIEdgeInsets(top: 0, left: collectionView.frame.width / 2 - first.bounds.size.width / 2, bottom: 0, right: collectionView.frame.width / 2 - last.bounds.size.width / 2)
}
let collectionViewSize = self.collectionView!.bounds.size
let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width * 0.5
var proposedRect = self.collectionView!.bounds
// comment this out if you don't want it to scroll so quickly
proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in self.layoutAttributesForElements(in: proposedRect)! {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != .cell {
continue
}
// Get collectionView current scroll position
let currentOffset = self.collectionView!.contentOffset
// Don't even bother with items on opposite direction
// You'll get at least one, or else the fallback got your back
if (attributes.center.x <= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x > 0) || (attributes.center.x >= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x < 0) {
continue
}
// First good item in the loop
if candidateAttributes == nil {
candidateAttributes = attributes
continue
}
// Save constants to improve readability
let lastCenterOffset = candidateAttributes!.center.x - proposedContentOffsetCenterX
let centerOffset = attributes.center.x - proposedContentOffsetCenterX
if fabsf( Float(centerOffset) ) < fabsf( Float(lastCenterOffset) ) {
candidateAttributes = attributes
}
}
if candidateAttributes != nil {
// Great, we have a candidate
return CGPoint(x: candidateAttributes!.center.x - collectionViewSize.width * 0.5, y: proposedContentOffset.y)
} else {
// Fallback
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
Thanks!
Hi!
Loved the @efremidze answer.
I simple remove "paging snap", and now collection view will move on centered element due several pages:
`
class CollectionViewFlowLayoutCenterItem: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
var result = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
guard let collectionView = collectionView else {
return result
}
let halfWidth = 0.5 * collectionView.bounds.size.width
let proposedContentCenterX = result.x + halfWidth
let targetRect = CGRect(origin: result, size: collectionView.bounds.size)
let layoutAttributes = layoutAttributesForElements(in: targetRect)?
.filter { $0.representedElementCategory == .cell }
.sorted { abs($0.center.x - proposedContentCenterX) < abs($1.center.x - proposedContentCenterX) }
guard let closest = layoutAttributes?.first else {
return result
}
result = CGPoint(x: closest.center.x - halfWidth, y: proposedContentOffset.y)
return result
}
}
`
@iniko1983x's solution works very nicely, although both the first element and last elements are centred instead of left and right aligned respectively
Maybe some one need
override func layoutSubviews() {
super.layoutSubviews()
if
let first = collectionView.layoutAttributesForItem(at: IndexPath(item: 0, section: 0)),
let last = collectionView.layoutAttributesForItem(at: IndexPath(item: collectionView.numberOfItems(inSection: 0) - 1, section: 0)) {
let left = collectionView.frame.width / 2 - first.bounds.size.width / 2
let right = collectionView.frame.width / 2 - last.bounds.size.width / 2
collectionView.contentInset = UIEdgeInsets(top: 0.0, left: left, bottom: 0.0, right: right)
}
self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
class CenterCellCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let collectionViewSize = self.collectionView!.bounds.size
let proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width * 0.5
var proposedRect = self.collectionView!.bounds
// comment this out if you don't want it to scroll so quickly
proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionViewSize.width, height: collectionViewSize.height)
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in self.layoutAttributesForElements(in: proposedRect)! {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != .cell {
continue
}
// Get collectionView current scroll position
let currentOffset = self.collectionView!.contentOffset
// Don't even bother with items on opposite direction
// You'll get at least one, or else the fallback got your back
// swiftlint:disable:next line_length
if (attributes.center.x <= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x > 0) || (attributes.center.x >= (currentOffset.x + collectionViewSize.width * 0.5) && velocity.x < 0) {
continue
}
// First good item in the loop
if candidateAttributes == nil {
candidateAttributes = attributes
continue
}
// Save constants to improve readability
let lastCenterOffset = candidateAttributes!.center.x - proposedContentOffsetCenterX
let centerOffset = attributes.center.x - proposedContentOffsetCenterX
if fabsf( Float(centerOffset) ) < fabsf( Float(lastCenterOffset) ) {
candidateAttributes = attributes
}
}
if candidateAttributes != nil {
// Great, we have a candidate
return CGPoint(x: candidateAttributes!.center.x - collectionViewSize.width * 0.5, y: proposedContentOffset.y)
} else {
// Fallback
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
}
@DavidSchechter did you found the solution for scrolling one cell at a time using this? or anyone else?
This will snap a UICollectionView with no paging to the center of an Item depending on the direction and speed with which the user scrolls