如何在Angular 9中使用Hammer.js实现垂直滚动和缩放,并检测水平和对角线手势?

3

我想要的

...是在Angular 9中使用Hammer.js检测平移(或滑动)。

它应该像这样工作:

  • 应检测水平平移
  • 应检测特定角度的对角线平移1
  • 不应防止垂直滚动
  • 不应防止捏合缩放,因为这会导致可访问性问题

enter image description here

在图片中, 绿色 表示默认的浏览器行为,不应被阻止。 蓝色 表示应该被防止并由Hammer处理的行为。

不确定我的意思?看看 Swiper 的演示。 它们的操作方式与此完全相同

1 可以使用 event.angle 来检测角度。但我不确定如何区分是否必要时防止事件。

我尝试过的

...是我在Stackoverflow和其他博客文章中找到的多个问题上尝试的所有这些方法:

import { BrowserModule, HammerModule, HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import * as Hammer from 'hammerjs';

class MyHammerConfig extends HammerGestureConfig {
  // Test #1
  overrides = <any>{
    swipe: { direction: hammer.DIRECTION_HORIZONTAL },
    pinch: { enable: false },
    rotate: { enable: false }
  };

  // Test #2
  overrides = <any>{
    swipe: { direction: Hammer.DIRECTION_ALL  }
  };

  // Test #3
  options = {
    domEvents: true
  }

  // Test #4
  buildHammer(element: HTMLElement) {
    const mc = new Hammer(element, {
      touchAction: 'pan-y'
    });

    return mc;
  }

  // Test #5
  buildHammer(element: HTMLElement) {
    const mc = new Hammer(element, {
        touchAction: 'auto'
     });

     return mc;
  }
}

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HammerModule
  ],
  providers: [
    {
      provide: Window,
      useValue: window
    },
    {
      provide: HAMMER_GESTURE_CONFIG,
      useClass: MyHammerConfig
    }
  ],
  bootstrap: [
    AppComponent
  ]
})

export class AppModule { }

所有这些方法都没有效果,因为它们的结果不同,并且在不同设备(例如iPhone和iPad)上不一致:

  • ✗ 捏合缩放无法使用
  • ✗ 垂直滚动无法使用
  • ✗ 无法检测到对角线移动

当前解决方案

... 直接在组件中创建Hammer

import { Component, OnInit, ViewChild, ElementRef, OnDestroy, ViewChildren, QueryList } from '@angular/core';

import * as Hammer from 'hammerjs';

@Component({
  selector: 'app-hero',
  templateUrl: './hero.component.html',
  styleUrls: ['./hero.component.scss']
})

export class HeroComponent implements OnInit, OnDestroy {
  @ViewChild('list', { static: true }) list: ElementRef;

  private hammertime: Hammer;
  private isLocked: boolean = false;

  ngOnInit() {
    this.initHammer();
  }

  ngOnDestroy() {
    // todo destroy hammertime
  }

  initHammer(): void {
    this.hammertime = new Hammer(this.list.nativeElement, {touchAction : 'auto'});
    this.hammertime.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL, threshold: 60 });
    this.hammertime.on('panleft panright', event => {
      if (this.isLocked || event.maxPointers !== 1) {
        return
      };

      this.goto(event.type === 'panLeft' ? 'next' : 'prev');
      this.isLocked = true;

      setTimeout(() => this.isLocked = false, 1000)
    });
  }
}

这个功能有以下优点:

  • ✓ 可以缩放
  • ✓ 可以上下滚动
  • ✓ 可以检测到水平滑动

但以下问题存在或者不够好:

  • ✗ 没有使用 HammerModule,不符合 Angular 的规范
  • ✗ 无法检测对角线滑动
  • Hammer 实例 this.hammertime 和绑定的事件没有被正确销毁
  • ✗ 为了避免 pan 事件多次触发使用了超时器
  • ✗ 必须手动检测指针数量

问题?

  • 如何使用 "Angular 规范" 来实现这个功能?
  • 如何检测对角线滑动?
  • 如果不能按照 "Angular 规范" 实现,如何正确销毁 this.hammertime

1
另外,https://zingchart.github.io/zingtouch/ 实际上是一个非常棒的库,我只需要修复几个小错误就可以使用它了,因为它还包括双指旋转功能。 - GrahamTheDevRel
1
您需要选择某些值之间的角度,例如在22.5度和77.5度之间作为对角线 - 就像一个8块饼图一样,只有当角度在对角线部分时才希望它被注册为对角线。因此,您将具有-22.5度(等效)到22.5度作为垂直线,22.5到77.5度作为向上右对角线,77.5到112.5度作为右侧等。使用zingtouch库可以给出角度,因此使用它可能更容易(加上它是7kb gzipped,而hammer大约是60kb gzipped,所以这里有额外的奖励点,即使您需要更多自定义代码)。 - GrahamTheDevRel
1
在 ZingTouch 中,您需要使用 'directionFromOrigin'。 - GrahamTheDevRel
2
@GrahamRitchie 非常感谢您的帮助和指引,让我朝着正确的方向前进。我找到了一个完美解决方案,并将其作为答案添加以帮助未来的访问者。 - lampshade
对于hammerJS,我找到了这种做法 https://dev59.com/Srnoa4cB1Zd3GeqPNEPF#60281913 - David
显示剩余2条评论
2个回答

1

一种替代方案

...使用ZingTouch,由Graham Ritchie提出。

它可以做到以下几点:

  • ✓ 检测水平和对角线移动
  • ✓ 不会阻止垂直滚动
  • ✓ 不会阻止捏合缩放

新的解决方案

import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';

import ZingTouch from 'zingtouch';

@Component({
  selector: 'app-hero',
  templateUrl: './hero.component.html',
  styleUrls: ['./hero.component.scss']
})

export class HeroComponent implements OnInit, OnDestroy {
  @ViewChild('list', { static: true }) list: ElementRef;

  private zingTouchRegion: ZingTouch.Region;
  private isLocked: boolean = false;

  ngOnInit() {
    this.initZingTouch();
  }

  ngOnDestroy() {
    this.zingTouchRegion.unbind(this.list.nativeElement);
  }

  initZingTouch(): void {
    const area = this.list.nativeElement;
    const gesture = new ZingTouch.Pan({threshold: 10});

    this.zingTouchRegion = new ZingTouch.Region(area, false, false);
    this.zingTouchRegion.bind(area, gesture, event => {
      const angle = event.detail.data[0].directionFromOrigin;

      if ((angle >= 40 && angle <= 140) || (angle >= 220 && angle <= 320)) {
        return;
      }

      event.detail.events[0].originalEvent.preventDefault();

      if (this.isLocked) {
        return
      };

      if (angle > 140 && angle < 220) {
        this.goto('next');
      }

      if (angle > 320 || angle < 40) {
        this.goto('prev');
      }

      this.isLocked = true;
      setTimeout(() => this.isLocked = false, 600)
    });
  }
}

0

我采用了启用 Hammer 的 pinch 识别器的“按需”方法。也就是说,检测用户在内容容器 div 上有两个触摸点时,然后启用 pinch 识别器。通过 hammer.on('pinchstart pinchend', listener) 设置的捏事件处理程序会在通过 touchstart 事件侦听器启用 pinch 识别器后立即响应。为确保它不干扰垂直滚动或水平平移,在 touchend 事件侦听器中关闭 pinch 识别器。

最初,我使用了“pointerdown”和“pointerup”侦听器。但是,“pointerup”并不一致地触发。

    var containerEl = document.getElementById("content-container");

    // Use the 'touchstart' listener to enable Hammer's 'pinch' recognizer.
    // 'touchstart' and 'touchend' are supported on Safari on iOS, Chrome, Firefox
    // caniuse.com/?search=touches
    // developer.mozilla.org/en-US/docs/Web/API/TouchEvent/touches
    containerEl.addEventListener('touchstart', touchstartListener);
    containerEl.addEventListener('touchend', touchendListener);

    // Create a 'Hammer' instance.
    // By default the 'Hammerjs' library adds horizontal recognizers only.
    // Hammer sets the 'domEvents' option to 'false' by default.
    // It's set in the following code for clarity that Hammer will
    // not fire DOM events on the 'containerEl' element. That is,
    // there's no event delegation. (hammerjs.github.io/api/#hammer-defaults)
    var hammer = new Hammer(containerEl, { domEvents: false });

    // hammerjs.github.io/recognizer-pan
    // From the 'Notes' heading:
    // "When calling Hammer() to create a simple instance,
    // the pan and swipe recognizers are configured to only detect
    // horizontal gestures."
    // Therefore, setting the 'direction' option to
    // 'Hammer.DIRECTION_HORIZONTAL' is unnecessary. It's
    // set here to make it clear while reading this code that
    // the pan gesture is captured in the horizontal direction only.
    hammer.get('pan').set({ direction: Hammer.DIRECTION_HORIZONTAL });

    var events = "panleft panright panend pinchstart pinchend";
    hammer.on(events, hammerListener);

    var gestureMessage = "";

    // TO DO
    // Add a more robust filter for a one-time detection of the 'panleft' or 'panright'
    // event type when the user first touches the screen to begin
    // a vertical scroll. Evaluating the 'e.angle' value in the
    // 'hammerListener()' function is an alternative option for
    // filtering out the first touch of a vertical scroll as
    // @lampshade adds in the ZingTouch alternative approach answer.
    var firstTouchCounter = 0;

    function hammerListener(e) {
        switch (e.type) {
            case 'panleft':
                if (firstTouchCounter < 2) { firstTouchCounter++; return; }
                gestureMessage = "Pan left";
                break;
            case 'panright':
                gestureMessage = "Pan right";
                break;
            case 'panend':
                firstTouchCounter = 0; // Reset counter
                gestureMessage = "Pan end";
                break;
            case 'pinchstart':
                gestureMessage = "Pinch start";
                break;
            case 'pinchend':
                gestureMessage = "Pinch end";
                break;
        }
        console.log(gestureMessage);
    }

    var pinchEnabled = false;

    function touchstartListener(e) {
        if (e.touches.length === 2) {
            hammer.get('pinch').set({ enable: true });
            pinchEnabled = true;
            console.log("pinch ON");
        }
    }

    function touchendListener(e) {
        if (pinchEnabled && e.touches.length === 0) {
            hammer.get('pinch').set({ enable: false });
            pinchEnabled = false;
            console.log("pinch OFF");
        }
    }

附注

我添加了前置的原生JavaScript,因为它解决了问题文本的90%:“如何保持垂直滚动和缩放,并检测Hammer.js中的水平和对角线pan / swipe”; @lampshade也提供了另一种方法。

还要注意,上面的原生JavaScript方法与@David在他对@lampshade的问题的评论中引用的solution不同。

我点赞这个问题,因为它非常详细,清晰地说明了需要什么和尝试了什么。这使我更容易迭代出一个解决方案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接