【问题标题】:How to trick an OS X app into thinking the mouse is a finger?如何欺骗 OS X 应用程序认为鼠标是手指?
【发布时间】:2014-10-20 09:30:36
【问题描述】:

我正在编写一个包含集合视图的 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 放在一起尝试改进滚动用户体验。我使用的触摸屏是基于 IR 的,并且会变得非常紧张(尤其是当太阳出来时)。

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

谢谢!

【问题讨论】:

  • OS X 多点触控 API 不支持事件注入,除非你做了一些 非常 肮脏的事情(手动构建内部事件结构并将它们放入 HID 事件流中...... .甚至这并不总是有效)。我已经被这个问题困扰过很多次了——我很想看到这个问题的答案。
  • 几年前,我通过使用 Cocoa 生成多点触控事件,将它们转换为 CGEvent,然后转换为 Carbon 事件(这需要逆向工程如何在 Carbon 中表示触摸事件,因为通常它们不会根本不会出现在 Carbon 流中……),然后将它们推送到 Carbon 事件流中。不幸的是,即使在旧版本的 OS X 上尝试编译 64 位代码也会给我带来一大堆错误,所以我怀疑它不再有效。

标签: objective-c macos cocoa


【解决方案1】:

我设法使用 NSPanGestureRecognizer 并模拟触控板滚轮事件取得了一些成功。如果你模拟得好,你会'免费'从 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 的反弹。

【讨论】:

    【解决方案2】:

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

    免责声明:我在学习 GCD 时写了这篇文章,所以请注意保留周期……我没有审查我刚刚发布的内容是否存在错误。随时指出:) 我刚刚在 Mac OS 10.9 上测试了它,它仍然可以工作(最初是为 10.7 IIRC 编写的),而不是在 10.10 上测试。

    可以肯定,这整件事是一个 hack,它看起来需要(似乎无论如何)异步 UI 操作(我认为是为了防止无限递归)。可能有更清洁/更好的方法,发现时请分享!

    我已经好几个月没碰这个了,所以我记不起所有细节了,但它的核心肯定是在NBBTableView 代码中,它会粘贴 sn-ps 的。

    首先有一个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跳板可排列图标的模拟(使用@987654339完成“摆动”动画@。

    希望这会有所帮助:)

    编辑: 似乎constrainScrollPoint: 在 OS X 10.9 中已被弃用。但是,将其重新实现为类别或其他东西应该相当简单。也许您可以从this SO question 调整解决方案。

    【讨论】:

    • 谢谢!我刚刚开始研究这个。我注意到你有几个类 NBBVirtualKeyboard 和 NBBKeyboardKeyCell 在那里。那些被放弃而支持不同的键盘解决方案的人吗?它们似乎没有被实施。
    • @SpencerWilliams 此代码不完整。这是对早期项目的一种重写,确实有一个虚拟键盘。如果它对你有好处的话,我仍然有那个代码。
    • 是的,您拥有的任何键盘代码都会有很大帮助!我们必须自己实现屏幕键盘,这是一项相当艰巨的任务
    • 我已经启动了一个开源“软键盘”库here
    猜你喜欢
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    • 2017-04-08
    • 1970-01-01
    • 2023-03-09
    相关资源
    最近更新 更多