如何欺骗OS X应用程序认为鼠标是一个手指?

时间:2014-08-26 22:38:29

标签: objective-c macos cocoa

我正在编写包含集合视图的Mac应用程序。此应用程序将在大型触摸屏显示器上运行(来自Planar的55" EP series)。由于硬件限制,触摸屏不会发送滚动事件(甚至任何多点触控事件)。 如何欺骗应用程序思考" mousedown + drag"与" mousescroll"?

相同

我通过继承NSCollectionView并在其中实现我自己的NSPanGestureRecognizer处理程序来实现它。不幸的是,结果很笨拙并且没有正常OS X滚动的感觉(即滚动结束时的速度效果,或内容末尾的滚动反弹)。

@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];
        }
    }
}

关于此实现的说明:我将scrollScalingpointSmoother放在一起以尝试改进滚动UX。我使用的触摸屏是基于红外线的并且非常紧张(特别是当太阳出来时)。

如果相关:我在Yosemite beta(14A329r)上使用Xcode 6 beta 6(6A280e),我的构建目标是10.10。

谢谢!

2 个答案:

答案 0 :(得分:1)

我成功地使用NSPanGestureRecognizer并模拟了track-pad滚轮事件。如果你很好地模拟它们,你将获得NSScrollView'免费'的反弹。

我没有公共代码,但是我发现的最好的资源解释了NSScrollView期望的是在下面的模拟动量滚动的单元测试中。 (见mouseScrollByWithWheelAndMomentumPhases此处)。

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

mouseScrollByWithWheelAndMomentumPhases的实现提供了有关如何在低级别合成滚动事件的一些提示。我发现我需要的一个补充是在事件中实际设置一个递增时间戳,以便让滚动视图播放球。

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

最后,为了实际创建衰减速度,我使用POPDecayAnimation并调整NSPanGestureRecognizer的速度来感觉相似。它并不完美,但确实坚持NSScrollView的反弹。

答案 1 :(得分:0)

我有(dead) project on Github使用NSTableView执行此操作,因此希望它适用于NSCollectionView

免责声明:我在学习GCD时写了这篇文章,所以请注意保留周期......我没有审核我刚刚发布的错误。随便指出任何问题:)我刚刚在Mac OS 10.9上测试了它,它仍然可以工作(最初是为10.7 IIRC编写的),未在10.10上测试过。

这整个事情是一个黑客可以确定,看起来它需要(似乎无论如何)异步UI操作(我想防止无限递归)。可能有更清洁/更好的方式,请在发现时分享!

我几个月没有碰到这个,所以我不记得所有细节,但它的核心肯定是在NBBTableView代码中,它将粘贴片段。

首先有一个NSAnimation子类NBBScrollAnimation来处理“橡皮筋”效应:

@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

您应该能够在NSClipView的任何控件上使用动画,方法是设置_scrollAnimation = [[NBBScrollAnimation scrollAnimationWithClipView:(NSClipView*)[self superview]] retain];

这里的诀窍是NSTableView的超级视图是NSClipView;我不知道NSCollectionView,但我怀疑任何可滚动控件都使用NSClipView

接下来是NBBTableView子类如何通过鼠标事件使用该动画:

- (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;
}

我认为自动滚动覆盖对良好行为至关重要。

整个代码都在我的github page上,如果您感兴趣,它还包含其他几个“触摸屏”仿真花絮,例如iOS跳板可安排图标的模拟(使用“摆动”动画完成) NSButtons

希望这会有所帮助:)

编辑:在OS X 10.9中似乎不推荐使用constrainScrollPoint:。但是,重新实现作为一个类别或其他东西应该是相当微不足道的。也许您可以调整this SO question的解决方案。