如何正确使用 swipeWithEvent 导航 webView,Obj-C

Posted

技术标签:

【中文标题】如何正确使用 swipeWithEvent 导航 webView,Obj-C【英文标题】:How to properly use swipeWithEvent to navigate a webView, Obj-C 【发布时间】:2012-10-05 14:08:01 【问题描述】:

我想通过使用事件 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];
    



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

【问题讨论】:

您能否上传一个未调用-swipeWithEvent: 的示例应用程序? 这里。 dropbox.com/s/sukkzvefwi836kr/swipeProject.zip?dl=1 【参考方案1】:

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

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

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

如果只需要支持10.8+

在历史堆栈模式下使用NSPageController

如果需要支持 10.7

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

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

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

    深呼吸;这个太棒了。

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

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

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

【讨论】:

除了,我的应用支持 Snow Leopard+。所以,NSPageController 不能用。 @AceLegend:布伦特的大部分答案是如何在没有 NSPageController 的情况下做到这一点(如果您使用的是 NSPageController,您将在第 1 步停止)。不过,当 10.7 及更高版本的 API 不可用时,您将有更多工作要做。 我明白了。也许我只能为 OS X 10.8 实现 NSPageController。我只是不喜欢使用特定于操作系统的代码。我发现它会导致问题。 好的。我已经决定我要使用 NSPageController。我确实通过示例学习,如果您可以演示如何使用 NSPageController,我将不胜感激。您可以使用以下链接作为我的应用程序设置方式的快速入门基础。 sites.google.com/site/infiniteopensyntax/basic-web-browser @Brent Royal-Gordon,嗯,这就是我打算做的。但是,我仍然需要找到一种方法让它也能正常工作。因此,我问了另一个问题。 ***.com/questions/13747190/…【参考方案2】:

来自the 10.7 release notes:

流体滑动跟踪 - API

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

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

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

2) 动量滚动 - 它们的阶段为 NSEventPhase none,但它们的动量阶段为 NSEventPhaseBegan/NSEventPhaseChanged/NSEventPhaseEnded。

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; 

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

【讨论】:

我读错了吗,但听起来你的例子需要你有一个 NSScrollView。我没有。此外,对于我需要的东西来说,这看起来有点复杂。我确实想跟踪滑动,但实际上我只需要知道用户何时“完成”任一方向的滑动。然后我只想执行一个方法。就是这样。 @AceLegend 你误读了,它不需要 NSScrollView。它记录了你有 NSScrollView 做什么。 好的,我很高兴。但是,伙计,我很惊讶这有多复杂。我正在为 OS X 10.9 SDK 的手势识别器交叉手指。 :) 其实,没有。这看起来很有希望。 好的,我看了这些。但我无法让它工作。它从不调用 swipeWithEvent:,与 beginGestureWithEvent 相同。我应该设置一些 NSResponder 委托还是什么?【参考方案3】:

我在 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 即可(还需要在初始化或drawRect 某处的“setAcceptsTouchEvents”)。这只是检测左右滑动的示例。上下滑动需要使用touch.normalizedPosition.y数据。

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

【讨论】:

以上是关于如何正确使用 swipeWithEvent 导航 webView,Obj-C的主要内容,如果未能解决你的问题,请参考以下文章

如何以正确的方式使用导航控制器实现标签栏控制器

我如何正确对齐我的导航栏项目(ul)

如何使用情节提要正确地转到嵌入式选项卡栏和导航控制器?

如何正确处理 Angular 中 Scroll 上的导航栏颜色变化?

如何正确构建突出当前页面的导航菜单

wordpress中的自定义导航栏。如何正确设置样式?