Reputation: 2796
… is to detect panning (or swiping) using Hammer.js in Angular 9.
It should should work like this:
In the image green
shows everything that is default by the browser and should not be prevented. blue
shows everything that should be prevented and be handled by Hammer.
Not sure what I mean? Take a look at Swiper
's demos. They work exactly like this.
1 It's clear how to detect the angle using event.angle
. But I'm not sure how to distinguish between preventing or not preventing the event if necessary.
… are all these things which I found on multipee questions on Stackoverflow and other blog articles:
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 { }
All these things didn't work as they had different results and were not consistent through different devices (e.g. iPhone vs iPad):
… creates Hammer
directly in the component, like this:
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)
});
}
}
This does a lot of things:
But these things don't work or are not good:
Hammer
-instance this.hammertime
and attached events are never destroyedpan
-event from firing multiple timesthis.hammertime
when the goal is not possible "the Angular way"?Upvotes: 3
Views: 2163
Reputation: 1800
I took the approach of enabling Hammer's pinch
recognizer "on-demand". That is, detect when the user has two touches on a content container div
and then enable the pinch
recognizer. The pinch event handler set up through hammer.on('pinchstart pinchend', listener)
responds right after the pinch
recognizer is enabled through a touchstart
event listener. The pinch
recognizer is switched off in a touchend
event listener to ensure it doesn't interfere with a vertical scroll or horizontal pan.
Initially, I used the 'pointerdown' and 'pointerup' listeners. However, 'pointerup' did not fire consistently.
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");
}
}
I added the preceding vanilla JavaScript because it addresses 90% of the question's text: "How to keep vertical scrolling and zooming and detect horizontal and diagonal panning/swiping with Hammer.js"; and @lampshade added an alternative approach as well.
Also note that the vanilla JavaScript approach above is different than the solution @David references in his comment to @lampshade's question.
I upvoted the question because it's detailed oriented, it provides a clear picture of what's needed and what was attempted. It made it easier for me to iterate on a solution.
Upvotes: 0
Reputation: 2796
… using ZingTouch as proposed by Graham Ritchie.
It does all these things:
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)
});
}
}
Upvotes: 1