8

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.

Andrei Chevozerov
  • 1,029
  • 1
  • 10
  • 24
  • I think the issue is related to using a UICollectionViewFlowLayout subclass vs your own UICollectionViewLayout. You are adjusting the center in layout attributes but not layoutAttributesForItemAtIndexPath as well. And I bet the content size the flow layout returns doesn't know about your change or if it does it's too late. Try rolling your own layout. – Henry T Kirk Jan 04 '15 at 06:12
  • @HenryTKirk, I'm pretty sure your approach is much more correct than mine. But when I've tried to implement my own layout I faced the same issues and also couple of other. Since the correct answer was given below I've decided to pause my research at this point as I have no more time for this control. – Andrei Chevozerov Jan 04 '15 at 15:18
  • can you send me your custome layout same issue on my layout – Maulik shah Dec 13 '17 at 07:20
  • @Maulikshah, it's been almost three years since that post and I'm not sure if that code is still compiles. But anyway you could find it by following the link in my post. Though you'd better learn how to do your own CollectionLayout and not use hacks described in my post. – Andrei Chevozerov Dec 16 '17 at 13:20

2 Answers2

4

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.

gvuksic
  • 2,983
  • 3
  • 32
  • 37
2

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];
Y.Bonafons
  • 2,329
  • 13
  • 20
  • It'll not work, this offset is incorrect. The problem is very simple: custom layout changes item's center coordinates, not width. And I guess this changes affects contentSize somehow, but don't know how to handle this changes. – Andrei Chevozerov Jan 02 '15 at 05:49