Monday 11 April 2011

SpringBoard



SpringBoard Implmentation


              I've implemented a little iPhone app that reproduces the behavior of the iPhone home screen's icon reorganization interface. (You know, dragging the wiggly icons around.).

The primary classes to look at are TilesViewController and Tile. The view controller implements all of the "logic" of the application, while the Tile class has the animations.
An instance of Tile represents one of the icons, and is derived from CAGradientLayer. The gradient layer properties get set to provide a gloss effect for the tiles. Tile also provides a few animations, initiated by calling these methods:
  • appearDraggable: Changes the tile to be partially transparent, and makes it slightly bigger. This is invoked when the user touches a tile.
  • appearNormal: Reverses the effects of appearDraggable. This is invoked when the tile is released.
  • startWiggling: Starts a tile "wiggling", as in the iPhone home screen while in reorganization mode.
  • stopWiggling: Stops the wiggling effect
The TilesViewController class is pretty straightforward. When the user touches a the screen, the touchesBegan method determines which tile was touched, calls its appearDraggable method, and calls other tiles' startWiggling methods.
As the user drags the tile around the screen, the touchesMoved method moves the dragged tile, and moves the other tiles as needed to provide an open space for it. Core Animation takes care of all the zooming around of the icons.
When the user lets go of the tile, the touchesEnded method drops it in place and removes all the animations.
Things I learned from this project:
  • Turning on the masksToBounds property for layers slows things down quite noticeably.
  • When hit-testing layers, you have to use a layer's presentation layer, not a model layer itself.
  • CAGradientLayer is easy to use.
Here are some things I don't understand. (Maybe some smart person can explain.)
  • When hit-testing to see which layer was touched, I had to do both [touch locationInView:view] and [view convertPoint:location toView:nil]. However, when handling touch-moves, I only have to use [touch locationInView:view]. I don't understand why the coordinate systems are (apparently) different.
 


    Here the code is:

     TilesViewController.h

    #import "uikit/uikit.h"
    #define TILE_ROWS 6
    #define TILE_COLUMNS 4
    #define TILE_COUNT (TILE_ROWS * TILE_COLUMNS)

    @class Tile;

    @interface TilesViewController : UIViewController {
    @private

    CGRect tileFrame[TILE_COUNT];
    Tile *tileForFrame[TILE_COUNT];
    Tile *heldTile;
    int heldFrameIndex;
    CGPoint heldStartPosition;
    CGPoint touchStartLocation;

    }
    @end

    TilesViewController.m
    #import "TilesViewController.h"
    #import "Tile.h"
    #import "QuartzCore/QuartzCore.h"


    #define TILE_WIDTH 57
    #define TILE_HEIGHT 57
    #define TILE_MARGIN 18


    @interface TilesViewController ()
    - (void)createTiles;
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    - (CALayer *)layerForTouch:(UITouch *)touch;
    - (int)frameIndexForTileIndex:(int)tileIndex;
    - (int)indexOfClosestFrameToPoint:(CGPoint)point;
    - (void)moveHeldTileToPoint:(CGPoint)location;
    - (void)moveUnheldTilesAwayFromPoint:(CGPoint)location;
    - (void)startTilesWiggling;
    - (void)stopTilesWiggling;
    @end


    @implementation TilesViewController


    - (void)viewDidLoad {
    [super viewDidLoad];
    [self createTiles];
    }


    - (void)createTiles {
    UIColor *tileColors[] = {
    [UIColor blueColor],
    [UIColor brownColor],
    [UIColor grayColor],
    [UIColor greenColor],
    [UIColor orangeColor],
    [UIColor purpleColor],
    [UIColor redColor],
    };
    int tileColorCount = sizeof(tileColors) / sizeof(tileColors[0]);

    for (int row = 0; row < TILE_ROWS; ++row) {
    for (int col = 0; col < TILE_COLUMNS; ++col) {
    int index = (row * TILE_COLUMNS) + col;
    CGRect frame = CGRectMake(TILE_MARGIN + col * (TILE_MARGIN + TILE_WIDTH),
    TILE_MARGIN + row * (TILE_MARGIN + TILE_HEIGHT),
    TILE_WIDTH, TILE_HEIGHT);

    tileFrame[index] = frame;
    Tile *tile = [[Tile alloc] init];
    tile.tileIndex = index;
    tileForFrame[index] = tile;
    tile.frame = frame;
    tile.backgroundColor = tileColors[index % tileColorCount].CGColor; tile.cornerRadius = 8;
    tile.delegate = self;

    [self.view.layer addSublayer:tile];
    [tile setNeedsDisplay];
    [tile release];
    }
    }
    }

    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    UIGraphicsPushContext(ctx);
    Tile *tile = (Tile *)layer;
    [tile draw];
    UIGraphicsPopContext(); }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    CALayer *hitLayer = [self layerForTouch:[touches anyObject]];
    if ([hitLayer isKindOfClass:[Tile class]]) {
    Tile *tile = (Tile*)hitLayer;
    heldTile = tile;
    touchStartLocation = [[touches anyObject] locationInView:self.view];
    heldStartPosition = tile.position;
    heldFrameIndex = [self frameIndexForTileIndex:tile.tileIndex];
    [tile moveToFront];
    [tile appearDraggable];
    [self startTilesWiggling];
    } }

    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

    if (heldTile) {
    UITouch *touch = [touches anyObject];
    UIView *view = self.view;
    CGPoint location = [touch locationInView:view];
    [self moveHeldTileToPoint:location];
    [self moveUnheldTilesAwayFromPoint:location];
    } }

    - (void)moveHeldTileToPoint:(CGPoint)location {
    float dx = location.x - touchStartLocation.x;
    float dy = location.y - touchStartLocation.y;
    CGPoint newPosition = CGPointMake(heldStartPosition.x + dx, heldStartPosition.y + dy);
    [CATransaction begin];
    [CATransaction setDisableActions:TRUE];
    heldTile.position = newPosition;
    [CATransaction commit];
    }

    - (void)moveUnheldTilesAwayFromPoint:(CGPoint)location {
    int frameIndex = [self indexOfClosestFrameToPoint:location];
    if (frameIndex != heldFrameIndex) {

    [CATransaction begin];
    if (frameIndex < heldFrameIndex) {
    for (int i = heldFrameIndex; i > frameIndex; --i) {

    Tile *movingTile = tileForFrame[i-1];
    movingTile.frame = tileFrame[i];
    tileForFrame[i] = movingTile;
    }
    }

    else if (heldFrameIndex < frameIndex) {
    for (int i = heldFrameIndex; i < frameIndex; ++i) {
    Tile *movingTile = tileForFrame[i+1];
    movingTile.frame = tileFrame[i];
    tileForFrame[i] = movingTile;
    } }
    heldFrameIndex = frameIndex;
    tileForFrame[heldFrameIndex] = heldTile;
    [CATransaction commit]; } }

    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    if (heldTile) {
    [heldTile appearNormal];
    heldTile.frame = tileFrame[heldFrameIndex];
    heldTile = nil; }
    [self stopTilesWiggling]; }

    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {

    [self touchesEnded:touches withEvent:event];
    }

    - (CALayer *)layerForTouch:(UITouch *)touch {

    UIView *view = self.view;
    CGPoint location = [touch locationInView:view];
    location = [view convertPoint:location toView:nil];

    CALayer *hitPresentationLayer = [view.layer.presentationLayer hitTest:location];
    if (hitPresentationLayer) {
    return hitPresentationLayer.modelLayer; }
    return nil; }

    - (int)frameIndexForTileIndex:(int)tileIndex {

    for (int i = 0; i < TILE_COUNT; ++i) {

    if (tileForFrame[i].tileIndex == tileIndex) {
    return i; } }
    return 0; }

    - (int)indexOfClosestFrameToPoint:(CGPoint)point {

    int index = 0;
    float minDist = FLT_MAX;
    for (int i = 0; i < TILE_COUNT; ++i) {
    CGRect frame = tileFrame[i];
    float dx = point.x - CGRectGetMidX(frame);
    float dy = point.y - CGRectGetMidY(frame);
    float dist = (dx * dx) + (dy * dy);
    if (dist < minDist) {
    index = i;
    minDist = dist; } }
    return index; }

    - (void)startTilesWiggling {
    for (int i = 0; i < TILE_COUNT; ++i) {
    Tile *tile = tileForFrame[i];
    if (tile != heldTile) {
    [tile startWiggling]; }
    } }

    - (void)stopTilesWiggling {
    for (int i = 0; i < TILE_COUNT; ++i) {
    Tile *tile = tileForFrame[i];
    [tile stopWiggling]; } }
    @end


    Tile.h
    #import <Foundation/Foundation.h>
    #import <QuartzCore/CAGradientLayer.h>
    #import "CALayer+Additions.h"
    @interface Tile : CAGradientLayer {
        int tileIndex;   
    }
    @property (nonatomic) int tileIndex;

    - (void)draw;
    - (void)appearDraggable;
    - (void)appearNormal;
    - (void)startWiggling;
    - (void)stopWiggling;
    @end

    Tile.m

    #import "Tile.h"
    #import "NSString+Additions.h"
    #import <QuartzCore/QuartzCore.h>

    @interface Tile ()
    - (void)setGlossGradientProperties;
    @end

    @implementation Tile
    @synthesize tileIndex;

    - (id)init {
        self = [super init];
        if (self) {
            [self setGlossGradientProperties];
        }
        return self;
    }

    - (void)setGlossGradientProperties {
        static CGFloat colorComponents0[] = { 1.0, 1.0, 1.0, 0.35 };
        static CGFloat colorComponents1[] = { 1.0, 1.0, 1.0, 0.06 };
         
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGColorRef color0 = CGColorCreate(colorSpace, colorComponents0);
        CGColorRef color1 = CGColorCreate(colorSpace, colorComponents1);
        NSArray *colors = [NSArray arrayWithObjects:(id)color0,
                                                    (id)color1,
                                                    nil];
        CGColorRelease(color0);
        CGColorRelease(color1);
        CGColorSpaceRelease(colorSpace);
       
        self.colors = colors;
        self.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
                                                   [NSNumber numberWithFloat:1.0],
                                                   nil];
        self.startPoint = CGPointMake(0.5f, 0.0f);
        self.endPoint = CGPointMake(0.5f, 0.5f);
    }


    - (void)draw {
        NSString *labelText = [NSString stringWithFormat:@"%d", (int)tileIndex];

        UIFont *font = [UIFont boldSystemFontOfSize:36];
        [[UIColor whiteColor] set];   
        [labelText drawCenteredInRect:self.bounds withFont:font];
    }


    - (void)appearDraggable {
        self.opacity = 0.6;
        [self setValue:[NSNumber numberWithFloat:1.25] forKeyPath:@"transform.scale"];
    }


    - (void)appearNormal {
        self.opacity = 1.0;
        [self setValue:[NSNumber numberWithFloat:1.0] forKeyPath:@"transform.scale"];
    }


    - (void)startWiggling {
        CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-0.05],
                                                [NSNumber numberWithFloat:0.05],
                                                nil];
        anim.duration = 0.09f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        [self addAnimation:anim forKey:@"wiggleRotation"];
       
        anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-1],
                       [NSNumber numberWithFloat:1],
                       nil];
        anim.duration = 0.07f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        anim.additive = YES;
        [self addAnimation:anim forKey:@"wiggleTranslationY"];
    }

    - (void)stopWiggling {
        [self removeAnimationForKey:@"wiggleRotation"];
        [self removeAnimationForKey:@"wiggleTranslationY"];
    }

    @end


    Tile.m // additional features

    #import "Tile.h"
    #import "NSString+Additions.h"
    #import <QuartzCore/QuartzCore.h>


    @interface Tile ()
    - (void)setGlossGradientProperties;
    @end


    @implementation Tile

    @synthesize tileIndex;


    - (id)init {
        self = [super init];
        if (self) {
            [self setGlossGradientProperties];
        }
        return self;
    }


    - (void)setGlossGradientProperties {
        static CGFloat colorComponents0[] = { 1.0, 1.0, 1.0, 0.35 };
        static CGFloat colorComponents1[] = { 1.0, 1.0, 1.0, 0.06 };
         
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGColorRef color0 = CGColorCreate(colorSpace, colorComponents0);
        CGColorRef color1 = CGColorCreate(colorSpace, colorComponents1);
        NSArray *colors = [NSArray arrayWithObjects:(id)color0,
                                                    (id)color1,
                                                    nil];
        CGColorRelease(color0);
        CGColorRelease(color1);
        CGColorSpaceRelease(colorSpace);
       
        self.colors = colors;
        self.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
                                                   [NSNumber numberWithFloat:1.0],
                                                   nil];
        self.startPoint = CGPointMake(0.5f, 0.0f);
        self.endPoint = CGPointMake(0.5f, 0.5f);
    }

    - (void)draw {
        NSString *labelText = [NSString stringWithFormat:@"%d", (int)tileIndex];

        UIFont *font = [UIFont boldSystemFontOfSize:36];
        [[UIColor whiteColor] set];   
        [labelText drawCenteredInRect:self.bounds withFont:font];
    }


    - (void)appearDraggable {
        self.opacity = 0.6;
        [self setValue:[NSNumber numberWithFloat:1.25] forKeyPath:@"transform.scale"];
    }

    - (void)appearNormal {
        self.opacity = 1.0;
        [self setValue:[NSNumber numberWithFloat:1.0] forKeyPath:@"transform.scale"];
    }
    - (void)startWiggling {
        CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-0.05],
                                                [NSNumber numberWithFloat:0.05],
                                                nil];
        anim.duration = 0.09f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        [self addAnimation:anim forKey:@"wiggleRotation"];
       
        anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-1],
                       [NSNumber numberWithFloat:1],
                       nil];
        anim.duration = 0.07f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        anim.additive = YES;
        [self addAnimation:anim forKey:@"wiggleTranslationY"];
    }
    - (void)stopWiggling {
        [self removeAnimationForKey:@"wiggleRotation"];
        [self removeAnimationForKey:@"wiggleTranslationY"];
    }
    @end


     NSString+Additions.h

    #import <Foundation/Foundation.h>

    @interface NSString (Additions)
    - (void)drawCenteredInRect:(CGRect)rect withFont:(UIFont *)font;
    @end

     NSString+Additions.m

    #import "NSString+Additions.h"

    @implementation NSString (Additions)

    - (void)drawCenteredInRect:(CGRect)rect withFont:(UIFont *)font {
        CGSize size = [self sizeWithFont:font];
       
        CGRect textBounds = CGRectMake(rect.origin.x + (rect.size.width - size.width) / 2,
                                       rect.origin.y + (rect.size.height - size.height) / 2,
                                       size.width, size.height);
        [self drawInRect:textBounds withFont:font];   
    }
    @end


    CALayer+Additions.h

    #import <Foundation/Foundation.h>
    #import <QuartzCore/CALayer.h>
    @interface CALayer (Additions)

    - (void)moveToFront;

    @end

    CALayer+Additions.m

    #import "CALayer+Additions.h"
    @implementation CALayer (Additions)

    - (void)moveToFront {
        CALayer *superlayer = self.superlayer;
        [self removeFromSuperlayer];
        [superlayer addSublayer:self];
    }
    @end

    No comments:

    Post a Comment