I have a (dead) Github project that does this with an NSTableView , so hopefully it will work well for an NSCollectionView .
Disclaimer: I wrote this while I was still studying GCD, so keep an eye on the save cycles ... I did not check what I just wrote for errors. feel free to specify any of them :) I just tested this on Mac OS 10.9 and it still works (originally written for 10.7 IIRC), not tested on 10.10.
This whole thing is a hack to be sure, it looks like it requires (it seems, one way or another) asynchronous manipulation of the user interface (I think to prevent infinite recursion). There is probably a cleaner / better way and please share it when you discover it!
I have not touched this for several months, so I canβt remember all the features, but its meat is probably in the NBBTableView code, in which fragments will be inserted.
first there is a subclass of NSAnimation NBBScrollAnimation that handles the effect of the rubber band:
@implementation NBBScrollAnimation @synthesize clipView; @synthesize originPoint; @synthesize targetPoint; + (NBBScrollAnimation*)scrollAnimationWithClipView:(NSClipView *)clipView { NBBScrollAnimation *animation = [[NBBScrollAnimation alloc] initWithDuration:0.6 animationCurve:NSAnimationEaseOut]; animation.clipView = clipView; animation.originPoint = clipView.documentVisibleRect.origin; animation.targetPoint = animation.originPoint; return [animation autorelease]; } - (void)setCurrentProgress:(NSAnimationProgress)progress { typedef float (^MyAnimationCurveBlock)(float, float, float); MyAnimationCurveBlock cubicEaseOut = ^ float (float t, float start, float end) { t--; return end*(t * t * t + 1) + start; }; dispatch_sync(dispatch_get_main_queue(), ^{ NSPoint progressPoint = self.originPoint; progressPoint.x += cubicEaseOut(progress, 0, self.targetPoint.x - self.originPoint.x); progressPoint.y += cubicEaseOut(progress, 0, self.targetPoint.y - self.originPoint.y); NSPoint constraint = [self.clipView constrainScrollPoint:progressPoint]; if (!NSEqualPoints(constraint, progressPoint)) { // constraining the point and reassigning to target gives us the "rubber band" effect self.targetPoint = constraint; } [self.clipView scrollToPoint:progressPoint]; [self.clipView.enclosingScrollView reflectScrolledClipView:self.clipView]; [self.clipView.enclosingScrollView displayIfNeeded]; }); } @end
You should be able to use animation for any control with NSClipView , setting it like this _scrollAnimation = [[NBBScrollAnimation scrollAnimationWithClipView:(NSClipView*)[self superview]] retain];
The trick here is that the NSTableView add-in is NSClipView ; I do not know about NSCollectionView , but I suspect that any scrollable control uses NSClipView .
Next, here's how the NBBTableView subclass uses this animation, although mouse events:
- (void)mouseDown:(NSEvent *)theEvent { _scrollDelta = 0.0; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ if (_scrollAnimation && _scrollAnimation.isAnimating) { [_scrollAnimation stopAnimation]; } }); } - (void)mouseUp:(NSEvent *)theEvent { if (_scrollDelta) { [super mouseUp:theEvent]; // reset the scroll animation dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSClipView* cv = (NSClipView*)[self superview]; NSPoint newPoint = NSMakePoint(0.0, ([cv documentVisibleRect].origin.y - _scrollDelta)); NBBScrollAnimation* anim = (NBBScrollAnimation*)_scrollAnimation; [anim setCurrentProgress:0.0]; anim.targetPoint = newPoint; [anim startAnimation]; }); } else { [super mouseDown:theEvent]; } } - (void)mouseDragged:(NSEvent *)theEvent { NSClipView* clipView=(NSClipView*)[self superview]; NSPoint newPoint = NSMakePoint(0.0, ([clipView documentVisibleRect].origin.y - [theEvent deltaY])); CGFloat limit = self.frame.size.height; if (newPoint.y >= limit) { newPoint.y = limit - 1.0; } else if (newPoint.y <= limit * -1) { newPoint.y = (limit * -1) + 1; } // do NOT constrain the point here. we want to "rubber band" [clipView scrollToPoint:newPoint]; [[self enclosingScrollView] reflectScrolledClipView:clipView]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NBBScrollAnimation* anim = (NBBScrollAnimation*)_scrollAnimation; anim.originPoint = newPoint; }); // because we have to animate asyncronously, we must save the target value to use later // instead of setting it in the animation here _scrollDelta = [theEvent deltaY] * 3.5; } - (BOOL)autoscroll:(NSEvent *)theEvent { return NO; }
I believe that outsorting redefinition is essential for good behavior.
All the code is on my github page , and it contains several other tidbits of touchscreen emulation, if you're interested, such as a simulation for convenient iOS icons (complete with wiggle animations using NSButtons .
Hope this helps :)
Edit: Looks like constrainScrollPoint: deprecated in OS X 10.9. However, it should be pretty trivial to redefine as a category or something else. Perhaps you can adapt the solution from this SO question .