Skip to content

Instantly share code, notes, and snippets.

@BenBarahona
Created October 25, 2013 21:21
Show Gist options
  • Save BenBarahona/7162028 to your computer and use it in GitHub Desktop.
Save BenBarahona/7162028 to your computer and use it in GitHub Desktop.
Modified FGScrollLayer class for creating horizontal or vertical scrolls in cocos2d
//
// FGScrollLayer.h
// Fall G
//
// Created by Dai Xuefeng on 23/9/12.
// Copyright 2012 Nofootbird. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "cocos2d.h"
@class FGScrollLayer;
@protocol FGScrollLayerDelegate
@optional
/** Called when scroll layer begins scrolling.
* Usefull to cancel CCTouchDispatcher standardDelegates.
*/
- (void) scrollLayerScrollingStarted:(FGScrollLayer *) sender;
/** Called at the end of moveToPage:
* Doesn't get called in selectPage:
*/
- (void) scrollLayer: (FGScrollLayer *) sender scrolledToPageNumber: (int) page;
@end
/** Scrolling layer for Menus, like iOS Springboard Screen.
*
* It is a very clean and elegant subclass of CCLayer that lets you pass-in an array
* of layers and it will then create a smooth scroller.
* Complete with the "snapping" effect. You can create screens with anything that can be added to a CCLayer.
*
* @version 0.2.1
*/
@interface FGScrollLayer : CCLayer
{
NSObject <FGScrollLayerDelegate> *delegate_;
// The screen coord of initial point the user starts their swipe.
CGFloat startSwipe_;
// The coord of initial position the user starts theri swipe.
CGFloat startSwipeLayerPos_;
// For what distance user must slide finger to start scrolling menu.
CGFloat minimumTouchLengthToSlide_;
// For what distance user must slide finger to change the page.
CGFloat minimumTouchLengthToChangePage_;
// Internal state of scrollLayer (scrolling or idle).
int state_;
BOOL stealTouches_;
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
// Holds the touch that started the scroll
UITouch *scrollTouch_;
#endif
// parent node for all layers
CCNode* layerParant; // use this node to simulate layer sliding effect
// Holds pages.
NSArray *layers_;
// Holds current pages width offset.
CGFloat pagesOffset_;
// Holds the maximum upper position
CGFloat maxVerticalPos_;
// Holds the maximum side position
CGFloat maxHorizontalPos_;
// holds the visible height / width for this layer
CGFloat visibleSize;
/*sliding effect controls*/
CGFloat slidingSpeed;
CGFloat friction;
BOOL horizontalView;
}
@property (readwrite, assign) NSObject <FGScrollLayerDelegate> *delegate;
#pragma mark Scroll Config Properties
/** Calibration property. Minimum moving touch length that is enough
* to cancel menu items and start scrolling a layer.
*/
@property(readwrite, assign) CGFloat minimumTouchLengthToSlide;
/** Calibration property. Minimum moving touch length that is enough to change
* the page, without snapping back to the previous selected page.
*/
@property(readwrite, assign) CGFloat minimumTouchLengthToChangePage;
/** If YES - when starting scrolling FGScrollLayer will claim touches, that are
* already claimed by others targetedTouchDelegates by calling CCTouchDispatcher#touchesCancelled
* Usefull to have ability to scroll with touch above menus in pages.
* If NO - scrolling will start, but no touches will be cancelled.
* Default is YES.
*/
@property(readwrite) BOOL stealTouches;
#pragma mark Pages Control Properties
/** Offset, that can be used to let user see next/previous page. */
@property(readwrite) CGFloat pagesOffset;
#pragma mark Init/Creation
/**
* create a scroll layer with an array of nodes
* @param nodes is the array containing all the nodes
* @param height is the visible height of this layer, used to calculate maxVerticalPos_
* return a scroll layer
*/
-(id) initWithNode:(NSArray*)nodes visibleHeight:(CGFloat)height horizontalScroller:(BOOL)horizontal;
/**
* Create a scroll layer with an array of nodes
* @param nodes is the array containing all the nodes
* @param height is the visible height of this layer, used to calculate maxVerticalPos_
* return an autoreleased scroll layer
*/
+(id) scrollLayerWithNodes:(NSArray*)nodes visibleHeight:(CGFloat)height horizontalScroller:(BOOL)horizontal;
#pragma mark Misc
/**
* Return the number of pages
*/
-(int) totalPagesCount;
@end
//
// FGScrollLayer.m
// Fall G
//
// Created by Dai Xuefeng on 23/9/12.
// Copyright 2012 Nofootbird. All rights reserved.
//
#import "FGScrollLayer.h"
#import "ReportSize.h"
enum
{
kFGScrollLayerStateIdle,
kFGScrollLayerStateSliding,
kFGScrollLayerStateInertia,
};
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
@interface CCTouchDispatcher (targetedHandlersGetter)
- (id<NSFastEnumeration>) targetedHandlers;
@end
@implementation CCTouchDispatcher (targetedHandlersGetter)
- (id<NSFastEnumeration>) targetedHandlers
{
return targetedHandlers;
}
@end
#endif
@implementation FGScrollLayer
@synthesize delegate = delegate_;
@synthesize minimumTouchLengthToSlide = minimumTouchLengthToSlide_;
@synthesize minimumTouchLengthToChangePage = minimumTouchLengthToChangePage_;
@synthesize pagesOffset = pagesOffset_;
@synthesize stealTouches = stealTouches_;
- (int) totalPagesCount
{
return [layers_ count];
}
#define DEFAULT_FRICTION 2400
-(id) initWithNode:(NSArray*)nodes visibleHeight:(CGFloat)height horizontalScroller:(BOOL)horizontal{
if (self = [super init]) {
NSAssert([nodes count], @"at least one node is necessary in array nodes!");
// Enable Touches/Mouse.
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
self.isTouchEnabled = YES;
#endif
self.stealTouches = YES;
// Set default minimum touch length to scroll.
self.minimumTouchLengthToSlide = 30.0f;
self.minimumTouchLengthToChangePage = 50.0f;
slidingSpeed = 0;
friction = DEFAULT_FRICTION;
horizontalView = horizontal;
// Save array of layers.
layers_ = [[NSArray arrayWithArray:nodes] retain];
[self updatePages:height];
[self schedule:@selector(tick:)];
}
return self;
}
+(id) scrollLayerWithNodes:(NSArray*)nodes visibleHeight:(CGFloat)height horizontalScroller:(BOOL)horizontal{
return [[[self alloc] initWithNode:nodes visibleHeight:height horizontalScroller:horizontal] autorelease];
}
- (void) dealloc
{
self.delegate = nil;
[layers_ release];
layers_ = nil;
[super dealloc];
}
- (void) updatePages:(CGFloat)height
{
if(horizontalView)
{
// Loop through the array and add the screens if needed.
int i = 0;
CGFloat previousPosX = 0;
layerParant = [CCNode node];
[layerParant setPosition:CGPointZero];
[self addChild:layerParant];
for (id<ReportSize> l in layers_)
{
[(CCNode*)l setPosition:ccp(previousPosX, 0)];
[(CCNode*)l setAnchorPoint:ccp(0.5, 0)];
previousPosX += [l getWidth];
if (!((CCNode*)l).parent)
[layerParant addChild:l];
i++;
}
visibleSize = height;
maxHorizontalPos_ = MAX(0, previousPosX - visibleSize) * -1;
NSLog(@"MAX H. POS: %f", maxHorizontalPos_);
}
else
{
// Loop through the array and add the screens if needed.
int i = 0;
CGFloat previousPosY = 0;
layerParant = [CCNode node];
[layerParant setPosition:CGPointZero];
[self addChild:layerParant];
for (id<ReportSize> l in layers_)
{
CGFloat height = [l getHeight];
[(CCNode*)l setPosition:ccp(0, -height - previousPosY)];
[(CCNode*)l setAnchorPoint:ccp(0.5, 0)];
previousPosY += [l getHeight];
if (!((CCNode*)l).parent)
[layerParant addChild:l];
i++;
}
visibleSize = height;
maxVerticalPos_ = MAX(0, previousPosY - visibleSize);
}
}
#pragma mark Touches
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
/** Register with more priority than CCMenu's but don't swallow touches. */
-(void) registerWithTouchDispatcher
{
#if COCOS2D_VERSION >= 0x00020000
CCTouchDispatcher *dispatcher = [[CCDirector sharedDirector] touchDispatcher];
int priority = kCCMenuHandlerPriority - 1;
#else
CCTouchDispatcher *dispatcher = [CCTouchDispatcher sharedDispatcher];
int priority = kCCMenuTouchPriority - 1;
#endif
[dispatcher addTargetedDelegate:self priority: priority swallowsTouches:NO];
}
/** Hackish stuff - stole touches from other CCTouchDispatcher targeted delegates.
Used to claim touch without receiving ccTouchBegan. */
- (void) claimTouch: (UITouch *) aTouch
{
#if COCOS2D_VERSION >= 0x00020000
CCTouchDispatcher *dispatcher = [[CCDirector sharedDirector] touchDispatcher];
#else
CCTouchDispatcher *dispatcher = [CCTouchDispatcher sharedDispatcher];
#endif
// Enumerate through all targeted handlers.
for ( CCTargetedTouchHandler *handler in [dispatcher targetedHandlers] )
{
// Only our handler should claim the touch.
if (handler.delegate == self)
{
if (![handler.claimedTouches containsObject: aTouch])
{
[handler.claimedTouches addObject: aTouch];
}
}
else
{
// Steal touch from other targeted delegates, if they claimed it.
if ([handler.claimedTouches containsObject: aTouch])
{
if ([handler.delegate respondsToSelector:@selector(ccTouchCancelled:withEvent:)])
{
[handler.delegate ccTouchCancelled: aTouch withEvent: nil];
}
[handler.claimedTouches removeObject: aTouch];
}
}
}
}
-(void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event
{
if( scrollTouch_ == touch ) {
scrollTouch_ = nil;
}
}
// these two variables are to make a sliding effect on scroll view
static CGFloat previousTouchPoint = -1;
static CGFloat previousTouchTime = -1;
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
if( scrollTouch_ == nil ) {
scrollTouch_ = touch;
} else {
return NO;
}
CGPoint touchPoint = [touch locationInView:[touch view]];
touchPoint = [[CCDirector sharedDirector] convertToGL:touchPoint];
startSwipe_ = (horizontalView) ? touchPoint.x : touchPoint.y;
startSwipeLayerPos_ = (horizontalView) ? [layerParant position].x : [layerParant position].y;
state_ = kFGScrollLayerStateIdle;
slidingSpeed = 0;
friction = DEFAULT_FRICTION;
return YES;
}
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
if( scrollTouch_ != touch ) {
return;
}
CGPoint touchPoint = [touch locationInView:[touch view]];
touchPoint = [[CCDirector sharedDirector] convertToGL:touchPoint];
// If finger is dragged for more distance then minimum - start sliding and cancel pressed buttons.
// Of course only if we not already in sliding mode
if(horizontalView)
{
if ( (state_ != kFGScrollLayerStateSliding)
&& (fabsf(touchPoint.x-startSwipe_) >= self.minimumTouchLengthToSlide) )
{
state_ = kFGScrollLayerStateSliding;
// Avoid jerk after state change.
startSwipe_ = touchPoint.x;
startSwipeLayerPos_ = [layerParant position].x;
previousTouchPoint = touchPoint.x;
previousTouchTime = [touch timestamp];
if (self.stealTouches)
{
[self claimTouch: touch];
}
if ([self.delegate respondsToSelector:@selector(scrollLayerScrollingStarted:)])
{
[self.delegate scrollLayerScrollingStarted: self];
}
}
}
else
{
if ( (state_ != kFGScrollLayerStateSliding)
&& (fabsf(touchPoint.y-startSwipe_) >= self.minimumTouchLengthToSlide) )
{
state_ = kFGScrollLayerStateSliding;
// Avoid jerk after state change.
startSwipe_ = touchPoint.y;
startSwipeLayerPos_ = [layerParant position].y;
previousTouchPoint = touchPoint.y;
previousTouchTime = [touch timestamp];
if (self.stealTouches)
{
[self claimTouch: touch];
}
if ([self.delegate respondsToSelector:@selector(scrollLayerScrollingStarted:)])
{
[self.delegate scrollLayerScrollingStarted: self];
}
}
}
if (state_ == kFGScrollLayerStateSliding)
{
if(horizontalView)
{
slidingSpeed = (touchPoint.x - previousTouchPoint)/([touch timestamp] - previousTouchTime) / 0.001;
CGFloat desiredX = [layerParant position].x + (touchPoint.x - previousTouchPoint);
// if we already out of the boundary, we need to slow down the drag
//if (desiredX > maxHorizontalPos_ || desiredX < 0) {
if (desiredX < maxHorizontalPos_ || desiredX > 0) {
desiredX = [layerParant position].x + (desiredX - [layerParant position].x) * 0.6;
}
[layerParant setPosition:ccp(desiredX, 0)];
// update scrolling effect variables
previousTouchPoint = touchPoint.x;
previousTouchTime = [touch timestamp];
}
else
{
slidingSpeed = (touchPoint.y - previousTouchPoint)/([touch timestamp] - previousTouchTime) / 0.001;
CGFloat desiredY = [layerParant position].y + (touchPoint.y - previousTouchPoint);
// if we already out of the boundary, we need to slow down the drag
if (desiredY > maxVerticalPos_ || desiredY < 0) {
desiredY = [layerParant position].y + (desiredY - [layerParant position].y) * 0.6;
}
[layerParant setPosition:ccp(0, desiredY)];
// update scrolling effect variables
previousTouchPoint = touchPoint.y;
previousTouchTime = [touch timestamp];
}
}
}
- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
if( scrollTouch_ != touch )
return;
scrollTouch_ = nil;
if ([touch timestamp] - previousTouchTime > 500) {
slidingSpeed = 0;
}else{
slidingSpeed /= 1000;
// set a cap for sliding speed
slidingSpeed = MIN(1400, MAX(-1400, slidingSpeed));
}
// CCLOG(@"sliding speed is %.02f", slidingSpeed);
state_ = kFGScrollLayerStateIdle;
// test whether we can directly into inertia state
[self switchToInertiaState];
previousTouchPoint = -1;
previousTouchTime = -1;
}
// when layer is out of the boundary, we need to use an inernia effect to drag it back
-(void)switchToInertiaState{
CGFloat layerPosition = (horizontalView) ? [layerParant position].x : [layerParant position].y;
CGFloat currentMaxPos = (horizontalView) ? maxHorizontalPos_ : maxVerticalPos_;
if(horizontalView)
{
if (layerPosition < currentMaxPos) {
friction = (layerPosition - currentMaxPos) * 8;
slidingSpeed = -0.5 * friction;
state_ = kFGScrollLayerStateInertia;
}
if (layerPosition > 0) {
friction = -layerPosition * 8;
slidingSpeed = 0.5 * friction;
state_ = kFGScrollLayerStateInertia;
}
}
else
{
if (layerPosition > currentMaxPos) {
friction = (layerPosition - currentMaxPos) * 8;
slidingSpeed = -0.5 * friction;
state_ = kFGScrollLayerStateInertia;
}
if (layerPosition < 0) {
friction = -layerPosition * 8;
slidingSpeed = 0.5 * friction;
state_ = kFGScrollLayerStateInertia;
}
}
}
// handle sliding effect
-(void)tick:(ccTime)dt{
if (state_ == kFGScrollLayerStateIdle)
{
layerParant.position = (horizontalView) ?
ccp([layerParant position].x + slidingSpeed * dt, [layerParant position].y) :
ccp([layerParant position].x, [layerParant position].y + slidingSpeed * dt);
if (slidingSpeed > 0) {
slidingSpeed = MAX(0, slidingSpeed - friction * dt);
}else{
slidingSpeed = MIN(0, slidingSpeed + friction * dt);
}
// test whether we need to switch to inertia state
[self switchToInertiaState];
}
else if (state_ == kFGScrollLayerStateInertia)
{
if(horizontalView)
{
if ([layerParant position].x == maxHorizontalPos_ || [layerParant position].x == 0)
{
slidingSpeed = 0;
}
[layerParant setPosition:ccp([layerParant position].x + slidingSpeed * dt, [layerParant position].y)];
if (slidingSpeed > 0) {
slidingSpeed = MAX(0, slidingSpeed - friction * dt);
[layerParant setPosition:ccp(MIN(maxHorizontalPos_, [layerParant position].x), [layerParant position].y)];
}else if(slidingSpeed < 0){
slidingSpeed = MIN(0, slidingSpeed + friction * dt);
[layerParant setPosition:ccp(MAX(0, [layerParant position].x), [layerParant position].y)];
}
}
else
{
if ([layerParant position].y == maxVerticalPos_ || [layerParant position].y == 0)
{
slidingSpeed = 0;
}
[layerParant setPosition:ccp([layerParant position].x, [layerParant position].y + slidingSpeed * dt)];
if (slidingSpeed > 0) {
slidingSpeed = MAX(0, slidingSpeed - friction * dt);
[layerParant setPosition:ccp([layerParant position].x, MIN(0, [layerParant position].y))];
}else if(slidingSpeed < 0){
slidingSpeed = MIN(0, slidingSpeed + friction * dt);
[layerParant setPosition:ccp([layerParant position].x, MAX(maxVerticalPos_, [layerParant position].y))];
}
}
}
}
#endif
@end
//
// ReportSize.h
// FallG
//
// Created by Dai Xuefeng on 19/5/13.
// Copyright (c) 2013 Nofootbird. All rights reserved.
//
// This protocol is used for rectangle node to report its size.
#import <Foundation/Foundation.h>
@protocol ReportSize <NSObject>
-(CGFloat)getWidth;
-(CGFloat)getHeight;
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment