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