I'm working on custom value picker inspired by UIPickerView. It looks like that:
As you can see, one of the main features of this picker is the central cell the should be wider than others to make it's neighbours visible beside of the central frame. When you scroll the picker with a pan gesture it should dynamically change central value and adjust cells according to the logic above. And it works just perfect:
The problem is in the tap gesture. When the user select any item on the picker by performing tap on it the picker trying to scroll to that item. But since it's offset was changed by the custom layout UIScrollView scrolls to the wrong point. And it looks like that:
When I trying to scroll to the offscreen cell all things works fine - that cell was not affected by the layout and it's coordinates are correct. The issue rises only for visible cells. I'm completely out of any ideas of how to fix that. You could find the whole project here: Carousel Collection View Test Project
Please find some significant code below.
// CarouselLayout.m
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *array = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
CGRect center = CGRectMake(CGRectGetMidX(visibleRect) - 1.0, 0.0, 2.0, CGRectGetHeight(visibleRect));
CGFloat coreWidth = CGRectGetWidth(self.centralFrame) / 3.0;
for (UICollectionViewLayoutAttributes *attributes in array) {
if (CGRectIntersectsRect(attributes.frame, rect)){
CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
CGFloat offset = 0.0;
CGRect coreFrame = CGRectMake(attributes.center.x - (coreWidth / 2.0), 0.0, coreWidth, CGRectGetHeight(self.centralFrame));
if (CGRectIntersectsRect(center, coreFrame)) {
if (attributes.indexPath.item % 2 == 0) {
self.centralItemOffset = (CGRectGetWidth(self.centralFrame) - CGRectGetWidth(attributes.frame) - 4.0) / 2.0;
if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:didChangeCentralItem:)]) {
[(id <CarouselLayoutDelegate>)self.collectionView.delegate collectionView:self.collectionView layout:self didChangeCentralItem:attributes.indexPath];
}
}
}
offset = (distance > 0) ? -self.centralItemOffset : self.centralItemOffset;
attributes.center = CGPointMake(attributes.center.x + offset, attributes.center.y);
}
}
return array;
}
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0);
CGRect targetRectHorizontal = CGRectMake(proposedContentOffset.x, 0.0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRectHorizontal];
for (UICollectionViewLayoutAttributes *attributes in array) {
if (attributes.indexPath.item % 2 == 1) {
continue;
}
CGFloat itemHorizontalCenter = attributes.center.x;
if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offsetAdjustment)) {
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}
return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}
// ViewController.m
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.item % 2 == 1) {
return;
}
NSString *nextValue = [self valueAtIndexPath:indexPath];
[self scrollToValue:nextValue animated:YES];
self.currentValue = nextValue;
}
- (void)scrollToValue:(NSString *)value animated:(BOOL)animated {
NSIndexPath *targetPath = nil;
NSIndexPath *currentPath = nil;
for (NSString *item in self.itemsArray) {
if (!targetPath && [value isEqualToString:item]) {
targetPath = [NSIndexPath indexPathForItem:([self.itemsArray indexOfObject:item] * 2) inSection:0];
}
if (!currentPath && [self.currentValue isEqualToString:item]) {
currentPath = [NSIndexPath indexPathForItem:([self.itemsArray indexOfObject:item] * 2) inSection:0];
}
if (targetPath && currentPath) {
break;
}
}
if (targetPath && currentPath) {
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}
}
If it's not enough please ask for additional code in comments.
In scrollToValue
method change:
if (targetPath && currentPath) {
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}
into:
if (targetPath && currentPath) {
if (targetPath.row < currentPath.row) {
[self.itemsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.itemsArray.count inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
} else {
[self.itemsCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
[self.itemsCollectionView scrollToItemAtIndexPath:targetPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated];
}
}
and it'll work.