10

我想通过使用事件 swipeWithEvent 来实现在我的 webView 中向后和第四次导航的能力。但是,我不知道我应该如何使用它。

我有一个主要的 webView,以及向后和第四个导航的两种方法。这里的一个大问题是我不确定如何写这个问题。我只需要知道如何识别我的 webView 上的滑动手势并调用我的两个方法。类似于 Safari、Google Chrome 和 Mozilla Firefox 所做的事情。谢谢。

编辑 我已经实现了这些方法,这些方法可以让我前后滑动。

- (void)swipeWithEvent:(NSEvent *)event {
    NSLog(@"Swipe With Event");
    CGFloat x = [event deltaX];
    //CGFloat y = [event deltaY];

    if (x != 0) {
        (x > 0) ? [self goBack:self] : [self goForward:self];
    }
}


-(BOOL)recognizeTwoFingerGestures
{
    NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
    return [defaults boolForKey:@"AppleEnableSwipeNavigateWithScrolls"];
}

- (void)beginGestureWithEvent:(NSEvent *)event
{
    if (![self recognizeTwoFingerGestures])
        return;

    NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseAny inView:nil];

    self.twoFingersTouches = [[NSMutableDictionary alloc] init];

    for (NSTouch *touch in touches) {
        [twoFingersTouches setObject:touch forKey:touch.identity];
    }
}

- (void)endGestureWithEvent:(NSEvent *)event
{
    if (!twoFingersTouches) return;

    NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseAny inView:nil];

    // release twoFingersTouches early
    NSMutableDictionary *beginTouches = [twoFingersTouches copy];
    self.twoFingersTouches = nil;

    NSMutableArray *magnitudes = [[NSMutableArray alloc] init];

    for (NSTouch *touch in touches)
    {
        NSTouch *beginTouch = [beginTouches objectForKey:touch.identity];

        if (!beginTouch) continue;

        float magnitude = touch.normalizedPosition.x - beginTouch.normalizedPosition.x;
        [magnitudes addObject:[NSNumber numberWithFloat:magnitude]];
    }

    // Need at least two points
    if ([magnitudes count] < 2) return;

    float sum = 0;

    for (NSNumber *magnitude in magnitudes)
        sum += [magnitude floatValue];

    // Handle natural direction in Lion
    BOOL naturalDirectionEnabled = [[[NSUserDefaults standardUserDefaults] valueForKey:@"com.apple.swipescrolldirection"] boolValue];

    if (naturalDirectionEnabled)
        sum *= -1;

    // See if absolute sum is long enough to be considered a complete gesture
    float absoluteSum = fabsf(sum);

    if (absoluteSum < kSwipeMinimumLength) return;

    // Handle the actual swipe
    if (sum > 0)
    {
        [self goForward:self];
    } else
    {
        [self goBack:self];
    }


}

但是,这段代码没有做任何事情。它似乎根本没有被调用。

4

3 回答 3

21

对于现代操作系统(Lion 和更新版本),这是“我如何用 Y 做 X?”之一。答案是“不要使用 Y”的问题。

-swipeWithEvent:用于实现 10.6 风格的触控板滑动,屏幕上的内容不会随着滑动而移动。大多数 Mac 触控板不再配置为允许这种滑动;触控板首选项窗格中的“在页面之间滑动”首选项必须设置为“用三个手指滑动”才能使用,这既不是默认设置,也不是用户更改的常用设置。

狮子风格的“流畅滑动”作为滚动事件出现。Xcode SDK 中的 PictureSwiper 示例项目是一个很好的起点,但作为概述,您可以这样做:

如果只需要支持10.8+

使用NSPageController历史堆栈模式。

如果需要支持 10.7

  1. 认真考虑仅支持 10.8+,至少对于此功能。手动实现它是一个巨大的混乱。别说我没警告过你。

  2. 创建一个自定义视图,它是您希望可滑动的任何视图的超级视图。

  3. 覆盖-wantsScrollEventsForSwipeTrackingOnAxis:以返回YES适当的轴。如果您正在执行 Safari 风格的后退/前进导航,那就是NSEventGestureAxisHorizontal.

  4. 深呼吸;这个太棒了。

  5. -scrollWheel:在您的自定义视图中覆盖。您的覆盖应该调用-trackSwipeEventWithOptions:dampenAmountThresholdMin:max:usingHandler:. 粗略地说,衰减量阈值最小值是用户可以向左滑动的数据浏览量,最大值是他们可以向右滑动的数据量。处理程序是一个在用户滑动时重复调用的块;它应该:

    1. 在内容视图后面放置一个NSImageView带有您要返回/前进到的页面的屏幕截图。
    2. 移动内容视图以匹配用户的动作。请注意,gestureAmount与阻尼量阈值一样,该参数是项目的(小数,可能是负数)数量;您必须将其乘以视图宽度才能正确定位内容视图。
    3. 如果手势阶段是NSEventPhaseEnded,则评估gestureAmount以确定用户是否完成了手势。如果他们没有,将内容视图动画化回原位;如果他们这样做了,请将内容视图放回原位,不使用动画并更新它以匹配屏幕截图。

如您所见,实际实现处理程序非常复杂,我什至没有描述所有细节。即使有了所有这些细节,一个技术高超的程序员也可能需要花几天时间才能做到这一点。10.7 SDK 中的 PictureSwiper 示例是一个很好的起点。(PictureSwiper 10.8 版本使用NSPageController. 就像你应该做的那样。)

于 2012-12-05T05:39:32.393 回答
10

10.7 发行说明

流体滑动跟踪 - API

以下 API 允许将手势滚轮事件作为流畅的滑动手势进行跟踪。与 iOS 类似,NSScrollView 将在需要时反弹一次,然后可选择传递手势滚轮事件,以便您的控制器可以使用此 API 将滚动手势作为流畅的滑动手势进行跟踪。

ScrollWheel NSEvents 现在响应 -phase 消息。卷轴有3种:

1) 手势滚动——这些滚动以 NSEventPhaseBegan 开始,有许多 NSEventPhaseChanged 事件,并在用户用 NSEventPhaseEnded 抬起手指时终止。

2) Momentum 滚动 - 这些具有 NSEventPhase none 的阶段,但它们具有 NSEventPhaseBegan/NSEventPhaseChanged/NSEventPhaseEnded 的momentumPhase。

3) Legacy scrolls - 这些滚轮事件有一个 NSEventPhaseNone 的阶段和一个 NSEventPhaseNone 的动量阶段。无法确定用户何时开始或停止执行传统滚动。

NSScrollView 处理所有手势滚轮事件并且不会将它们传递给响应者链。通常,在响应者链中跟踪滑动是在更高级别完成的,例如在 WindowController 级别。要实现 iOS 风格的“不在边缘反弹,否则滑动”行为,您需要通知 NSScrollView 它应该在适当的时候转发滚轮消息。您的 NSResponder 可以实现以下方法并返回 YES,而不是手动设置 NSScrollView 上的属性。

- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(NSEventGestureAxis)axis;

当适当的控制器接收到带有非 NSEventNone 阶段的 -scrollWheel: 消息时,您可以在该事件实例上调用以下消息来跟踪滑动或滚动到用户完成事件和动画完成。

enum {
    NSEventSwipeTrackingLockDirection = 0x1 << 0,
    NSEventSwipeTrackingClampGestureAmount = 0x1 << 1
};

typedef NSUInteger NSEventSwipeTrackingOptions;

@interface NSEvent ...
- (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options
          dampenAmountThresholdMin:(CGFloat)minDampenThreshold
                               max:(CGFloat)maxDampenThreshold
                      usingHandler:(void (^)(CGFloat gestureAmount, NSEventPhase phase, > BOOL isComplete, BOOL *stop))handler;
...

下面是一个像 iOS Photo 应用一样滑动图片集合的伪代码示例。

- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(NSEventGestureAxis)axis {
    return (axis == NSEventGestureAxisHorizontal) ? YES : NO; }
- (void)scrollWheel:(NSEvent *)event {
    // NSScrollView is instructed to only forward horizontal scroll gesture events (see code above). However, depending
    // on where your controller is in the responder chain, it may receive other scrollWheel events that we don't want
    // to track as a fluid swipe because the event wasn't routed though an NSScrollView first.
    if ([event phase] == NSEventPhaseNone) return; // Not a gesture scroll event.
    if (fabsf([event scrollingDeltaX]) <= fabsf([event scrollingDeltaY])) return; // Not horizontal
    // If the user has disabled tracking scrolls as fluid swipes in system preferences, we should respect that.
    // NSScrollView will do this check for us, however, depending on where your controller is in the responder chain,
    // it may scrollWheel events that are not filtered by an NSScrollView.
    if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) return;
    if (_swipeAnimationCanceled && *_swipeAnimationCanceled == NO) {
        // A swipe animation is still in gestureAmount. Just kill it.
        *_swipeAnimationCanceled = YES;
        _swipeAnimationCanceled = NULL;
    }
    CGFloat numPhotosToLeft = // calc num of photos we can move to the left and negate
    CGFloat numPhotosToRight = // calc num of photos we can move to the right
    __block BOOL animationCancelled = NO;
    [event trackSwipeEventWithOptions:0 dampenAmountThresholdMin:numPhotosToLeft max:numPhotosToRight
                         usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, BOOL isComplete, BOOL *stop) {
        if (animationCancelled) {
            *stop = YES;
            // Tear down animation overlay
            return;
        }
        if (phase == NSEventPhaseBegan) {
            // Setup animation overlay layers
        }
        // Update animation overlay to match gestureAmount
        if (phase == NSEventPhaseEnded) {
            // The user has completed the gesture successfully.
            // This handler will continue to get called with updated gestureAmount
            // to animate to completion, but just in case we need
            // to cancel the animation (due to user swiping again) setup the
            // controller / view to point to the new content / index / whatever
        } else if (phase == NSEventPhaseCancelled) {
            // The user has completed the gesture un-successfully.
            // This handler will continue to get called with updated gestureAmount
            // But we don't need to change the underlying controller / view settings.
        }
        if (isComplete) {
            // Animation has completed and gestureAmount is either -1, 0, or 1.
            // This handler block will be released upon return from this iteration.
            // Note: we already updated our view to use the new (or same) content
            // above. So no need to do that here. Just...
            // Tear down animation overlay here
            self->_swipeAnimationCanceled = NULL;
        }
    }];
    // We keep a pointer to a BOOL __block variable so that we can cancel our
    // block handler at any time. Note: You must assign the BOOL pointer to your
    // ivar after block creation and copy!
    self->_swipeAnimationCanceled = &animationCancelled; }

其他解决方案是直接处理触摸事件以识别手势。有关更多信息,请考虑查看文档

于 2012-12-05T06:10:51.977 回答
0

我在 NSViewController 类中使用了touchesBeganWithEvent:和方法。touchesEndedWithEvent:下面的代码只是一个粗略的例子。出于某种原因,swipeWithEvent:永远不会被解雇。所以我放弃了这个(swipeWithEvent)。以下代码在 High Sierra 中使用 Xcode 10 进行了测试(在视图控制器的实现部分内):

- (void)viewDidAppear {
       [超级 viewDidAppear];

[self.view setAcceptsTouchEvents:YES]; } float beginX, endX; - (void)touchesBeganWithEvent:(NSEvent *)event { if(event.type == NSEventTypeGesture){ NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseAny inView:self.view]; if(touches.count == 2){ for (NSTouch *touch in touches) { beginX = touch.normalizedPosition.x; } // since there are two touches, beginX will always end up with the data from the second touch } } } - (void)touchesEndedWithEvent:(NSEvent *)event { if(event.type == NSEventTypeGesture){ NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseAny inView:self.view]; if(touches.count == 2){ for (NSTouch *touch in touches) { endX = touch.normalizedPosition.x; } // since there are two touches, endX will always end up with the data from the second touch if (endX > beginX) { NSLog(@"swipe right!"); } else if (endX < beginX) { NSLog(@"swipe left!"); } else { NSLog(@"no swipe!"); } } } }

[self.view setAcceptsTouchEvents:YES];声明非常重要。我把它放在 viewDidAppear 方法中,以便在所有视图出现后开始接受触摸信息。如果您在子类中使用它,只需更改self.view为即可self(还需要在初始化或的某处使用“setAcceptsTouchEvents” drawRect)。这只是检测左右滑动的示例。对于上下滑动,您需要使用touch.normalizedPosition.y数据。

如果您在 NSWindowController 子类中使用它,则需要self.window.contentView.acceptsTouchEvents=YES;在 windowDidLoad 中使用这一行。您可能还需要将您的 OSX 目标设置为 10.10 以使所有这些都可以正常工作,因为 setAcceptsTouchEvents 已经不推荐用于更高版本。

于 2020-04-14T06:29:47.263 回答