ionic-framework icon indicating copy to clipboard operation
ionic-framework copied to clipboard

feat: generic touch gestures (press, pinch, etc)

Open Domvel opened this issue 4 years ago • 33 comments

Feature Request

Ionic version: ionic v4 ionic/angular: 4.7.1 ionic-cli: 5.2.5

Describe the Feature Request Since Ionic 4 there are no more touch gestures like press (long press). For a mobile framework, this is a very important part. This have to be an official supported feature in such a UI. The developer of Ionic should not hack for this feature. Like this or this.

Describe Preferred Solution Ionic should provide this feature in an official way. No hacks. No polyfills. I can not say which solution would be the best. Maybe use Angular with Hammerjs. Or build an own touch lib.

Related Code Ionic 3:

<button ion-button (press)="sayHello()">Press me long.</button>
import { Gesture } from 'ionic-angular/gestures/gesture';
// ...
  this.pressGesture = new Gesture(this.nativeElement);
  this.pressGesture.listen();
  this.pressGesture.on('press', event => {/*...*/});
// ...

In Ionic 4 this is not possible anymore. 😢 Or did I miss something? Any questions? I'm glad if I can help.

Domvel avatar Aug 26 '19 08:08 Domvel

Thanks for the issue. Ionic 3 had HammerJS internally for these kinds of things. We found that HammerJS had a tendency to capture events and not let them propagate. This caused things as simple as scrolling to no longer work. As a result, we removed it in Ionic 4 and are not likely to add it back.

Users are welcome to install it on their own, but we do not provide any kind of official support for HammerJS at this time.

In terms of having our own solution, we exposed a createGesture function in Ionic 4.8.0. It is still being fully tested, so things might change until it's fully "released". I'd recommend giving that a try and seeing if that works for your use case. I am going to keep this issue open in case you are interested in testing/providing any feedback on the createGesture function.

Here is the source for createGesture: https://github.com/ionic-team/ionic/blob/master/core/src/utils/gesture/index.ts And here is an example of it being used: https://github.com/ionic-team/ionic/blob/cd75428785fc04caa65278438c55aac5b4265db8/core/src/components/menu/menu.tsx#L174-L184

Thanks!

liamdebeasi avatar Aug 26 '19 13:08 liamdebeasi

Thanks for the answer. I also don't like hammerjs. Ionic should have an own solution as a mobile framework. Your solution looks promising. But I'm not sure how to create a long press event like in Ionic v3 the (press) event for html-elements. In my case, currently I'm on the migration from v3 to v4. 😓 It's hard for our complex app. I found no documentation / breaking changes in the migration guide about the missing press events. It was a surprise for me. Anyway... I found a (temp) solution for me by a custom Angular directive longPress. Which detect a long press by setTimeout() (and clearTimeout to abort). I also have a pressing event, which uses setInterval() to tick every x time while the button is being pressed. ...

Are you sure what your suggestion can create a long-press event? Like the (press) event from Ionic v3? I'm not sure. Anyway, there should be several basic gestures in Ionic. (some events may already exist.)

  • tab
  • press (long press, after 500ms? maybe customizable)
  • pressing (after long press it should fired every x time. (time is a property, by default 500ms?))
  • release
  • swipe
  • pan
  • rotate
  • pinch

https://ionicframework.com/docs/v3/components/#gestures

Ok, e.g. rotate, pinch, etc. are more special gestures which should only accessible with a custom property / directive on an element. (triviality)

Back to the utils/gesture. I have no idea how to use it and I don't know if a long press is feasible. In my opinion it should be very easy to use.

  • For the html part: Just html events like
<ion-button (press)="onLongPress()" (pressing)="onHoldingEachXTime()"></ion-button>
  • For the programmatically way. If pressing as a cycling holding event does already exist in Ionic, I do not need to create my own Angular directive. But if the Ionic-Team say "no", I could create my directive like so:
import { Gesture } from `@ionic/angular`;
// Or from another source. But maybe this should be an Angular thing? idk.

@Directive({
  selector: '[longPress]'
})
export class LongPressDirective implements OnInit, OnDestroy {
  ionicGesture: Gesture;
  
  ngOnInit() {
    this.ionicGesture = new Gesture(this.nativeElement);
    this.ionicGesture.on('press', event => {
      // Enable the pressing-event cyclical event emitter.
      this.pressingTimerSubscribe();
    })
    // Currently I use native HTML event listener to detect `pointerup` to clear the timer.
    // ...
  }
}

I also uses pointercancel, pointerout, pointerleave to avoid latching behavior. It's really challenging to handle these events. In my case at the mobile device (Android 7), the cancel events etc. are sometimes triggered without any reason. And a few cases the are never triggered. It's weird. ... sigh It's not a trivial issue. Maybe it has to do with the Ionic v3 and Hammerjs. But anyway... I would be happy if the awesome Ionic-Team build an own gesture feature with blackjack and hoo... you know? --quote bender 😄

For now I'd built my own long press directive by setInterval. But it will not win a beauty contest. Sorry for too many words. ^^

Domvel avatar Aug 26 '19 15:08 Domvel

Thanks for the clarification. We definitely intend on adding more documentation and best practices for doing specialized gestures. This is something we are actively discussing, so we hope to have more to share soon!

liamdebeasi avatar Aug 29 '19 15:08 liamdebeasi

Any update on this?

Ovilia avatar Oct 15 '19 12:10 Ovilia

We still have plans for this. More to announce soon!

liamdebeasi avatar Oct 15 '19 12:10 liamdebeasi

Exist any way to prevent issues using Hammerjs in the meantime? 😅

jdnichollsc avatar Nov 02 '19 03:11 jdnichollsc

Not sure if the below follows best practices throughout, but for what it's worth, my best shot at implementing a LongPress gesture. It works very well I must say.

import { createGesture, Gesture, GestureDetail } from '@ionic/core';
import { EventEmitter, Directive, OnInit, OnDestroy, Output, Input, ElementRef } from '@angular/core';

@Directive({
  selector: '[appLongPress]'
})
export class LongPressDirective implements OnInit, OnDestroy {

  ionicGesture: Gesture;
  timerId: any;

  @Input() delay: number;
  @Output() longPressed: EventEmitter<any> = new EventEmitter();

  constructor(
    private elementRef: ElementRef
  ) {  }

  ngOnInit() {
    this.ionicGesture = createGesture({
      el: this.elementRef.nativeElement,
      gestureName: 'longpress',
      threshold: 0,
      canStart: () => true,
      onStart: (gestureEv: GestureDetail) => {
        gestureEv.event.preventDefault();
        this.timerId = setTimeout(() => {
          this.longPressed.emit(gestureEv.event);
        }, this.delay);
      },
      onEnd: () => {
        clearTimeout(this.timerId);
      }
    });
    this.ionicGesture.setDisabled(false);
  }

  ngOnDestroy() {
    this.ionicGesture.destroy();
  }
}

olivermuc avatar Nov 18 '19 16:11 olivermuc

@olivermuc it looks like you need to compare the distance when the setTimeout is executed:

this.pressGesture = createGesture({
  el: this.pressButton,
  gestureName: 'button-press',
  gesturePriority: 100,
  threshold: 0,
  direction: 'x',
  passive: true,
  onStart: (detail: GestureDetail) => {
    this.pressGesture['pressed'] = false;
    this.pressGesture['timerId'] = setTimeout(() => {
      if (Math.abs(detail.deltaX) < 10 && Math.abs(detail.deltaX) < 10)
      {
        this.onPress();
        this.pressGesture['pressed'] = true;
      }
    }, 251)
  },
  onEnd: () => {
    clearTimeout(this.pressGesture['timerId'])
    if (this.pressGesture['pressed']) {
      this.onPressUp();
    }
  }
});
this.pressGesture.setDisabled(false);

jdnichollsc avatar Nov 19 '19 08:11 jdnichollsc

Thanks for the feedback @jdnichollsc. Definitely an option to add, for me staying within the initial click area is not critical hence I didn't add it - but good addition in case it is needed.

olivermuc avatar Nov 19 '19 08:11 olivermuc

@olivercodes Could you please provide a sample of how this is configured and used? Thanks for the long press directive example. 👍

davidquon avatar Nov 19 '19 17:11 davidquon

I figured it out with some trial and error since I'm wasn't familiar with the way this directive communication worked. In case anyone else could use some help this is what was added to the HTML. Thanks @olivercodes for the help with the directive.

<ion-button (longPressed)="longPressFunction()" appLongPress delay=1000>

Also had to add this to the local .module.ts file in Ionic 4 as the app.module.ts didn't work for some reason.

import { LongPressDirective } from 'src/app/directives/long-press.directive';

and

declarations: [
    LongPressDirective,
  ]

davidquon avatar Nov 20 '19 16:11 davidquon

@davidquon who is @olivercodes?

jdnichollsc avatar Nov 20 '19 17:11 jdnichollsc

Whoops. Sorry @olivercodes I meant @olivermuc. 🤦‍♂ Thanks @jdnichollsc and @olivermuc. 👍

davidquon avatar Nov 20 '19 19:11 davidquon

@olivermuc I mean about this PR https://github.com/ionic-team/ionic/pull/19861 Having a maxThreshold option to allow a little movement on the x and y axis

jdnichollsc avatar Nov 20 '19 20:11 jdnichollsc

@olivermuc I mean about this PR #19861 Having a maxThreshold option to allow a little movement on the x and y axis

When I tested, no movement constraints showed. Events were fired directly, regardless of any additional vertical or horizontal motion, and the onMove would continuously fire - if needed.

olivermuc avatar Nov 22 '19 07:11 olivermuc

Just out of curiosity: The reason I ditched Hammer.JS and used above approach was a terrible conflict with Chrome's devtool device/touch emulator. Essentially, when assigning new 'Recognizers', it caused Chrome's scrolling to stop. Apparently due to the way it hogs touch events.

Unfortunately I'm seeing similar behaviour with the above approach - not always, and hard to reproduce, but it happens about 3 out of 10 long-presses.

Once that happens, you actually have to close the browser tab and reopen for scrolling to work again.

Anyone else seeing this too?

olivermuc avatar Nov 22 '19 17:11 olivermuc

@olivermuc Nice try with the custom gesture directive, I tested it and unfortunately found that:

  • ~~with preventDefault called there is no scrolling of the parenting ion-container when dragging the element with the directive (understandably), without it scrolling works but...~~ disregard this because it was an effect of trying to scroll immediately after dismissing an action sheet, weird ... however ...

  • it disables pull to refresh component on the parenting ion-content when pulling the element with the directive and also it disables ripple effect component on the element with the directive.

Tried messing with gesture priority without any luck. There has to be a way to make gestures play nice with each other. Can't wait for an updated documentation on gestures.

I'll try this approach to see how it behaves: https://github.com/Bengejd/Useful-ionic-solutions/blob/master/src/directives/long-press.directive.ts ... nope, doesn't use latest gesture system

lgovorko avatar Jan 20 '20 19:01 lgovorko

I settled down on this solution because it plays really nice with Gestures, and that's because it doesn't use Gestures:

import { EventEmitter, Directive, OnInit, Output, Input, ElementRef } from '@angular/core';
import { timer, Subscription } from 'rxjs';


@Directive({
  selector: '[appLongPress]'
})
export class LongPressDirective implements OnInit {

  timerSub: Subscription;

  @Input() delay: number;
  @Output() longPressed: EventEmitter<any> = new EventEmitter();

  constructor(
    private elementRef: ElementRef<HTMLElement>
  ) { }

  ngOnInit() {
    const isTouch = ('ontouchstart' in document.documentElement);
    const element = this.elementRef.nativeElement;
    element.onpointerdown = (ev) => {
      this.timerSub = timer(this.delay).subscribe(() => {
        this.longPressed.emit(ev);
      });
    };
    element.onpointerup = () => { this.unsub(); };
    element.onpointercancel = () => { this.unsub(); };
    if (isTouch) {
      element.onpointerleave = () => { this.unsub(); };
    }
  }

  private unsub() {
    if (this.timerSub && !this.timerSub.closed) { this.timerSub.unsubscribe(); }
  }

}

It tries not to do unnecessary onpointerleave event handler on non touch devices, ... I'm not sure if it's even necessary for the touch devices because onpointercancel seems to fire consistently to clear the timer.

Result: pull to refresh works now and also custom ripple effects on the elements with the directive :joy:

lgovorko avatar Jan 21 '20 09:01 lgovorko

Using configuration provided by josh morony in his youtube video https://www.youtube.com/watch?v=TdORJC-J1gg and using the event bindings from the medium article https://medium.com/madewithply/ionic-4-long-press-gestures-96cf1e44098b, I was able to get what I needed done, including simulating an "on-hold" event

Apro123 avatar May 17 '20 05:05 Apro123

if it is possible for you to upgrade to angular 9 you can use HammerModule in your whole application by simply binding to elements. HammerModule: https://next.angular.io/api/platform-browser/HammerModule

your module.ts

import { BrowserModule ,  HammerModule} from '@angular/platform-browser';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HammerModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})

and in any part of your application you can simply bind with all of hammerjs methods

<ion-item (press)="editMessage()">
<ion-label>message</ion-label>
</ion-item>

EmreAkkoc avatar Jun 03 '20 09:06 EmreAkkoc

Any Update on this Issue?

Saqib92 avatar Jun 03 '20 09:06 Saqib92

How do I test this or import module?

https://codepen.io/liamdebeasi/pen/KKdodjd

import { createGesture } from 'https://cdn.jsdelivr.net/npm/@ionic/core/dist/esm/index.mjs';

Says: Cannot find module 'https://cdn.jsdelivr.net/npm/@ionic/core/dist/esm/index.mjs'.ts(2307)

MInesGomes avatar Jun 10 '20 12:06 MInesGomes

if it is possible for you to upgrade to angular 9 you can use HammerModule in your whole application by simply binding to elements.

does Hammer now support stopPropagation() ? If you check the second comment, @liamdebeasi mentions Hammer's lack of it "We found that HammerJS had a tendency to capture events and not let them propagate" and it is totally annoying.

yogibimbi avatar Jul 08 '20 23:07 yogibimbi

@liamdebeasi Hello, are there any changes about Gesture. I'm looking for swipe gesture for y and x direction. Has ionic created a simple swipe gesture? or Do I need to create my own.

ludonoel1 avatar Jul 09 '20 20:07 ludonoel1

@ludonoel1 I have already marked your issue as a feature request for the x and y swipe direction: https://github.com/ionic-team/ionic-framework/issues/21704. It is currently in our backlog.

liamdebeasi avatar Jul 09 '20 20:07 liamdebeasi

Press and pressup works for me when I downgrade angular versions. My ionic info:

Ionic:

Ionic CLI : 6.11.8 (/usr/local/lib/node_modules/@ionic/cli) Ionic Framework : @ionic/angular 5.0.4 @angular-devkit/build-angular : 0.803.25 @angular-devkit/schematics : 8.3.25 @angular/cli : 8.3.25 @ionic/angular-toolkit : 2.2.0

Cordova:

Cordova CLI : 10.0.0 Cordova Platforms : 6.0.0, browser Cordova Plugins : cordova-plugin-ionic-keyboard 2.2.0, cordova-plugin-ionic-webview 4.1.3, (and 8 other plugins)

Utility:

cordova-res : not installed native-run (update available: 1.1.0) : 1.0.0

System:

NodeJS : v12.18.3 (/usr/local/bin/node) npm : 6.14.8 OS : macOS Catalina

Apro123 avatar Sep 18 '20 18:09 Apro123

How to use the button events e.g. (tap) and (press) in Ionic 5? Removed? No replacement? I don't want to create a custom directive now. Without the obsolete hammer-js. Is something what a UI framework should handle.

infacto avatar Nov 12 '20 11:11 infacto

@infacto We have documentation on gestures that you can use for now: https://ionicframework.com/docs/utilities/gestures. We plan to add some built-in gestures in a future update.

liamdebeasi avatar Nov 12 '20 14:11 liamdebeasi

@liamdebeasi Thanks for the info. At this moment I crafting a directive with the Ionic GesturesController to implement the events from Ionic 3 and more e.g. period long-press:

  • press Emitted once on long press. ("tap" will not be fired after this.)
  • tap For short press. Only emitted when "press" is not fired.
  • hold (pressing) Periodically fired by an input "interval" (100...1000ms)

Please consider these events. And care about cancel events. On Ionic 3 I detected some quirks about unintentional interruptions or missing release events. Especially dangerous for the periodic event. The hold event is nice to have, the others required. Why hold event? Use case: You press and hold a button to change a number until you release it. Or send values over the web or bluetooth. - Other ideas are welcome.

Additional event ideas:

  • pressStart Immediately fired when button is touched.
  • pressEnd Fired on button release. Not fired if "longPressEnd" is triggered. It's like Ionic 3 "tap".
  • longPressStart Fired once on long press. Like Ionic 3 "press".
  • longPressHold Like "press" (longPressStart) but periodically. (equal "hold").
  • longPressEnd Fired if long press is triggered.

(In this context "press" is short. And a long press is explicitly named.) Ok, this is maybe a bit too special. I just think aloud. To trigger ideas. 🙂 I believe in the Ionic team. You are doing a great job. I'm sure you have more / better ideas about it. Oh, this proposal here didn't handle the other gestures like pan, move, etc. At this moment it's only about short and long press.

Update: The ripple-effect does not work anymore if the gestures directive is active. sigh

infacto avatar Nov 12 '20 14:11 infacto

Could anyone suggest please .. (press) event dose not work on iOS platform.

<ion-item (press)="tapEvent($event,)"></ion-item>

Installed platforms ios 6.1.1,

Tested on iOS version ipad 12.5.1 and iphone 14.4

ionic info

Ionic:

   Ionic CLI                     : 5.4.16 (/usr/local/lib/node_modules/ionic)
   Ionic Framework               : @ionic/angular 5.5.2
   @angular-devkit/build-angular : 0.1000.8
   @angular-devkit/schematics    : 10.0.8
   @angular/cli                  : 10.0.8
   @ionic/angular-toolkit        : 2.3.3

Cordova:

   Cordova CLI       : 10.0.0
   Cordova Platforms : none
   Cordova Plugins   : no whitelisted plugins (0 plugins total)

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   ios-deploy : 1.11.3
   ios-sim    : 8.0.2
   NodeJS     : v14.15.4 (/usr/local/bin/node)
   npm        : 6.14.10
   OS         : macOS Big Sur
   Xcode      : Xcode 12.4 Build version 12D4e

klochko7 avatar Feb 03 '21 15:02 klochko7

if it is possible for you to upgrade to angular 9 you can use HammerModule in your whole application by simply binding to elements. HammerModule: https://next.angular.io/api/platform-browser/HammerModule

your module.ts

import { BrowserModule ,  HammerModule} from '@angular/platform-browser';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HammerModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})

and in any part of your application you can simply bind with all of hammerjs methods

<ion-item (press)="editMessage()">
<ion-label>message</ion-label>
</ion-item>

helped.

klochko7 avatar Feb 03 '21 15:02 klochko7

Just out of curiosity: The reason I ditched Hammer.JS and used above approach was a terrible conflict with Chrome's devtool device/touch emulator. Essentially, when assigning new 'Recognizers', it caused Chrome's scrolling to stop. Apparently due to the way it hogs touch events.

Unfortunately I'm seeing similar behaviour with the above approach - not always, and hard to reproduce, but it happens about 3 out of 10 long-presses.

Once that happens, you actually have to close the browser tab and reopen for scrolling to work again.

Anyone else seeing this too?

Yes I see it all the time. Anyone know why this is happening and how to avoid it? It may be that the page changes during a gesture and Chrome devtools phone emulator can't handle it.

It's only the phone emulator; turn that off and scrolling comes back. Turn it back on and the problem is fixed, so no need to close the tab. I.e. workaround: double click the phone emulator icon in the upper left of devtools. Although sometimes you do need to close the tab...hmmm.

...Better workaround: If you get stuck in the phone emulator, turn the phone emulator off, do a longpress, and then turn it back on. What I found was the longpress brings up the context menu in the phone emulator but doesn't when the emulator is off. So it's probably that context menu that is holding everything up. This workaround clears that it seems. If so, the solution would be disabling or avoiding the longpress context menu in the phone emu.

OK I have a working fix: Set window.oncontextmenu = function () { return false; }; before any long press while in devtools Device Mode and make it return true a second after the longpress is done (using setTimeout or however you prefer). There are a variety of hacks to see if devtools is open such as @sindresorhus/devtools-detect . It's actually screwing up the scrolling any time that context menu is showing up, not even on a specific longpress element, so you may want to leave it off for your entire devtools session (but turn it back on for web users if they need it).

spicemix avatar Jul 26 '21 04:07 spicemix

I settled down on this solution because it plays really nice with Gestures, and that's because it doesn't use Gestures:

import { EventEmitter, Directive, OnInit, Output, Input, ElementRef } from '@angular/core';
import { timer, Subscription } from 'rxjs';


@Directive({
  selector: '[appLongPress]'
})
export class LongPressDirective implements OnInit {

  timerSub: Subscription;

  @Input() delay: number;
  @Output() longPressed: EventEmitter<any> = new EventEmitter();

  constructor(
    private elementRef: ElementRef<HTMLElement>
  ) { }

  ngOnInit() {
    const isTouch = ('ontouchstart' in document.documentElement);
    const element = this.elementRef.nativeElement;
    element.onpointerdown = (ev) => {
      this.timerSub = timer(this.delay).subscribe(() => {
        this.longPressed.emit(ev);
      });
    };
    element.onpointerup = () => { this.unsub(); };
    element.onpointercancel = () => { this.unsub(); };
    if (isTouch) {
      element.onpointerleave = () => { this.unsub(); };
    }
  }

  private unsub() {
    if (this.timerSub && !this.timerSub.closed) { this.timerSub.unsubscribe(); }
  }

}

It tries not to do unnecessary onpointerleave event handler on non touch devices, ... I'm not sure if it's even necessary for the touch devices because onpointercancel seems to fire consistently to clear the timer.

Result: pull to refresh works now and also custom ripple effects on the elements with the directive 😂

I was already going to use hammerjs again, but it works like a charm. Thanks mate

nosTa1337 avatar Jul 08 '22 07:07 nosTa1337