Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fix scrolling to a visible cell in UICollectionView with custom layout?

I'm working on custom value picker inspired by UIPickerView. It looks like that: Technical prototype screen 1

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: Technical prototype screen 2

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: Technical prototype screen 3

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.

like image 710
Andrei Chevozerov Avatar asked Dec 26 '14 04:12

Andrei Chevozerov


2 Answers

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.

like image 191
gvuksic Avatar answered Nov 04 '22 03:11

gvuksic


I think you need to manually set the content offset in your scrollToValue method:

[self.itemsCollectionView setContentOffset:offset animated:YES];

To find this offset you need to catch the current cell index (currentCell) and to find the width of cell before. For example:

CGFloat xOffset = 0;
for (int i = 0 ; i < currentCell ; i++)
{
     if(i % 2 == 1){
         xOffset += dotCellWidth; // 20 in your case
     }
     else{
         // here you need to find the width of the cell at index i (25 or 34 in your case)
         xOffset += numberCellWidth
     }
}
// finish by adjusting the offset to the center of the current cell
xOffset += currentCellWidth / 2; // 12.5f or 17.f

CGPoint offset = CGPointMake(xOffset, 0.0);
[self.itemsCollectionView setContentOffset:offset animated:YES];
like image 42
Y.Bonafons Avatar answered Nov 04 '22 04:11

Y.Bonafons