fix(toggle): uses PanGesture abstraction

fixes #9428
This commit is contained in:
Manu Mtz.-Almeida
2016-12-01 16:15:19 +01:00
parent 0b642e7e96
commit 6ef6f0aeea
7 changed files with 192 additions and 102 deletions

View File

@ -22,6 +22,16 @@
</ion-item-options> </ion-item-options>
</ion-item-sliding> </ion-item-sliding>
<ion-item>
<ion-label>Apple</ion-label>
<ion-toggle item-left></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Apple</ion-label>
<ion-toggle></ion-toggle>
</ion-item>
<button ion-item menuClose="left" class="e2eCloseLeftMenu" detail-none> <button ion-item menuClose="left" class="e2eCloseLeftMenu" detail-none>
Close Menu Close Menu
</button> </button>
@ -94,6 +104,16 @@
<ion-list> <ion-list>
<ion-item>
<ion-label>Apple</ion-label>
<ion-toggle item-left></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Apple</ion-label>
<ion-toggle></ion-toggle>
</ion-item>
<button ion-item *ngFor="let p of pages" (click)="openPage(p)"> <button ion-item *ngFor="let p of pages" (click)="openPage(p)">
{{p.title}} {{p.title}}
</button> </button>

View File

@ -74,6 +74,16 @@
</ion-item-options> </ion-item-options>
</ion-item-sliding> </ion-item-sliding>
<ion-item>
<ion-label>Apple</ion-label>
<ion-toggle item-left></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Apple</ion-label>
<ion-toggle></ion-toggle>
</ion-item>
<button ion-item (click)="goToPage1()">Push same page</button> <button ion-item (click)="goToPage1()">Push same page</button>
<ion-item> <ion-item>

View File

@ -0,0 +1,45 @@
import { GestureController, GesturePriority, GESTURE_TOGGLE } from '../../gestures/gesture-controller';
import { PanGesture } from '../../gestures/drag-gesture';
import { pointerCoord } from '../../util/dom';
import { NativeRafDebouncer } from '../../util/debouncer';
import { Toggle } from './toggle';
/**
* @private
*/
export class ToggleGesture extends PanGesture {
constructor(public toogle: Toggle, gestureCtrl: GestureController) {
super(toogle.getNativeElement(), {
maxAngle: 20,
threshold: 0,
debouncer: new NativeRafDebouncer(),
gesture: gestureCtrl.createGesture({
name: GESTURE_TOGGLE,
priority: GesturePriority.Toggle,
})
});
}
canStart(ev: any): boolean {
return true;
}
onDragStart(ev: any) {
ev.preventDefault();
this.toogle._onDragStart(pointerCoord(ev).x);
}
onDragMove(ev: any) {
ev.preventDefault();
this.toogle._onDragMove(pointerCoord(ev).x);
}
onDragEnd(ev: any) {
ev.preventDefault();
this.toogle._onDragEnd(pointerCoord(ev).x);
}
}

View File

@ -3,13 +3,13 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Form, IonicTapInput } from '../../util/form'; import { Form, IonicTapInput } from '../../util/form';
import { isTrueProperty } from '../../util/util'; import { isTrueProperty, assert } from '../../util/util';
import { Ion } from '../ion'; import { Ion } from '../ion';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { pointerCoord } from '../../util/dom';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { Haptic } from '../../util/haptic'; import { Haptic } from '../../util/haptic';
import { UIEventManager } from '../../util/ui-event-manager'; import { ToggleGesture } from './toggle-gesture';
import { GestureController } from '../../gestures/gesture-controller';
export const TOGGLE_VALUE_ACCESSOR: any = { export const TOGGLE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -76,28 +76,18 @@ export const TOGGLE_VALUE_ACCESSOR: any = {
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Toggle extends Ion implements IonicTapInput, AfterContentInit, ControlValueAccessor, OnDestroy { export class Toggle extends Ion implements IonicTapInput, AfterContentInit, ControlValueAccessor, OnDestroy {
/** @private */
_checked: boolean = false;
/** @private */
_init: boolean = false;
/** @private */
_disabled: boolean = false;
/** @private */
_labelId: string;
/** @private */
_activated: boolean = false;
/** @private */
_startX: number;
/** @private */
_msPrv: number = 0;
/** @private */
_fn: Function = null;
/** @private */
_events: UIEventManager = new UIEventManager();
/** _checked: boolean = false;
* @private _init: boolean = false;
*/ _disabled: boolean = false;
_labelId: string;
_activated: boolean = false;
_startX: number;
_msPrv: number = 0;
_fn: Function = null;
_gesture: ToggleGesture;
/** @private */
id: string; id: string;
/** /**
@ -126,8 +116,9 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
config: Config, config: Config,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
public _haptic: Haptic, private _haptic: Haptic,
@Optional() public _item: Item @Optional() public _item: Item,
private _gestureCtrl: GestureController
) { ) {
super(config, elementRef, renderer, 'toggle'); super(config, elementRef, renderer, 'toggle');
_form.register(this); _form.register(this);
@ -142,59 +133,63 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
/** /**
* @private * @private
*/ */
pointerDown(ev: UIEvent): boolean { ngAfterContentInit() {
this._startX = pointerCoord(ev).x; this._init = true;
this._activated = true; this._gesture = new ToggleGesture(this, this._gestureCtrl);
return true; this._gesture.listen();
} }
/** /**
* @private * @private
*/ */
pointerMove(ev: UIEvent) { _onDragStart(startX: number) {
if (this._startX) { this._startX = startX;
let currentX = pointerCoord(ev).x; this._activated = true;
console.debug('toggle, pointerMove', ev.type, currentX); }
if (this._checked) { /**
if (currentX + 15 < this._startX) { * @private
this.onChange(false); */
this._haptic.selection(); _onDragMove(currentX: number) {
this._startX = currentX; assert(this._startX, '_startX must be valid');
this._activated = true;
}
} else if (currentX - 15 > this._startX) { console.debug('toggle, pointerMove', currentX);
this.onChange(true);
// Create a haptic event if (this._checked) {
if (currentX + 15 < this._startX) {
this.onChange(false);
this._haptic.selection(); this._haptic.selection();
this._startX = currentX; this._startX = currentX;
this._activated = (currentX < this._startX + 5); this._activated = true;
} }
} else if (currentX - 15 > this._startX) {
this.onChange(true);
this._haptic.selection();
this._startX = currentX;
this._activated = (currentX < this._startX + 5);
} }
} }
/** /**
* @private * @private
*/ */
pointerUp(ev: UIEvent) { _onDragEnd(endX: number) {
if (this._startX) { assert(this._startX, '_startX must be valid');
let endX = pointerCoord(ev).x;
if (this.checked) { if (this.checked) {
if (this._startX + 4 > endX) { if (this._startX + 4 > endX) {
this.onChange(false); this.onChange(false);
this._haptic.selection();
}
} else if (this._startX - 4 < endX) {
this.onChange(true);
this._haptic.selection(); this._haptic.selection();
} }
this._activated = false; } else if (this._startX - 4 < endX) {
this._startX = null; this.onChange(true);
this._haptic.selection();
} }
this._activated = false;
this._startX = null;
} }
/** /**
@ -273,19 +268,6 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
*/ */
onTouched() {} onTouched() {}
/**
* @private
*/
ngAfterContentInit() {
this._init = true;
this._events.pointerEvents({
elementRef: this._elementRef,
pointerDown: this.pointerDown.bind(this),
pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerUp.bind(this)
});
}
/** /**
* @private * @private
*/ */
@ -310,7 +292,7 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
*/ */
ngOnDestroy() { ngOnDestroy() {
this._form.deregister(this); this._form.deregister(this);
this._events.unlistenAll(); this._gesture.destroy();
this._fn = null; this._fn = null;
} }

View File

@ -23,6 +23,7 @@ export interface PanGestureConfig {
* @private * @private
*/ */
export class PanGesture { export class PanGesture {
private debouncer: Debouncer; private debouncer: Debouncer;
private events: UIEventManager = new UIEventManager(false); private events: UIEventManager = new UIEventManager(false);
private pointerEvents: PointerEvents; private pointerEvents: PointerEvents;
@ -58,7 +59,9 @@ export class PanGesture {
capture: opts.capture, capture: opts.capture,
passive: opts.passive passive: opts.passive
}; };
this.detector = new PanRecognizer(opts.direction, opts.threshold, opts.maxAngle); if (opts.threshold > 0) {
this.detector = new PanRecognizer(opts.direction, opts.threshold, opts.maxAngle);
}
} }
listen() { listen() {
@ -100,40 +103,42 @@ export class PanGesture {
return false; return false;
} }
} }
let coord = pointerCoord(ev);
this.detector.start(coord);
this.started = true; this.started = true;
this.captured = false; this.captured = false;
const coord = pointerCoord(ev);
if (this.detector) {
this.detector.start(coord);
} else {
if (!this.tryToCapture(ev)) {
this.started = false;
this.captured = false;
this.gestute.release();
return false;
}
}
return true; return true;
} }
pointerMove(ev: any) { pointerMove(ev: any) {
if (!this.started) { assert(this.started === true, 'started must be true');
if (this.captured) {
this.debouncer.debounce(() => {
this.onDragMove(ev);
});
return; return;
} }
this.debouncer.debounce(() => {
if (this.captured) {
this.onDragMove(ev);
return;
}
let coord = pointerCoord(ev);
if (this.detector.detect(coord)) {
if (this.detector.pan() !== 0 && assert(this.detector, 'detector has to be valid');
(!this.gestute || this.gestute.capture())) { const coord = pointerCoord(ev);
this.onDragStart(ev); if (this.detector.detect(coord)) {
this.captured = true; if (this.detector.pan() !== 0) {
return; if (!this.tryToCapture(ev)) {
this.abort(ev);
} }
// Detection/capturing was not successful, aborting!
this.started = false;
this.captured = false;
this.pointerEvents.stop();
this.notCaptured(ev);
} }
}); }
} }
pointerUp(ev: any) { pointerUp(ev: any) {
@ -151,6 +156,26 @@ export class PanGesture {
this.started = false; this.started = false;
} }
tryToCapture(ev: any): boolean {
assert(this.started === true, 'started has be true');
assert(this.captured === false, 'captured has be false');
if (this.gestute && !this.gestute.capture()) {
return false;
}
this.onDragStart(ev);
this.captured = true;
return true;
}
abort(ev: any) {
this.started = false;
this.captured = false;
this.gestute.release();
this.pointerEvents.stop();
this.notCaptured(ev);
}
getNativeElement(): HTMLElement { getNativeElement(): HTMLElement {
return this.element; return this.element;
} }

View File

@ -14,6 +14,9 @@ export const GESTURE_ITEM_SWIPE = 'item-swipe';
/** @private */ /** @private */
export const GESTURE_REFRESHER = 'refresher'; export const GESTURE_REFRESHER = 'refresher';
/** @private */
export const GESTURE_TOGGLE = 'toggle';
/** /**
* @private * @private
*/ */
@ -24,11 +27,13 @@ export const enum GesturePriority {
Normal = 0, Normal = 0,
High = 10, High = 10,
VeryHigh = 20, VeryHigh = 20,
VeryVeryHigh = 30,
SlidingItem = Low, SlidingItem = Low,
MenuSwipe = High, MenuSwipe = High,
GoBackSwipe = VeryHigh, GoBackSwipe = VeryHigh,
Refresher = Normal, Refresher = Normal,
Toggle = VeryVeryHigh
} }
/** /**

View File

@ -2,15 +2,17 @@ import { PointerCoordinates } from '../util/dom';
export class PanRecognizer { export class PanRecognizer {
private startCoord: PointerCoordinates; private startCoord: PointerCoordinates;
private dirty: boolean = false; private dirty: boolean = false;
private threshold: number; private threshold: number;
private maxCosine: number; private maxCosine: number;
private _angle: any = 0; private _angle: any = 0;
private _isPan: number = 0; private _isPan: number = 0;
constructor(private direction: string, threshold: number, maxAngle: number) { constructor(private direction: string, threshold: number, maxAngle: number) {
let radians = maxAngle * (Math.PI / 180); const radians = maxAngle * (Math.PI / 180);
this.maxCosine = Math.cos(radians); this.maxCosine = Math.cos(radians);
this.threshold = threshold * threshold; this.threshold = threshold * threshold;
} }
@ -26,12 +28,13 @@ export class PanRecognizer {
if (!this.dirty) { if (!this.dirty) {
return false; return false;
} }
let deltaX = (coord.x - this.startCoord.x); const deltaX = (coord.x - this.startCoord.x);
let deltaY = (coord.y - this.startCoord.y); const deltaY = (coord.y - this.startCoord.y);
let distance = deltaX * deltaX + deltaY * deltaY; const distance = deltaX * deltaX + deltaY * deltaY;
if (distance >= this.threshold) { if (distance >= this.threshold) {
let angle = Math.atan2(deltaY, deltaX); var angle = Math.atan2(deltaY, deltaX);
let cosine = (this.direction === 'y') var cosine = (this.direction === 'y')
? Math.sin(angle) ? Math.sin(angle)
: Math.cos(angle); : Math.cos(angle);