How to fool an OS X app by counting a mouse with a finger?

I am writing a Mac application containing a collection view. This application should run on a large touch screen (Planar's 55 "EP series ). Due to hardware limitations, the touch screen does not send scrolling (or even any multi-touch events). How can I trick the application into thinking that" mousedown + drag " Is it the same as mousescroll?

I got it halfway by subclassing the NSCollectionView and injecting my own NSPanGestureRecognizer handler into it. Unfortunately, the result is awkward and lacks the feel of normal OS X scrolling (i.e., the Speed ​​Effect at the end of scrolling or scrolling at the ends of content).

@implementation UCTouchScrollCollectionView ... - (IBAction)showGestureForScrollGestureRecognizer:(NSPanGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:self]; if (recognizer.state == NSGestureRecognizerStateBegan) { touchStartPt = location; startOrigin = [(NSClipView*)[self superview] documentVisibleRect].origin; } else if (recognizer.state == NSGestureRecognizerStateEnded) { /* Some notes here about a future feature: the Scroll Bounce I don't want to have to reinvent the wheel here, but it appears I already am. Crud. 1. when the touch ends, get the velocity in view 2. Using the velocity and a constant "deceleration" factor, you can determine a. The time taken to decelerate to 0 velocity b. the distance travelled in that time 3. If the final scroll point is out of bounds, update it. 4. set up an animation block to scroll the document to that point. Make sure it uses the proper easing to feel "natural". 5. make sure you retain a pointer or something to that animation so that a touch DURING the animation will cancel it (is this even possible?) */ [self.scrollDelegate.pointSmoother clearPoints]; refreshDelegateTriggered = NO; } else if (recognizer.state == NSGestureRecognizerStateChanged) { CGFloat dx = 0; CGFloat dy = (startOrigin.y - self.scrollDelegate.scrollScaling * (location.y - touchStartPt.y)); NSPoint scrollPt = NSMakePoint(dx, dy); [self.scrollDelegate.pointSmoother addPoint:scrollPt]; NSPoint smoothedPoint = [self.scrollDelegate.pointSmoother getSmoothedPoint]; [self scrollPoint:smoothedPoint]; CGFloat end = self.frame.size.height - self.superview.frame.size.height; CGFloat threshold = self.superview.frame.size.height * kUCPullToRefreshScreenFactor; if (smoothedPoint.y + threshold >= end && !refreshDelegateTriggered) { NSLog(@"trigger pull to refresh"); refreshDelegateTriggered = YES; [self.refreshDelegate scrollViewReachedBottom:self]; } } } 

Note on this implementation: I put together scrollScaling and pointSmoother to try and improve UX scrolling. The touch screen that I use is based on the IR interface and is very nervous (especially when the sun is out).

In case that matters: I'm using Xcode 6 beta 6 (6A280e) on the Yosemite beta (14A329r), and my build goal is 10.10.

Thanks!

+8
objective-c cocoa macos
source share
2 answers

I was able to succeed using NSPanGestureRecognizer and modeling the events of the track scroll wheel. If you simulate them well, you will get a rebound from NSScrollView β€œfor free.”

I don't have open source, but the best resource I found explained what NSScrollView expects is in the next unit test, simulating a pulse scroll. (Here mouseScrollByWithWheelAndMomentumPhases ).

https://github.com/WebKit/webkit/blob/master/LayoutTests/fast/scrolling/latching/scroll-iframe-in-overflow.html

The implementation of mouseScrollByWithWheelAndMomentumPhases gives some tips on how to synthesize scroll events at a low level. One addition that I thought was necessary was to actually set an incremental timestamp to get a scroll view for playing the ball.

https://github.com/WebKit/webkit/blob/master/Tools/WebKitTestRunner/mac/EventSenderProxy.mm

Finally, in order to actually create a decay rate, I used a POPDecayAnimation and changed the speed from NSPanGestureRecognizer to feel similar. This is not ideal, but it remains true for the NSScrollView bounce.

+1
source share

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 .

0
source share

All Articles