matsuda
5/18/2012 - 3:26 AM

UIScrollViewの無限スクロールサンプル

UIScrollViewの無限スクロールサンプル

#import "InfiniteCarouselView.h"

static int const kTopCarouselViewContentSizeWidth = INT_MAX;

@interface InfiniteCarouselView () {
    NSInteger _leftIndex;
    NSInteger _rightIndex;
}

@property (nonatomic, retain) NSMutableArray *visibleItems;
@property (nonatomic, retain) UIView         *itemContainerView;

- (void)initSubviews;
- (void)tileItemsFromMinX:(CGFloat)minimumVisibleX toMaxX:(CGFloat)maximumVisibleX;
//- (CGPoint)adjustOffset:(CGPoint)contentOffset;
- (void)recenterIfNecessary:(CGPoint)contentOffset;

@end

@implementation InfiniteCarouselView

@synthesize dataSource = _dataSource;
@synthesize carouselDelegate = _carouselDelegate;
@synthesize visibleItems = _visibleItems;
@synthesize itemContainerView = _itemContainerView;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initSubviews];
    }
    return self;
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/

- (void)dealloc
{
    DHC_RELEASE_SAFELY(_visibleItems);
    DHC_RELEASE_SAFELY(_itemContainerView);
    [super dealloc];
}

- (void)initSubviews
{
    self.visibleItems = [[[NSMutableArray alloc] init] autorelease];

    self.itemContainerView = [[[UIView alloc] initWithFrame:self.bounds] autorelease];
    [self addSubview:self.itemContainerView];

    // [self.itemContainerView setUserInteractionEnabled:NO];

    // hide horizontal scroll indicator so our recentering trick is not revealed
    self.bounces = NO;
    self.showsHorizontalScrollIndicator = NO;
    self.showsVerticalScrollIndicator = NO;
}

// recenter content periodically to achieve impression of infinite scrolling
- (void)recenterIfNecessary:(CGPoint)contentOffset
{
    // コンテンツ1個のサイズ : CGSizeMake(102, 135)
    CGSize size = [self.dataSource sizeOfItemCarouselView:self];
    // 1まとまりの個数 : 15
    NSInteger count = [self.dataSource numberOfItemsInCarouselView:self];
    // 1まとまりのコンテンツの高さ : 135 * 15 = 2025
    CGFloat unitWidth = count * size.width;

    NSInteger unit = [self contentSize].width / unitWidth;
    CGFloat minOffsetX = unitWidth;
    CGFloat maxOffsetX = (unit - 1) * unitWidth;
    CGFloat centerOffsetX = unit / 2 * unitWidth; // 6075

    CGPoint realOffset = contentOffset;
    if (realOffset.x < minOffsetX || realOffset.x > maxOffsetX) {
        self.contentOffset = CGPointMake(centerOffsetX, realOffset.x);

        // move content by the same amount so it appears to stay still
        for (UIView *item in self.visibleItems) {
            CGPoint center = [self.itemContainerView convertPoint:item.center toView:self];
            center.x += (centerOffsetX - realOffset.x);
            item.center = [self convertPoint:center toView:self.itemContainerView];
        }
    }
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    // 通常このケースはないはず。念のため
    NSInteger count = [self.dataSource numberOfItemsInCarouselView:self];
    if (count == 0) {
        self.contentSize = self.bounds.size;
        self.scrollEnabled = NO;
        return;
    } else {
        CGSize size = [self.dataSource sizeOfItemCarouselView:self];
        CGFloat w = size.width * count;
        self.contentSize = CGSizeMake(w, self.frame.size.height);

        CGRect contentFrame = self.itemContainerView.frame;
        contentFrame.size = self.contentSize;
        self.itemContainerView.frame = contentFrame;

        self.scrollEnabled = YES;
    }

    // [self recenterIfNecessary:self.contentOffset];

    // tile content in visible bounds
    CGRect visibleBounds = [self convertRect:self.bounds toView:self.itemContainerView];
    CGFloat minimumVisibleX = CGRectGetMinX(visibleBounds);
    CGFloat maximumVisibleX = CGRectGetMaxX(visibleBounds);

    [self tileItemsFromMinX:minimumVisibleX toMaxX:maximumVisibleX];

    // NSLog(@"visibleItems >>> %@", self.visibleItems);
    // NSLog(@"contentOffset >>> %@", NSStringFromCGPoint(self.contentOffset));
}

#pragma mark -
#pragma mark item Tiling

- (UIView *)insertViewAtIndex:(NSInteger)index
{
    UIView *view = [self.dataSource carouselView:self viewAtIndex:index];
    [self.itemContainerView addSubview:view];

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureOnItemContainerView:)];
    [view addGestureRecognizer:tapGesture];
    [tapGesture release];

    return view;
}
- (NSInteger)nextIndex:(NSInteger)index
{
    NSInteger count = [self.dataSource numberOfItemsInCarouselView:self];
    if ([self.visibleItems count] > 0) {
        if (index >= count - 1) {
            index = 0;
        } else {
            index++;
        }
    }
    return index;
}
- (NSInteger)prevIndex:(NSInteger)index
{
    NSInteger count = [self.dataSource numberOfItemsInCarouselView:self];
    if ([self.visibleItems count] > 0) {
        if (index <= 0) {
            index = count - 1;
        } else {
            index--;
        }
    }
    return index;
}
- (CGFloat)placeNewItemOnRight:(CGFloat)rightEdge
{
    NSInteger index = [self nextIndex:_rightIndex];
    UIView *item = [self insertViewAtIndex:index];
    [self.visibleItems addObject:item]; // add rightmost item at the end of the array
    _rightIndex = index;

    CGRect frame = [item frame];
    frame.origin.x = rightEdge;
    frame.origin.y = [self.itemContainerView bounds].size.height - frame.size.height;
    [item setFrame:frame];

    return CGRectGetMaxX(frame);
}

- (CGFloat)placeNewItemOnLeft:(CGFloat)leftEdge
{
    NSInteger index = [self prevIndex:_leftIndex];
    UIView *item = [self insertViewAtIndex:index];
    [self.visibleItems insertObject:item atIndex:0]; // add leftmost item at the beginning of the array
    _leftIndex = index;

    CGRect frame = [item frame];
    frame.origin.x = leftEdge - frame.size.width;
    frame.origin.y = [self.itemContainerView bounds].size.height - frame.size.height;
    [item setFrame:frame];

    return CGRectGetMinX(frame);
}

- (void)tileItemsFromMinX:(CGFloat)minimumVisibleX toMaxX:(CGFloat)maximumVisibleX
{
    // the upcoming tiling logic depends on there already being at least one item in the visibleItems array, so
    // to kick off the tiling we need to make sure there's at least one item
    if ([self.visibleItems count] == 0) {
        [self placeNewItemOnRight:minimumVisibleX];
    }

    // add items that are missing on right side
    UIView *lastItem = [self.visibleItems lastObject];
    CGFloat rightEdge = CGRectGetMaxX([lastItem frame]);
    while (rightEdge < maximumVisibleX) {
        rightEdge = [self placeNewItemOnRight:rightEdge];
    }

    // add items that are missing on left side
    UIView *firstItem = [self.visibleItems objectAtIndex:0];
    CGFloat leftEdge = CGRectGetMinX([firstItem frame]);
    while (leftEdge > minimumVisibleX) {
        leftEdge = [self placeNewItemOnLeft:leftEdge];
    }

    // remove items that have fallen off right edge
    lastItem = [self.visibleItems lastObject];
    while (CGRectGetMinX([lastItem frame]) > maximumVisibleX) {
        [lastItem removeFromSuperview];
        [self.visibleItems removeLastObject];
        NSInteger prevIndex = [self prevIndex:_rightIndex];
        _rightIndex = prevIndex;
        lastItem = [self.visibleItems lastObject];
    }

    // remove items that have fallen off left edge
    firstItem = [self.visibleItems objectAtIndex:0];
    while (CGRectGetMaxX([firstItem frame]) < minimumVisibleX) {
        [firstItem removeFromSuperview];
        [self.visibleItems removeObjectAtIndex:0];
        NSInteger nextIndex = [self nextIndex:_leftIndex];
        _leftIndex = nextIndex;
        firstItem = [self.visibleItems objectAtIndex:0];
    }
}

- (void)handleTapGestureOnItemContainerView:(UITapGestureRecognizer *)gestureRecognizer
{
    UIView *view = gestureRecognizer.view;
    if ([self.carouselDelegate respondsToSelector:@selector(carouselView:didSelectView:)]) {
        [self.carouselDelegate carouselView:self didSelectView:view];
    }
}

@end
#import <UIKit/UIKit.h>

@protocol InfiniteCarouselViewDataSource;
@protocol InfiniteCarouselViewDelegate;

@interface InfiniteCarouselView : UIScrollView <UIScrollViewDelegate>

@property (nonatomic, assign) id<InfiniteCarouselViewDataSource> dataSource;
@property (nonatomic, assign) id<InfiniteCarouselViewDelegate> carouselDelegate;

@end

@protocol InfiniteCarouselViewDataSource <NSObject>
- (NSInteger)numberOfItemsInCarouselView:(InfiniteCarouselView *)carouselView;
- (UIView *)carouselView:(InfiniteCarouselView *)carouselView viewAtIndex:(NSInteger)index;
- (CGSize)sizeOfItemCarouselView:(InfiniteCarouselView *)carouselView;
@end

@protocol InfiniteCarouselViewDelegate <NSObject>
- (void)carouselView:(InfiniteCarouselView *)carouselView didSelectView:(UIView *)selectedView;
@end