diff --git a/src/components/picker/picker-component.ts b/src/components/picker/picker-component.ts index 1e028e69ce..40845254bb 100644 --- a/src/components/picker/picker-component.ts +++ b/src/components/picker/picker-component.ts @@ -8,6 +8,7 @@ import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; import { Picker } from './picker'; import { PickerOptions, PickerColumn, PickerColumnOption } from './picker-options'; +import { Haptic } from '../../util/haptic'; import { UIEventManager } from '../../util/ui-event-manager'; import { ViewController } from '../../navigation/view-controller'; @@ -53,12 +54,13 @@ export class PickerColumnCmp { maxY: number; rotateFactor: number; lastIndex: number; + lastTempIndex: number; receivingEvents: boolean = false; events: UIEventManager = new UIEventManager(); @Output() ionChange: EventEmitter = new EventEmitter(); - constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizer) { + constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizer, private _haptic: Haptic) { this.rotateFactor = config.getNumber('pickerRotateFactor', 0); } @@ -114,6 +116,9 @@ export class PickerColumnCmp { this.minY = (minY * this.optHeight * -1); this.maxY = (maxY * this.optHeight * -1); + + this._haptic.gestureSelectionStart(); + return true; } @@ -146,6 +151,14 @@ export class PickerColumnCmp { } this.update(y, 0, false, false); + + let currentIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0); + if (currentIndex !== this.lastTempIndex) { + // Trigger a haptic event for physical feedback that the index has changed + this._haptic.gestureSelectionChanged(); + } + this.lastTempIndex = currentIndex; + } pointerEnd(ev: UIEvent) { @@ -209,6 +222,8 @@ export class PickerColumnCmp { if (isNaN(this.y) || !this.optHeight) { // fallback in case numbers get outta wack this.update(y, 0, true, true); + this._haptic.gestureSelectionEnd(); + } else if (Math.abs(this.velocity) > 0) { // still decelerating @@ -230,12 +245,13 @@ export class PickerColumnCmp { this.velocity = 0; } - console.log(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`); + //console.debug(`decelerate y: ${y}, velocity: ${this.velocity}, optHeight: ${this.optHeight}`); var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1); this.update(y, 0, true, !notLockedIn); + if (notLockedIn) { // isn't locked in yet, keep decelerating until it is this.rafId = raf(this.decelerate.bind(this)); @@ -247,9 +263,17 @@ export class PickerColumnCmp { // create a velocity in the direction it needs to scroll this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1); + this._haptic.gestureSelectionEnd(); this.decelerate(); } + + let currentIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0); + if (currentIndex !== this.lastTempIndex) { + // Trigger a haptic event for physical feedback that the index has changed + this._haptic.gestureSelectionChanged(); + } + this.lastTempIndex = currentIndex; } optClick(ev: UIEvent, index: number) { diff --git a/src/components/range/range.ts b/src/components/range/range.ts index 580d7a9c1a..4aa05e735a 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -8,6 +8,7 @@ import { Form } from '../../util/form'; import { Ion } from '../ion'; import { Item } from '../item/item'; import { PointerCoordinates, pointerCoord, raf } from '../../util/dom'; +import { Haptic } from '../../util/haptic'; import { UIEventManager } from '../../util/ui-event-manager'; export const RANGE_VALUE_ACCESSOR: any = { @@ -343,6 +344,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O constructor( private _form: Form, + private _haptic: Haptic, @Optional() private _item: Item, config: Config, elementRef: ElementRef, @@ -436,6 +438,8 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O this._active.position(); this._pressed = this._active.pressed = true; + this._haptic.gestureSelectionStart(); + return true; } @@ -473,6 +477,8 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O // update the active knob's position this._active.position(); + this._haptic.gestureSelectionEnd(); + // clear the start coordinates and active knob this._start = this._active = null; this._pressed = this._knobs.first.pressed = this._knobs.last.pressed = false; @@ -505,6 +511,12 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O let newVal = this._active.value; if (oldVal !== newVal) { + // Trigger a haptic selection changed event if this is + // a snap range + if (this.snaps) { + this._haptic.gestureSelectionChanged(); + } + // value has been updated if (this._dual) { this.value = { diff --git a/src/components/toggle/toggle.ts b/src/components/toggle/toggle.ts index 2a109d239f..4de2131113 100644 --- a/src/components/toggle/toggle.ts +++ b/src/components/toggle/toggle.ts @@ -7,6 +7,7 @@ import { isTrueProperty } from '../../util/util'; import { Ion } from '../ion'; import { Item } from '../item/item'; import { pointerCoord } from '../../util/dom'; +import { Haptic } from '../../util/haptic'; import { UIEventManager } from '../../util/ui-event-manager'; export const TOGGLE_VALUE_ACCESSOR: any = { @@ -124,6 +125,7 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso config: Config, elementRef: ElementRef, renderer: Renderer, + public _haptic: Haptic, @Optional() public _item: Item ) { super(config, elementRef, renderer); @@ -158,12 +160,15 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso if (this._checked) { if (currentX + 15 < this._startX) { this.onChange(false); + this._haptic.selection(); this._startX = currentX; this._activated = true; } } else if (currentX - 15 > this._startX) { this.onChange(true); + // Create a haptic event + this._haptic.selection(); this._startX = currentX; this._activated = (currentX < this._startX + 5); } @@ -180,10 +185,12 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso if (this.checked) { if (this._startX + 4 > endX) { this.onChange(false); + this._haptic.selection(); } } else if (this._startX - 4 < endX) { this.onChange(true); + this._haptic.selection(); } this._activated = false; diff --git a/src/index.ts b/src/index.ts index 6be00c3f3b..be81af4023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from './gestures/gesture-controller'; export * from './util/click-block'; export * from './util/events'; +export * from './util/haptic'; export * from './util/keyboard'; export * from './util/form'; export { reorderArray } from './util/util'; diff --git a/src/module.ts b/src/module.ts index b7bd2c0aad..94657166a5 100644 --- a/src/module.ts +++ b/src/module.ts @@ -15,6 +15,7 @@ import { DeepLinker, setupDeepLinker } from './navigation/deep-linker'; import { Events, setupProvideEvents } from './util/events'; import { Form } from './util/form'; import { GestureController } from './gestures/gesture-controller'; +import { Haptic } from './util/haptic'; import { IonicGestureConfig } from './gestures/gesture-config'; import { Keyboard } from './util/keyboard'; import { LoadingController } from './components/loading/loading'; @@ -52,6 +53,7 @@ import { ToastCmp } from './components/toast/toast-component'; */ export { Config, setupConfig, ConfigToken } from './config/config'; export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform'; +export { Haptic } from './util/haptic'; export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params'; export { DeepLinker } from './navigation/deep-linker'; export { NavController } from './navigation/nav-controller'; @@ -163,6 +165,7 @@ export class IonicModule { App, Events, Form, + Haptic, GestureController, Keyboard, LoadingController, diff --git a/src/util/haptic.ts b/src/util/haptic.ts new file mode 100644 index 0000000000..37b21a3222 --- /dev/null +++ b/src/util/haptic.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@angular/core'; + +import { Platform } from '../platform/platform'; + +declare var window; + +/** + * @name Haptic + * @description + * The `Haptic` class interacts with a haptic engine on the device, if available. Generally, + * Ionic components use this under the hood, but you're welcome to get a bit crazy with it + * if you fancy. + * + * Currently, this uses the Taptic engine on iOS. + * + * @usage + * ```ts + * export class MyClass{ + * constructor(haptic: Haptic){ + * haptic.selection(); + * } + * } + * + * ``` + */ + +@Injectable() +export class Haptic { + plugin: any; + + constructor(platform: Platform) { + platform.ready().then(() => { + this.plugin = window.TapticEngine; + }); + } + + available() { + return !!this.plugin; + } + + /** + * Trigger a selection changed haptic event. Good for one-time events (not for gestures) + */ + selection() { + if(!this.plugin) { + return; + } + + this.plugin.selection(); + } + + /** + * Tell the haptic engine that a gesture for a selection change is starting. + */ + gestureSelectionStart() { + if(!this.plugin) { + return; + } + + this.plugin.gestureSelectionStart(); + } + + /** + * Tell the haptic engine that a selection changed during a gesture. + */ + gestureSelectionChanged() { + if(!this.plugin) { + return; + } + + this.plugin.gestureSelectionChanged(); + } + + /** + * Tell the haptic engine we are done with a gesture. This needs to be + * called lest resources are not properly recycled. + */ + gestureSelectionEnd() { + if(!this.plugin) { + return; + } + + this.plugin.gestureSelectionEnd(); + } + + /** + * Use this to indicate success/failure/warning to the user. + * options should be of the type { type: 'success' } (or 'warning'/'error') + */ + notification(options: { type: string }) { + if(!this.plugin) { + return; + } + + this.plugin.notification(options); + } + + /** + * Use this to indicate success/failure/warning to the user. + * options should be of the type { style: 'light' } (or 'medium'/'heavy') + */ + impact(options: { style: string }) { + if(!this.plugin) { + return; + } + + this.plugin.impact(options); + } +}