Created
December 4, 2011 15:47
-
-
Save mrjjwright/1430496 to your computer and use it in GitHub Desktop.
TUILayout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// TUILayout.h | |
// Crew | |
// | |
// Created by John Wright on 11/13/11. | |
// Copyright (c) 2011 AirHeart. All rights reserved. | |
// | |
#define kTUILayoutViewHeight @"kTUILayoutViewHeight" | |
#define kTUILayoutViewWidth @"kTUILayoutViewWidth" | |
#import "TUIKit.h" | |
typedef enum { | |
TUILayoutVertical, | |
TUILayoutHorizontal, | |
} TUILayoutType; | |
#define kTUILayoutAnimation @"TUILayoutAnimation" | |
@interface TUILayoutObject : NSObject | |
+(TUILayoutObject*) objectWithDictionary:(NSDictionary*) dict; | |
@property (nonatomic, strong) NSDictionary *dictionary; | |
@property (nonatomic) CGFloat height; | |
@property (nonatomic) CGFloat width; | |
@property (nonatomic) CGFloat y; | |
@property (nonatomic) CGFloat x; | |
@property (nonatomic,readonly) CGRect calculatedFrame; | |
@property (nonatomic, strong) CAAnimation *animation; | |
@property (nonatomic) BOOL markedForInsertion; | |
@end | |
@protocol TUILayoutView<NSObject> | |
-(void) setLayoutObject:(TUILayoutObject*)object; | |
@end | |
@interface TUILayout : TUIScrollView | |
// Set this to an array of TUILayout objects | |
@property (nonatomic, strong) NSMutableArray *objects; | |
@property (nonatomic) TUILayoutType typeOfLayout; | |
@property (nonatomic, strong) Class viewClass; | |
// Space between views horizontally or vertically | |
@property (nonatomic) CGFloat spaceBetweenViews; | |
-(void) animatedLayout; | |
-(void) applyLayout; | |
-(void) setObjectHeight:(CGFloat) newHeight animated:(BOOL) animated; | |
-(void) clearAllViews; | |
-(void) insertObject:(TUILayoutObject *)object atIndex:(NSInteger) index; | |
- (void) removeObject:(TUILayoutObject*) object; | |
-(void) removeObjectsAtIndexes:(NSIndexSet *)indexes; | |
- (TUIView*) viewAtPoint:(CGPoint) point; | |
- (NSArray *)objectIndexesInRect:(CGRect)rect; | |
-(NSString*) indexKeyForObject:(TUILayoutObject*) object; | |
-(CGRect) rectOfViewAtIndex:(NSInteger) index; | |
-(BOOL) isVerticalScroll:(NSEvent*) event; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// TUILayout.m | |
// Crew | |
// | |
// Created by John Wright on 11/13/11. | |
// Copyright (c) 2011 AirHeart. All rights reserved. | |
// | |
#import "TUILayout.h" | |
#define kTUILayoutDefaultAnimationDuration 1.7 | |
#define kTUILayoutMarkedForRemoval @"markedForRemoval" | |
@implementation TUILayoutObject | |
@synthesize dictionary; | |
@synthesize width; | |
@synthesize height; | |
@synthesize y; | |
@synthesize x; | |
@synthesize animation; | |
@synthesize markedForInsertion; | |
+(TUILayoutObject*) objectWithDictionary:(NSDictionary*) dict { | |
TUILayoutObject *object = [[TUILayoutObject alloc] init]; | |
object.dictionary = dict; | |
return object; | |
} | |
-(CGRect) calculatedFrame { | |
return CGRectMake(self.x, self.y, self.width, self.height); | |
} | |
@end | |
@interface TUILayout() | |
-(void) addNewVisibleViews; | |
- (void) bringNewViewsOntoScreen:(NSArray*) objectIndexesToAdd; | |
-(void) cleanupRemovedAndOffscreenViews; | |
-(void) layoutVertical; | |
-(void) layoutHorizontal; | |
-(void) setCalculatedContentSize; | |
-(CGFloat) offsetAtIndex:(NSInteger) index; | |
-(void) moveObjectsOnScreenAfterPoint:(CGPoint) point byIndexAmount:(NSInteger) indexAmount; | |
- (void) enqueueReusableView:(TUIView *)view; | |
- (TUIView<TUILayoutView> *)dequeueReusableView; | |
- (TUIView<TUILayoutView> *)createView; | |
@end | |
@implementation TUILayout { | |
NSMutableArray *reusableViews; | |
NSMutableDictionary *objectsOnScreen; | |
NSMutableSet *objectsToRemove; | |
CGFloat calculatedContentHeight; | |
CGFloat calculatedContentWidth; | |
BOOL animating; | |
} | |
@synthesize typeOfLayout; | |
@synthesize objects; | |
@synthesize viewClass; | |
@synthesize spaceBetweenViews; | |
- (id)initWithFrame:(CGRect)frame { | |
if((self = [super initWithFrame:frame])) { | |
self.objects = [NSMutableArray array]; | |
objectsOnScreen = [NSMutableDictionary dictionaryWithCapacity:50]; | |
objectsToRemove = [NSMutableSet set]; | |
self.typeOfLayout = TUILayoutVertical; | |
self.spaceBetweenViews = 0; | |
} | |
return self; | |
} | |
#pragma mark - Layout | |
-(void) animatedLayout { | |
__weak TUILayout *weakSelf = self; | |
animating = YES; | |
[TUIView beginAnimations:nil context:nil]; | |
[TUIView setAnimationDuration:kTUILayoutDefaultAnimationDuration]; | |
[TUIView setAnimationCompletionBlock:^(BOOL finished) { | |
[TUIView commitAnimations]; | |
animating = NO; | |
[weakSelf cleanupRemovedAndOffscreenViews]; | |
[self layoutSubviews]; | |
}]; | |
[weakSelf layoutSubviews]; | |
} | |
- (void) layoutSubviews{ | |
// Recycle in any needed new views | |
[self addNewVisibleViews]; | |
[super layoutSubviews]; | |
if (![self.objects count] || ![[objectsOnScreen allKeys] count]) return; | |
// layout the objects on screen | |
[objectsOnScreen enumerateKeysAndObjectsUsingBlock:^(NSString* key, TUIView *v, BOOL *stop) { | |
NSInteger index = [key integerValue]; | |
TUILayoutObject *object = [self.objects objectAtIndex:index]; | |
if (object.animation) { | |
[v.layer addAnimation:object.animation forKey:kTUILayoutAnimation]; | |
object.animation = nil; | |
} | |
CGRect oldFrame = v.frame; // so it can be debugged | |
if (!CGRectEqualToRect(object.calculatedFrame, oldFrame)) | |
{ | |
v.frame = object.calculatedFrame; | |
} | |
if (object.markedForInsertion) { | |
//Newly inserted views shouldn't be moved in | |
[v.layer removeAnimationForKey:@"position"]; | |
[v.layer removeAnimationForKey:@"bounds"]; | |
// Send new views to back so other views animate over it | |
[self sendSubviewToBack:v]; | |
object.markedForInsertion = NO; | |
} | |
[v layoutSubviews]; | |
}]; | |
[self cleanupRemovedAndOffscreenViews]; | |
} | |
-(void) addNewVisibleViews { | |
CGRect rect = self.visibleRect; | |
rect.size.height = self.visibleRect.size.height *2; | |
NSArray *proposedNewObjectIndexes = [self objectIndexesInRect:rect]; | |
NSMutableArray *objectIndexesToAdd = [proposedNewObjectIndexes mutableCopy]; | |
[objectIndexesToAdd removeObjectsInArray:[objectsOnScreen allKeys]]; | |
[self bringNewViewsOntoScreen:objectIndexesToAdd]; | |
} | |
- (void) bringNewViewsOntoScreen:(NSArray*) objectIndexesToAdd { | |
// add new views | |
for (NSString *objectIndex in objectIndexesToAdd) { | |
TUILayoutObject *object = [self.objects objectAtIndex:[objectIndex intValue]]; | |
if([objectsOnScreen objectForKey:objectIndex]) { | |
NSLog(@"!!! Warning: already have a view in place for index %@\n\n\n", object); | |
} else { | |
TUIView<TUILayoutView> * v = [self dequeueReusableView]; | |
v.frame = object.calculatedFrame; | |
v.alpha = 1; | |
[v.layer removeAllAnimations]; | |
// Only add subviews if they are on screen | |
if (!v.superview) { | |
if (object.markedForInsertion) { | |
// fade new views in | |
[self addSubview:v]; | |
CABasicAnimation *fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"]; | |
[fadeAnim setDuration:kTUILayoutDefaultAnimationDuration]; | |
[fadeAnim setFromValue:[NSNumber numberWithFloat:0.0f]]; | |
[fadeAnim setToValue:[NSNumber numberWithFloat:1.0f]]; | |
[v.layer addAnimation:fadeAnim forKey:kTUILayoutAnimation]; | |
} else { | |
[self addSubview:v]; | |
} | |
} | |
[v setLayoutObject:object]; | |
[objectsOnScreen setObject:v forKey:objectIndex]; | |
[v setNeedsLayout]; | |
//object.markedForInsertion = YES; | |
} | |
} | |
} | |
// Remove views marked for removal or no longer on screen | |
-(void) cleanupRemovedAndOffscreenViews { | |
// Don't cleanup if in the middle of an animation that needs to complete | |
if (animating) return; | |
CGRect rect = self.visibleRect; | |
rect.size.height = self.visibleRect.size.height *2; | |
NSMutableArray *indexesToRemove = [[NSMutableArray alloc] init]; | |
[objectsOnScreen enumerateKeysAndObjectsUsingBlock:^(NSString* key, TUIView *v, BOOL *stop) { | |
// check if this view is still on screen | |
if (!CGRectIntersectsRect(v.frame, rect)) { | |
[indexesToRemove addObject:key]; | |
} | |
}]; | |
for (NSString* index in indexesToRemove) { | |
TUIView *v = [objectsOnScreen objectForKey:index]; | |
[self enqueueReusableView:v]; | |
[v removeFromSuperview]; | |
[objectsOnScreen removeObjectForKey:index]; | |
} | |
} | |
- (void) applyLayout { | |
(typeOfLayout == TUILayoutVertical) ? [self layoutVertical] : [self layoutHorizontal]; | |
[self setCalculatedContentSize]; | |
} | |
-(void) setCalculatedContentSize { | |
if (typeOfLayout == TUILayoutVertical) { | |
self.contentSize = CGSizeMake(self.bounds.size.width, calculatedContentHeight); | |
} else { | |
self.contentSize = CGSizeMake(calculatedContentWidth, self.bounds.size.height); | |
} | |
} | |
- (void) layoutVertical { | |
CGFloat offset = 0; | |
for (TUILayoutObject *object in objects) { | |
offset += object.height; | |
} | |
calculatedContentHeight = offset; | |
for (TUILayoutObject *object in self.objects) { | |
offset -= object.height + spaceBetweenViews; | |
object.y = offset + spaceBetweenViews; | |
} | |
} | |
- (void) layoutHorizontal { | |
CGFloat i = 0; | |
CGFloat offset = 0; | |
for (TUILayoutObject *object in self.objects) { | |
object.x = i * (object.width + spaceBetweenViews); | |
i += 1; | |
offset += object.width + self.spaceBetweenViews; | |
} | |
calculatedContentWidth = offset; | |
} | |
-(void) setTypeOfLayout:(TUILayoutType)t { | |
typeOfLayout = t; | |
if (typeOfLayout == TUILayoutHorizontal) { | |
self.horizontalScrolling = YES; | |
} | |
} | |
-(void) setObjectHeight:(CGFloat) newHeight animated:(BOOL) animated { | |
for (TUILayoutObject *object in self.objects) { | |
object.height = newHeight; | |
} | |
[self applyLayout]; | |
[self addNewVisibleViews]; | |
if (animated) { | |
[self animatedLayout]; | |
} else { | |
[self layoutSubviews]; | |
} | |
} | |
# pragma mark - Inserting and Removing objects | |
-(void) clearAllViews { | |
[objectsOnScreen enumerateKeysAndObjectsUsingBlock:^(NSString* key, TUIView *v, BOOL *stop) { | |
[v removeFromSuperview]; | |
[self enqueueReusableView:v]; | |
}]; | |
[objectsOnScreen removeAllObjects]; | |
} | |
-(void) insertObject:(TUILayoutObject *)object atIndex:(NSInteger) index | |
{ | |
// Check for a valid insertion point | |
NSAssert(index >= 0 || index < ([self.objects count]), @"TUILayout object out of range"); | |
// Is the object on screen? | |
// If not, only need to relayout | |
BOOL objectVisible = [objectsOnScreen objectForKey:[NSString stringWithFormat:@"%d", index - 1]] != nil; | |
// Update our model | |
[self.objects insertObject:object atIndex:index]; | |
if (objectVisible) { | |
// This will animate in the object | |
object.markedForInsertion = YES; | |
// Shift objects on screen | |
if (([self.objects count] > index + 1)) { | |
TUILayoutObject *objectAfter = [self.objects objectAtIndex:index + 1]; | |
CGPoint insertionPoint = objectAfter.calculatedFrame.origin; | |
[self moveObjectsOnScreenAfterPoint:insertionPoint byIndexAmount:1]; | |
} | |
} | |
//Apply the layout | |
[self applyLayout]; | |
// Ready to rock! | |
if (objectVisible) { | |
[self animatedLayout]; | |
} else { | |
[self layoutSubviews]; | |
} | |
} | |
- (void) removeObject:(TUILayoutObject*) object { | |
NSAssert(object, @"Must supply object"); | |
NSInteger index = [self.objects indexOfObject:object]; | |
[self removeObjectsAtIndexes:[NSIndexSet indexSetWithIndex:index]]; | |
} | |
-(void) removeObjectsAtIndexes:(NSIndexSet *)indexes { | |
// Update the model | |
[self.objects removeObjectsAtIndexes:indexes]; | |
// If the object isn't visible there isn't anything todo except relayout | |
__block BOOL objectVisible; | |
// Apply the layout and get a new content size | |
// If the content size is less than the bounds | |
// we remove the subview before laying out so that the animation runs correctly | |
BOOL shouldRemoveBeforeLayingOut = NO; | |
[self applyLayout]; | |
if (self.contentSize.height < self.bounds.size.height) { | |
shouldRemoveBeforeLayingOut = YES; | |
} | |
__weak TUILayout *weakSelf = self; | |
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { | |
// Valid index | |
NSAssert(idx >= 0 || idx < ([self.objects count]), @"TUILayout object out of range"); | |
// Animate out view if it's on screen | |
NSString *indexKey = [NSString stringWithFormat:@"%d", idx]; | |
__weak TUIView *v = [objectsOnScreen objectForKey:indexKey]; | |
if (v) { | |
objectVisible = YES; | |
[TUIView animateWithDuration:kTUILayoutDefaultAnimationDuration animations:^{ | |
v.alpha = 0; | |
} completion:^(BOOL finished) { | |
[v removeFromSuperview]; | |
[weakSelf enqueueReusableView:v]; | |
if (shouldRemoveBeforeLayingOut) { | |
[weakSelf animatedLayout]; | |
} | |
}]; | |
// Remove object from screen map | |
[objectsOnScreen removeObjectForKey:indexKey]; | |
} | |
}]; | |
if (objectVisible) { | |
// Shift objects on screen | |
if (([self.objects count] >= [indexes lastIndex] + 1)) { | |
// Our screen map needs updating | |
TUILayoutObject *objectAfter = [self.objects objectAtIndex:[indexes lastIndex]]; | |
CGPoint insertionPoint = objectAfter.calculatedFrame.origin; | |
[self moveObjectsOnScreenAfterPoint:insertionPoint byIndexAmount:-1]; | |
} | |
if (shouldRemoveBeforeLayingOut) return; | |
// Ready to rock! | |
[self animatedLayout]; | |
} else { | |
[self layoutSubviews]; | |
} | |
} | |
// Shift the index of objects to views on the screen by a positive or negative amount | |
-(void) moveObjectsOnScreenAfterPoint:(CGPoint) point byIndexAmount:(NSInteger) indexAmount { | |
if ([objectsOnScreen count]) { | |
NSMutableDictionary *newObjectsOnScreen = [[NSMutableDictionary alloc] init]; | |
[objectsOnScreen enumerateKeysAndObjectsUsingBlock:^(NSString* key, TUIView<TUILayoutView> *v, BOOL *stop) { | |
if (v.frame.origin.y <= point.y) { | |
NSInteger index = [key intValue]; | |
NSString *newIndexKey = [NSString stringWithFormat:@"%d", index + indexAmount]; | |
[newObjectsOnScreen setValue:v forKey:newIndexKey]; | |
TUILayoutObject *nextObject = [self.objects objectAtIndex:index + indexAmount]; | |
[v setLayoutObject:nextObject]; | |
} else { | |
[newObjectsOnScreen setValue:v forKey:key]; | |
} | |
}]; | |
[objectsOnScreen setDictionary:newObjectsOnScreen]; | |
} | |
// Update model | |
} | |
#pragma mark - Geometry | |
-(NSString*) indexKeyForObject:(TUILayoutObject*) object { | |
return [NSString stringWithFormat:@"%d", [self.objects indexOfObject:object]]; | |
} | |
-(CGRect) rectOfViewAtIndex:(NSInteger) index { | |
TUILayoutObject *object = [self.objects objectAtIndex:index]; | |
if (typeOfLayout == TUILayoutVertical) { | |
return CGRectMake(0, [self offsetAtIndex:index], self.bounds.size.width, object.height); | |
} | |
return CGRectMake([self offsetAtIndex:index], 0, object.width, self.bounds.size.height);; | |
} | |
-(CGFloat) offsetAtIndex:(NSInteger) index { | |
CGFloat offset = 0; | |
int i = 0; | |
for (TUILayoutObject *object in self.objects) { | |
offset += (typeOfLayout == TUILayoutVertical) ? object.height : object.width; | |
if (i == index) continue; | |
} | |
return offset; | |
} | |
- (NSArray *)objectIndexesInRect:(CGRect)rect | |
{ | |
NSMutableArray *foundObjects = [NSMutableArray arrayWithCapacity:5]; | |
for(TUILayoutObject *object in self.objects) { | |
if(CGRectIntersectsRect(object.calculatedFrame, rect)) { | |
[foundObjects addObject:[self indexKeyForObject:object]]; | |
} | |
} | |
return foundObjects; | |
} | |
- (TUIView*) viewAtPoint:(CGPoint) point { | |
for (TUIView *view in self.subviews) { | |
if (CGRectContainsPoint(view.frame, point)) { | |
return view; | |
} | |
} | |
return nil; | |
} | |
#pragma mark - View Reuse | |
- (void) enqueueReusableView:(TUIView *)view | |
{ | |
if(!reusableViews) { | |
reusableViews = [[NSMutableArray alloc] init]; | |
} | |
view.alpha =1; | |
view.frame = CGRectZero; | |
[reusableViews addObject:view]; | |
} | |
- (TUIView<TUILayoutView> *)dequeueReusableView | |
{ | |
TUIView<TUILayoutView> *v = [reusableViews lastObject]; | |
if(v) [reusableViews removeLastObject]; | |
if (!v) v = [self createView]; | |
return v; | |
} | |
- (TUIView<TUILayoutView> *)createView { | |
Class class = self.viewClass ? self.viewClass : [TUIView class]; | |
TUIView<TUILayoutView> *v = [[class alloc] initWithFrame:CGRectZero]; | |
return v; | |
} | |
#pragma mark - TUIScrolling Interceptor helper | |
-(BOOL) isVerticalScroll:(NSEvent*) event { | |
// Get the amount of scrolling | |
double dx = 0.0; | |
double dy = 0.0; | |
CGEventRef cgEvent = [event CGEvent]; | |
const int64_t isContinuous = CGEventGetIntegerValueField(cgEvent, kCGScrollWheelEventIsContinuous); | |
if(isContinuous) { | |
dx = CGEventGetDoubleValueField(cgEvent, kCGScrollWheelEventPointDeltaAxis2); | |
dy = CGEventGetDoubleValueField(cgEvent, kCGScrollWheelEventPointDeltaAxis1); | |
} else { | |
CGEventSourceRef source = CGEventCreateSourceFromEvent(cgEvent); | |
if(source) { | |
const double pixelsPerLine = CGEventSourceGetPixelsPerLine(source); | |
dx = CGEventGetDoubleValueField(cgEvent, kCGScrollWheelEventFixedPtDeltaAxis2) * pixelsPerLine; | |
dy = CGEventGetDoubleValueField(cgEvent, kCGScrollWheelEventFixedPtDeltaAxis1) * pixelsPerLine; | |
CFRelease(source); | |
} else { | |
NSLog(@"Critical: NULL source from CGEventCreateSourceFromEvent"); | |
} | |
} | |
if (fabsf(dx) > fabsf(dy)) return NO; | |
return YES; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment