Skip to content

Commit 713e2a1

Browse files
committed
feat(haptic): add haptic/taptic support to toggle/range/picker
1 parent 63c6d46 commit 713e2a1

File tree

6 files changed

+158
-2
lines changed

6 files changed

+158
-2
lines changed

src/components/picker/picker-component.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Key } from '../../util/key';
88
import { NavParams } from '../../navigation/nav-params';
99
import { Picker } from './picker';
1010
import { PickerOptions, PickerColumn, PickerColumnOption } from './picker-options';
11+
import { Haptic } from '../../util/haptic';
1112
import { UIEventManager } from '../../util/ui-event-manager';
1213
import { ViewController } from '../../navigation/view-controller';
1314

@@ -53,12 +54,13 @@ export class PickerColumnCmp {
5354
maxY: number;
5455
rotateFactor: number;
5556
lastIndex: number;
57+
lastTempIndex: number;
5658
receivingEvents: boolean = false;
5759
events: UIEventManager = new UIEventManager();
5860

5961
@Output() ionChange: EventEmitter<any> = new EventEmitter();
6062

61-
constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizer) {
63+
constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizer, private _haptic: Haptic) {
6264
this.rotateFactor = config.getNumber('pickerRotateFactor', 0);
6365
}
6466

@@ -114,6 +116,9 @@ export class PickerColumnCmp {
114116

115117
this.minY = (minY * this.optHeight * -1);
116118
this.maxY = (maxY * this.optHeight * -1);
119+
120+
this._haptic.gestureSelectionStart();
121+
117122
return true;
118123
}
119124

@@ -146,6 +151,14 @@ export class PickerColumnCmp {
146151
}
147152

148153
this.update(y, 0, false, false);
154+
155+
let currentIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0);
156+
if (currentIndex !== this.lastTempIndex) {
157+
// Trigger a haptic event for physical feedback that the index has changed
158+
this._haptic.gestureSelectionChanged();
159+
}
160+
this.lastTempIndex = currentIndex;
161+
149162
}
150163

151164
pointerEnd(ev: UIEvent) {
@@ -209,6 +222,8 @@ export class PickerColumnCmp {
209222
if (isNaN(this.y) || !this.optHeight) {
210223
// fallback in case numbers get outta wack
211224
this.update(y, 0, true, true);
225+
this._haptic.gestureSelectionEnd();
226+
212227

213228
} else if (Math.abs(this.velocity) > 0) {
214229
// still decelerating
@@ -230,12 +245,13 @@ export class PickerColumnCmp {
230245
this.velocity = 0;
231246
}
232247

233-
console.log(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`);
248+
//console.debug(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`);
234249

235250
var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1);
236251

237252
this.update(y, 0, true, !notLockedIn);
238253

254+
239255
if (notLockedIn) {
240256
// isn't locked in yet, keep decelerating until it is
241257
this.rafId = raf(this.decelerate.bind(this));
@@ -247,9 +263,17 @@ export class PickerColumnCmp {
247263

248264
// create a velocity in the direction it needs to scroll
249265
this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1);
266+
this._haptic.gestureSelectionEnd();
250267

251268
this.decelerate();
252269
}
270+
271+
let currentIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0);
272+
if (currentIndex !== this.lastTempIndex) {
273+
// Trigger a haptic event for physical feedback that the index has changed
274+
this._haptic.gestureSelectionChanged();
275+
}
276+
this.lastTempIndex = currentIndex;
253277
}
254278

255279
optClick(ev: UIEvent, index: number) {

src/components/range/range.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Form } from '../../util/form';
88
import { Ion } from '../ion';
99
import { Item } from '../item/item';
1010
import { PointerCoordinates, pointerCoord, raf } from '../../util/dom';
11+
import { Haptic } from '../../util/haptic';
1112
import { UIEventManager } from '../../util/ui-event-manager';
1213

1314
export const RANGE_VALUE_ACCESSOR: any = {
@@ -343,6 +344,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
343344

344345
constructor(
345346
private _form: Form,
347+
private _haptic: Haptic,
346348
@Optional() private _item: Item,
347349
config: Config,
348350
elementRef: ElementRef,
@@ -436,6 +438,8 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
436438
this._active.position();
437439
this._pressed = this._active.pressed = true;
438440

441+
this._haptic.gestureSelectionStart();
442+
439443
return true;
440444
}
441445

@@ -473,6 +477,8 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
473477
// update the active knob's position
474478
this._active.position();
475479

480+
this._haptic.gestureSelectionEnd();
481+
476482
// clear the start coordinates and active knob
477483
this._start = this._active = null;
478484
this._pressed = this._knobs.first.pressed = this._knobs.last.pressed = false;
@@ -505,6 +511,12 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
505511
let newVal = this._active.value;
506512

507513
if (oldVal !== newVal) {
514+
// Trigger a haptic selection changed event if this is
515+
// a snap range
516+
if (this.snaps) {
517+
this._haptic.gestureSelectionChanged();
518+
}
519+
508520
// value has been updated
509521
if (this._dual) {
510522
this.value = {

src/components/toggle/toggle.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { isTrueProperty } from '../../util/util';
77
import { Ion } from '../ion';
88
import { Item } from '../item/item';
99
import { pointerCoord } from '../../util/dom';
10+
import { Haptic } from '../../util/haptic';
1011
import { UIEventManager } from '../../util/ui-event-manager';
1112

1213
export const TOGGLE_VALUE_ACCESSOR: any = {
@@ -124,6 +125,7 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso
124125
config: Config,
125126
elementRef: ElementRef,
126127
renderer: Renderer,
128+
public _haptic: Haptic,
127129
@Optional() public _item: Item
128130
) {
129131
super(config, elementRef, renderer);
@@ -158,12 +160,15 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso
158160
if (this._checked) {
159161
if (currentX + 15 < this._startX) {
160162
this.onChange(false);
163+
this._haptic.selection();
161164
this._startX = currentX;
162165
this._activated = true;
163166
}
164167

165168
} else if (currentX - 15 > this._startX) {
166169
this.onChange(true);
170+
// Create a haptic event
171+
this._haptic.selection();
167172
this._startX = currentX;
168173
this._activated = (currentX < this._startX + 5);
169174
}
@@ -180,10 +185,12 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso
180185
if (this.checked) {
181186
if (this._startX + 4 > endX) {
182187
this.onChange(false);
188+
this._haptic.selection();
183189
}
184190

185191
} else if (this._startX - 4 < endX) {
186192
this.onChange(true);
193+
this._haptic.selection();
187194
}
188195

189196
this._activated = false;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './gestures/gesture-controller';
1010

1111
export * from './util/click-block';
1212
export * from './util/events';
13+
export * from './util/haptic';
1314
export * from './util/keyboard';
1415
export * from './util/form';
1516
export { reorderArray } from './util/util';

src/module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { DeepLinker, setupDeepLinker } from './navigation/deep-linker';
1515
import { Events, setupProvideEvents } from './util/events';
1616
import { Form } from './util/form';
1717
import { GestureController } from './gestures/gesture-controller';
18+
import { Haptic } from './util/haptic';
1819
import { IonicGestureConfig } from './gestures/gesture-config';
1920
import { Keyboard } from './util/keyboard';
2021
import { LoadingController } from './components/loading/loading';
@@ -52,6 +53,7 @@ import { ToastCmp } from './components/toast/toast-component';
5253
*/
5354
export { Config, setupConfig, ConfigToken } from './config/config';
5455
export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform';
56+
export { Haptic } from './util/haptic';
5557
export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params';
5658
export { DeepLinker } from './navigation/deep-linker';
5759
export { NavController } from './navigation/nav-controller';
@@ -163,6 +165,7 @@ export class IonicModule {
163165
App,
164166
Events,
165167
Form,
168+
Haptic,
166169
GestureController,
167170
Keyboard,
168171
LoadingController,

src/util/haptic.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Injectable } from '@angular/core';
2+
3+
import { Platform } from '../platform/platform';
4+
5+
declare var window;
6+
7+
/**
8+
* @name Haptic
9+
* @description
10+
* The `Haptic` class interacts with a haptic engine on the device, if available. Generally,
11+
* Ionic components use this under the hood, but you're welcome to get a bit crazy with it
12+
* if you fancy.
13+
*
14+
* Currently, this uses the Taptic engine on iOS.
15+
*
16+
* @usage
17+
* ```ts
18+
* export class MyClass{
19+
* constructor(haptic: Haptic){
20+
* haptic.selection();
21+
* }
22+
* }
23+
*
24+
* ```
25+
*/
26+
27+
@Injectable()
28+
export class Haptic {
29+
plugin: any;
30+
31+
constructor(platform: Platform) {
32+
platform.ready().then(() => {
33+
this.plugin = window.TapticEngine;
34+
});
35+
}
36+
37+
available() {
38+
return !!this.plugin;
39+
}
40+
41+
/**
42+
* Trigger a selection changed haptic event. Good for one-time events (not for gestures)
43+
*/
44+
selection() {
45+
if(!this.plugin) {
46+
return;
47+
}
48+
49+
this.plugin.selection();
50+
}
51+
52+
/**
53+
* Tell the haptic engine that a gesture for a selection change is starting.
54+
*/
55+
gestureSelectionStart() {
56+
if(!this.plugin) {
57+
return;
58+
}
59+
60+
this.plugin.gestureSelectionStart();
61+
}
62+
63+
/**
64+
* Tell the haptic engine that a selection changed during a gesture.
65+
*/
66+
gestureSelectionChanged() {
67+
if(!this.plugin) {
68+
return;
69+
}
70+
71+
this.plugin.gestureSelectionChanged();
72+
}
73+
74+
/**
75+
* Tell the haptic engine we are done with a gesture. This needs to be
76+
* called lest resources are not properly recycled.
77+
*/
78+
gestureSelectionEnd() {
79+
if(!this.plugin) {
80+
return;
81+
}
82+
83+
this.plugin.gestureSelectionEnd();
84+
}
85+
86+
/**
87+
* Use this to indicate success/failure/warning to the user.
88+
* options should be of the type { type: 'success' } (or 'warning'/'error')
89+
*/
90+
notification(options: { type: string }) {
91+
if(!this.plugin) {
92+
return;
93+
}
94+
95+
this.plugin.notification(options);
96+
}
97+
98+
/**
99+
* Use this to indicate success/failure/warning to the user.
100+
* options should be of the type { style: 'light' } (or 'medium'/'heavy')
101+
*/
102+
impact(options: { style: string }) {
103+
if(!this.plugin) {
104+
return;
105+
}
106+
107+
this.plugin.impact(options);
108+
}
109+
}

0 commit comments

Comments
 (0)