ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
51.13k stars 13.5k forks source link

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

Open Domvel opened 5 years ago

Domvel commented 5 years ago

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.

liamdebeasi commented 5 years ago

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!

Domvel commented 5 years ago

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.)

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.

<ion-button (press)="onLongPress()" (pressing)="onHoldingEachXTime()"></ion-button>
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. ^^

liamdebeasi commented 5 years ago

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!

Ovilia commented 5 years ago

Any update on this?

liamdebeasi commented 5 years ago

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

jdnichollsc commented 5 years ago

Exist any way to prevent issues using Hammerjs in the meantime? πŸ˜…

olivermuc commented 5 years ago

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();
  }
}
jdnichollsc commented 5 years ago

@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);
olivermuc commented 5 years ago

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.

davidquon commented 5 years ago

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

davidquon commented 5 years ago

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,
  ]
jdnichollsc commented 5 years ago

@davidquon who is @olivercodes?

davidquon commented 5 years ago

Whoops. Sorry @olivercodes I meant @olivermuc. πŸ€¦β€β™‚ Thanks @jdnichollsc and @olivermuc. πŸ‘

jdnichollsc commented 5 years ago

@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

olivermuc commented 5 years ago

@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 commented 5 years ago

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?

lgovorko commented 4 years ago

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

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 commented 4 years ago

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:

Apro123 commented 4 years ago

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

EmreAkkoc commented 4 years ago

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>
Saqib92 commented 4 years ago

Any Update on this Issue?

MInesGomes commented 4 years ago

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)

yogibimbi commented 4 years ago

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.

ludonoel1 commented 4 years ago

@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.

liamdebeasi commented 4 years ago

@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.

Apro123 commented 4 years ago

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

infacto commented 4 years ago

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.

liamdebeasi commented 4 years ago

@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.

infacto commented 4 years ago

@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:

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:

(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

klochko7 commented 3 years ago

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 commented 3 years ago

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.

spicemix commented 3 years ago

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).

nosTa1337 commented 2 years ago

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

JulienLecoq commented 2 weeks ago

Any news on those builtin gestures?

JulienLecoq commented 2 weeks ago

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 πŸ˜‚

It works well except that it still triggers a click event after the long press event which causes conflicts when you need to handle both events on the same element.