Hi,
I've always wanted to do a pure Sparrow equivalent of UIScrollView, and now that we have clipping support in SPSprite as standard, it is perfect to implement this.
So, with the help of Daniel's intertial code snippet, I have a basic prototype working.
The scroll view should be able to hold any type of Sparrow content without modification (that means no custom SPButtons with special event listeners etc). The content should either be scrolled or triggered.
The scroll view should listen for touches, and figure out whether to scroll its view or allow the contents to receive the tap.
So, to do this I laid a transparent SPQuad over the scroll view to initially receive the touches, figure out if the finger is being dragged or simply tapped, and act accordingly.
The scrolling stuff works perfectly, but when a simple tap/touch is detected, passing it to the relevant underlying object is much more of a pain than it ought to be.
In the code below I am identifying which object the touch coincides with, and forwarding the SPTouchEvent to it. It gets the event, however touchEvent.touches will never include a touch with the target set to the correct object. That is because the SPQuad intercepts and recieves all the touches itself.
Because SPTouch is internal to Sparrow, I cannot modify/spoof the touch and change the target to the correct object.
I have thus far done a workaround by sending SP_EVENT_TYPE_TRIGGERED to the child too (this works because I am using SPButtons). The buttons get triggered, but it is far from ideal, for example the growing/shrinking of the buttons doesn't work properly.
Ideally I would send a touch phase began and a touch phase ended to the objects. They may not be SPButtons, so cannot respond to the triggered event.
Events are supposed to bubble up the display hierarchy, but they can only do this by bubbling from PARENT to PARENT. When you have a selection of objects, and a view presented over the top of them, then none of them are the parent of the overlay view, and thus never receive the bubbled event. So even though they are part of the VISIBLE view hierarchy, they are not part of the INTERNAL view hierarchy (parent to parent).
An alternative I have played with is setting the onTouch listener on self or _contentSprite (and removing touchQuad completely). In this case, the scrolling works, but then the buttons ALWAYS receive the events too, and since they are at the bottom of the view hierarchy, they recieve the touches FIRST and so the scroll view cannot intercept the touches if a drag is detected.
So, ideally, I would either be able to bubble events up the "visible" display hierarchy correctly, or modify/pass on SPTouch events to specified NEW target objects.
Any ideas? Can the community come up with a workaround that has thus far eluded me?
The key here is that the scroll view must be able to contain ANY SPDisplayObject.
(as an aside, the class isn't finished yet, I've still got to add scroll limit, bounce effect and other stuff).
You can just drop this into any project and play around:
Usage:
SXScrollViewSprite *scroll1 = [[SXScrollViewSprite alloc] initWithWidth:300.0f andHeight:150.0f];
[scroll1 setBackgroundColor:0xff0000 andAlpha:0.5f];
[self addChild:scroll1];
SPButton *b1...;
[scroll1 addContent:b1];
//
// SXScrollViewSprite.h
// Sparrow Extension
@interface SXScrollViewSprite : SPSprite
-(instancetype)initWithWidth:(float)width andHeight:(float)height;
-(void)setBackgroundColor:(uint)newcolor andAlpha:(float)newalpha;
-(void)addContent:(SPDisplayObject *)child;
-(void)addContent:(SPDisplayObject *)child atIndex:(int)index;
@end
//
// SXScrollViewSprite.m
// Sparrow Extension
#import "SXScrollViewSprite.h"
@implementation SXScrollViewSprite {
SPQuad *_fillerQuad;
SPQuad *_touchQuad;
SPSprite *_contentSprite;
SPPoint *startPoint;
BOOL _touching;
float _lastScrollDist;
}
-(instancetype)initWithWidth:(float)width andHeight:(float)height {
if (self = [super init]) {
_fillerQuad = [SPQuad quadWithWidth:width height:height color:0x000000];
_fillerQuad.alpha = 0.0f;
[self addChild:_fillerQuad];
self.clipRect = _fillerQuad.bounds;
_contentSprite = [[SPSprite alloc] init];
[self addChild:_contentSprite];
_touchQuad = [SPQuad quadWithWidth:width height:height color:0x000000];
_touchQuad.alpha = 0.0f;
[self addChild:_touchQuad];
[_touchQuad addEventListener:@selector(onTouch:) atObject:self forType:SP_EVENT_TYPE_TOUCH];
[self addEventListener:@selector(onEnterFrame:) atObject:self forType:SP_EVENT_TYPE_ENTER_FRAME];
}
return self;
}
-(void)setBackgroundColor:(uint)newcolor andAlpha:(float)newalpha {
_fillerQuad.color = newcolor;
_fillerQuad.alpha = newalpha;
}
-(void)addContent:(SPDisplayObject*)child {
[_contentSprite addChild:child];
}
-(void)addContent:(SPDisplayObject *)child atIndex:(int)index {
[_contentSprite addChild:child atIndex:index];
}
-(void)onTouch:(SPTouchEvent*)touchEvent {
_touching = [[touchEvent touchesWithTarget:self andPhase:SPTouchPhaseEnded] count] == 0;
if (!_touching) {
SPPoint *endPoint = [[[touchEvent touchesWithTarget:self andPhase:SPTouchPhaseEnded] anyObject] locationInSpace:self];
if (fabs(startPoint.y - endPoint.y) < 5.0f) {
for (int i = 0; i < [_contentSprite numChildren]; i++) {
if ([[[_contentSprite childAtIndex:i] boundsInSpace:self] containsPoint:endPoint]) {
// We have FOUND the damned object, now PLEASE can we interact with it???
SPTouchEvent *newTouch = [SPTouchEvent eventWithType:SP_EVENT_TYPE_TOUCH touches:touchEvent.touches];
[[_contentSprite childAtIndex:i] dispatchEvent:newTouch];
[[_contentSprite childAtIndex:i] dispatchEvent:[SPEvent eventWithType:SP_EVENT_TYPE_TRIGGERED]];
return;
}
}
}
return;
}
SPTouch *touch = [[touchEvent touchesWithTarget:self andPhase:SPTouchPhaseMoved] anyObject];
SPPoint *localPos = [touch locationInSpace:self];
SPPoint *previousLocalPos = [touch previousLocationInSpace:self];
_lastScrollDist = localPos.y - previousLocalPos.y;
_contentSprite.y += _lastScrollDist;
SPTouch *touch2 = [[touchEvent touchesWithTarget:self andPhase:SPTouchPhaseBegan] anyObject];
if (touch2) {
startPoint = [touch2 locationInSpace:self];
//NSLog(@"here 1");
}
}
-(void)onEnterFrame:(SPEnterFrameEvent*)event {
if (!_touching) {
float slowDown = 0.9f;
if (fabsf(_lastScrollDist) < 0.5f) {
slowDown = 0.0f;
}
_lastScrollDist *= slowDown;
_contentSprite.y += _lastScrollDist;
}
}
@end
Any help on this, plus any further comments/critiques welcomed 🙂