Skip to content

Instantly share code, notes, and snippets.

@millenomi
Created November 30, 2009 09:48
Show Gist options
  • Save millenomi/245372 to your computer and use it in GitHub Desktop.
Save millenomi/245372 to your computer and use it in GitHub Desktop.
//
// ILCarousel.h
// Carousel
//
// Created by ∞ on 29/11/09.
// Copyright 2009 __MyCompanyName__. All rights reserved.
//
#import <UIKit/UIKit.h>
// A carousel is a horizontally-scrolling view that lays out a number of provided views, or "picks", in a row. It will also manage its own content size and insets to make sure the user can scroll to "select" any of these picks. Selection is done by moving the view so that its width contains the current centerpoint of the carousel's frame's width, similar to selection in a UIPickerView.
// Unlike a picker, a carousel will not react when the user touches the view it currently manages as picks. However, you can manage those views yourself. To select them, call the -setCurrentPick:animated: method; doing so on view touch will act similarly to a UIPickerView.
// Unlike a picker, a carousel can send messages to its user via the scroll view's delegate as the selection changes temporarily or permanently. It will send -scrollViewDidScroll: messages as the user scrolls the selections, changing the .currentPick temporarily; and will send -scrollViewDidEndDragging:willDecelerate: and -scrollViewDidEndDecelerating: messages when the user has made his lifted his finger, making his final choice. In all these methods, the .currentPick property can be inspected to know what is the currently centered view.
@interface ILCarousel : UIScrollView {
NSArray* picks;
CGFloat pickMargin;
}
// The views that the carousel will present as picks.
// When set, old views will be removed as the receiver's subviews, and the newly set ones readded. The current pick will then be preserved, if still in the array; otherwise, the carousel will scroll to the first pick in the new array, if present.
// IMPLEMENTATION NOTE: The carousel is designed to host a limited amount of picks, since it implements some methods as O(n) operations on the number of picks. It also obviously requires that all views be available at once for layout.
@property(copy) NSArray* picks;
// The margin to the left and right of each pick. Defaults to 5.0.
@property(assign) CGFloat pickMargin;
// The view that is currently in the middle, that is, selected by the user. Setting this property is equivalent to calling -setCurrentPick:animated: with the second parameter as NO.
// IMPLEMENTATION NOTE: Getting and setting this property are both O(n), where n is the number of picks in the view. Do not call this repeatedly if you can avoid it.
@property(assign) UIView* currentPick;
// Centers the given view (which must be one of the views in .picks), making it the current view. If animated, it will animate the centering in the same way -setContentOffset:animated: does.
// Setting this is subject to the same implementation notes as .currentPick.
- (void) setCurrentPick:(UIView*) p animated:(BOOL) a;
// Causes the currently selected view to be centered. This is useful if the view is selected -- that is, if the carousel's bounds' center lies in the space reserved for this view -- but the view is off-center.
// Using this is subject to the same implementation notes as .currentPick and setCurrentPick:animated:.
- (void) recenterOnCurrentPickAnimated:(BOOL) a;
@end
//
// ILCarousel.m
// Carousel
//
// Created by ∞ on 29/11/09.
// Copyright 2009 __MyCompanyName__. All rights reserved.
//
#import "ILCarousel.h"
@interface ILCarousel ()
- (void) setUpCarousel;
- (void) moveViewsInCarouselLine;
@end
@implementation ILCarousel
- (id)initWithFrame:(CGRect) frame;
{
if (self = [super initWithFrame:frame]) {
[self setUpCarousel];
}
return self;
}
- (id) initWithCoder:(NSCoder*) c;
{
if (self = [super initWithCoder:c])
[self setUpCarousel];
return self;
}
- (void) setUpCarousel;
{
self.picks = [NSArray array];
self.pickMargin = 5.0;
self.alwaysBounceHorizontal = YES;
}
@synthesize pickMargin, picks;
- (void) dealloc
{
self.picks = nil;
[super dealloc];
}
// NIB support.
- (void) awakeFromNib;
{
[super awakeFromNib];
[self moveViewsInCarouselLine];
}
#pragma mark Laying out picks.
- (void) setPicks:(NSArray*) p;
{
if (p != picks) {
UIView* currentPick = [[self.currentPick retain] autorelease];
for (UIView* pick in picks)
[pick removeFromSuperview];
[picks release]; picks = [p copy];
if (!picks) return;
for (UIView* pick in picks)
[self addSubview:pick];
[self moveViewsInCarouselLine];
if ([picks containsObject:currentPick])
self.currentPick = currentPick;
else if (picks && [picks count] > 0)
self.currentPick = [picks objectAtIndex:0];
}
}
- (void) setPickMargin:(CGFloat) m;
{
pickMargin = m;
[self moveViewsInCarouselLine];
}
- (void) setFrame:(CGRect) f;
{
[super setFrame:f];
self.contentSize = CGSizeMake(self.contentSize.width, self.bounds.size.height);
[self moveViewsInCarouselLine];
}
- (void) moveViewsInCarouselLine;
{
if (self.picks && [self.picks count] > 0) {
const CGFloat
selfMiddle = self.bounds.size.height / 2.0,
selfHalfLength = self.bounds.size.width / 2.0;
CGFloat x = 0;
for (UIView* v in self.picks) {
x += self.pickMargin;
CGRect f = v.frame;
f.origin.y = selfMiddle - f.size.height / 2;
f.origin.x = x;
v.frame = f;
x += f.size.width;
x += self.pickMargin;
}
self.contentSize = CGSizeMake(x, self.bounds.size.height);
//self.contentSize = CGSizeMake(([[self.picks lastObject] bounds].size.width + 2 * self.pickMargin) * [self.picks count], self.bounds.size.height);
self.contentInset = UIEdgeInsetsMake(0,
selfHalfLength - [[self.picks objectAtIndex:0] bounds].size.width / 2.0 - self.pickMargin,
0,
selfHalfLength - [[self.picks lastObject] bounds].size.width / 2.0 - self.pickMargin);
}
}
- (UIView*) currentPick;
{
CGFloat position = self.contentOffset.x + self.bounds.size.width / 2.0;
for (UIView* v in self.picks) {
if (position >= v.frame.origin.x - self.pickMargin &&
position < v.frame.origin.x + v.frame.size.width + self.pickMargin)
return v;
}
return nil;
}
- (void) setCurrentPick:(UIView *) p;
{
[self setCurrentPick:p animated:NO];
}
- (void) setCurrentPick:(UIView*) p animated:(BOOL) a;
{
NSAssert([self.picks containsObject:p], @"You can only choose a view in the picks array as the current pick");
CGRect r = p.frame;
CGFloat x = r.origin.x + r.size.width / 2.0;
[self setContentOffset:CGPointMake(x - self.bounds.size.width / 2, 0) animated:a];
}
- (void) recenterOnCurrentPickAnimated:(BOOL) a;
{
UIView* v = self.currentPick;
if (v) [self setCurrentPick:v animated:a];
}
@end
//
// ILCarouselController.m
// Carousel
//
// Created by ∞ on 29/11/09.
// Copyright 2009 __MyCompanyName__. All rights reserved.
//
// This is a simple UIScrollViewDelegate implementation for ILCarousel
// that adds snap-back-to-selection (recentering) and prevents the current
// selection from picking up touches and preventing scroll (in the demo,
// picks are UIButtons).
// The buttons themselves have an action that simply uses setCurrentPick:animated:
// to pick up left/right touches and move the selection to them.
#import "ILCarouselController.h"
#import "ILCarousel.h"
@implementation ILCarouselController
- (void) scrollViewDidScroll:(UIScrollView *)scrollView;
{
ILCarousel* carousel = (ILCarousel*) scrollView;
NSLog(@"%@", NSStringFromCGPoint(scrollView.contentOffset));
UIView* pick = carousel.currentPick;
pick.alpha = 1.0;
pick.userInteractionEnabled = NO;
[UIView beginAnimations:nil context:NULL];
for (UIView* v in carousel.picks) {
if (pick != v) {
v.alpha = 0.8;
v.userInteractionEnabled = YES;
}
}
[UIView commitAnimations];
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
ILCarousel* carousel = (ILCarousel*) scrollView;
if (!decelerate)
[carousel recenterOnCurrentPickAnimated:YES];
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
ILCarousel* carousel = (ILCarousel*) scrollView;
[carousel recenterOnCurrentPickAnimated:YES];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment