Skip to content

Instantly share code, notes, and snippets.

@mrjjwright
Created December 4, 2011 15:47
Show Gist options
  • Save mrjjwright/1430496 to your computer and use it in GitHub Desktop.
Save mrjjwright/1430496 to your computer and use it in GitHub Desktop.
TUILayout
//
// 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
//
// 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