feat(haptic): add haptic/taptic support to toggle/range/picker

This commit is contained in:
Max Lynch
2016-10-05 20:49:55 -05:00
parent 63c6d468f2
commit 713e2a1a33
6 changed files with 158 additions and 2 deletions

View File

@ -8,6 +8,7 @@ import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { Picker } from './picker'; import { Picker } from './picker';
import { PickerOptions, PickerColumn, PickerColumnOption } from './picker-options'; import { PickerOptions, PickerColumn, PickerColumnOption } from './picker-options';
import { Haptic } from '../../util/haptic';
import { UIEventManager } from '../../util/ui-event-manager'; import { UIEventManager } from '../../util/ui-event-manager';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
@ -53,12 +54,13 @@ export class PickerColumnCmp {
maxY: number; maxY: number;
rotateFactor: number; rotateFactor: number;
lastIndex: number; lastIndex: number;
lastTempIndex: number;
receivingEvents: boolean = false; receivingEvents: boolean = false;
events: UIEventManager = new UIEventManager(); events: UIEventManager = new UIEventManager();
@Output() ionChange: EventEmitter<any> = new EventEmitter(); @Output() ionChange: EventEmitter<any> = 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); this.rotateFactor = config.getNumber('pickerRotateFactor', 0);
} }
@ -114,6 +116,9 @@ export class PickerColumnCmp {
this.minY = (minY * this.optHeight * -1); this.minY = (minY * this.optHeight * -1);
this.maxY = (maxY * this.optHeight * -1); this.maxY = (maxY * this.optHeight * -1);
this._haptic.gestureSelectionStart();
return true; return true;
} }
@ -146,6 +151,14 @@ export class PickerColumnCmp {
} }
this.update(y, 0, false, false); 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) { pointerEnd(ev: UIEvent) {
@ -209,6 +222,8 @@ export class PickerColumnCmp {
if (isNaN(this.y) || !this.optHeight) { if (isNaN(this.y) || !this.optHeight) {
// fallback in case numbers get outta wack // fallback in case numbers get outta wack
this.update(y, 0, true, true); this.update(y, 0, true, true);
this._haptic.gestureSelectionEnd();
} else if (Math.abs(this.velocity) > 0) { } else if (Math.abs(this.velocity) > 0) {
// still decelerating // still decelerating
@ -230,12 +245,13 @@ export class PickerColumnCmp {
this.velocity = 0; 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); var notLockedIn = (y % this.optHeight !== 0 || Math.abs(this.velocity) > 1);
this.update(y, 0, true, !notLockedIn); this.update(y, 0, true, !notLockedIn);
if (notLockedIn) { if (notLockedIn) {
// isn't locked in yet, keep decelerating until it is // isn't locked in yet, keep decelerating until it is
this.rafId = raf(this.decelerate.bind(this)); this.rafId = raf(this.decelerate.bind(this));
@ -247,9 +263,17 @@ export class PickerColumnCmp {
// create a velocity in the direction it needs to scroll // create a velocity in the direction it needs to scroll
this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1); this.velocity = (currentPos > (this.optHeight / 2) ? 1 : -1);
this._haptic.gestureSelectionEnd();
this.decelerate(); 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) { optClick(ev: UIEvent, index: number) {

View File

@ -8,6 +8,7 @@ import { Form } from '../../util/form';
import { Ion } from '../ion'; import { Ion } from '../ion';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { PointerCoordinates, pointerCoord, raf } from '../../util/dom'; import { PointerCoordinates, pointerCoord, raf } from '../../util/dom';
import { Haptic } from '../../util/haptic';
import { UIEventManager } from '../../util/ui-event-manager'; import { UIEventManager } from '../../util/ui-event-manager';
export const RANGE_VALUE_ACCESSOR: any = { export const RANGE_VALUE_ACCESSOR: any = {
@ -343,6 +344,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
constructor( constructor(
private _form: Form, private _form: Form,
private _haptic: Haptic,
@Optional() private _item: Item, @Optional() private _item: Item,
config: Config, config: Config,
elementRef: ElementRef, elementRef: ElementRef,
@ -436,6 +438,8 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
this._active.position(); this._active.position();
this._pressed = this._active.pressed = true; this._pressed = this._active.pressed = true;
this._haptic.gestureSelectionStart();
return true; return true;
} }
@ -473,6 +477,8 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
// update the active knob's position // update the active knob's position
this._active.position(); this._active.position();
this._haptic.gestureSelectionEnd();
// clear the start coordinates and active knob // clear the start coordinates and active knob
this._start = this._active = null; this._start = this._active = null;
this._pressed = this._knobs.first.pressed = this._knobs.last.pressed = false; 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; let newVal = this._active.value;
if (oldVal !== newVal) { 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 // value has been updated
if (this._dual) { if (this._dual) {
this.value = { this.value = {

View File

@ -7,6 +7,7 @@ import { isTrueProperty } 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 { pointerCoord } from '../../util/dom';
import { Haptic } from '../../util/haptic';
import { UIEventManager } from '../../util/ui-event-manager'; import { UIEventManager } from '../../util/ui-event-manager';
export const TOGGLE_VALUE_ACCESSOR: any = { export const TOGGLE_VALUE_ACCESSOR: any = {
@ -124,6 +125,7 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso
config: Config, config: Config,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
public _haptic: Haptic,
@Optional() public _item: Item @Optional() public _item: Item
) { ) {
super(config, elementRef, renderer); super(config, elementRef, renderer);
@ -158,12 +160,15 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso
if (this._checked) { if (this._checked) {
if (currentX + 15 < this._startX) { if (currentX + 15 < this._startX) {
this.onChange(false); this.onChange(false);
this._haptic.selection();
this._startX = currentX; this._startX = currentX;
this._activated = true; this._activated = true;
} }
} else if (currentX - 15 > this._startX) { } else if (currentX - 15 > this._startX) {
this.onChange(true); this.onChange(true);
// Create a haptic event
this._haptic.selection();
this._startX = currentX; this._startX = currentX;
this._activated = (currentX < this._startX + 5); this._activated = (currentX < this._startX + 5);
} }
@ -180,10 +185,12 @@ export class Toggle extends Ion implements AfterContentInit, ControlValueAccesso
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) { } else if (this._startX - 4 < endX) {
this.onChange(true); this.onChange(true);
this._haptic.selection();
} }
this._activated = false; this._activated = false;

View File

@ -10,6 +10,7 @@ export * from './gestures/gesture-controller';
export * from './util/click-block'; export * from './util/click-block';
export * from './util/events'; export * from './util/events';
export * from './util/haptic';
export * from './util/keyboard'; export * from './util/keyboard';
export * from './util/form'; export * from './util/form';
export { reorderArray } from './util/util'; export { reorderArray } from './util/util';

View File

@ -15,6 +15,7 @@ import { DeepLinker, setupDeepLinker } from './navigation/deep-linker';
import { Events, setupProvideEvents } from './util/events'; import { Events, setupProvideEvents } from './util/events';
import { Form } from './util/form'; import { Form } from './util/form';
import { GestureController } from './gestures/gesture-controller'; import { GestureController } from './gestures/gesture-controller';
import { Haptic } from './util/haptic';
import { IonicGestureConfig } from './gestures/gesture-config'; import { IonicGestureConfig } from './gestures/gesture-config';
import { Keyboard } from './util/keyboard'; import { Keyboard } from './util/keyboard';
import { LoadingController } from './components/loading/loading'; 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 { Config, setupConfig, ConfigToken } from './config/config';
export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform'; export { Platform, setupPlatform, UserAgentToken, DocumentDirToken, DocLangToken, NavigatorPlatformToken } from './platform/platform';
export { Haptic } from './util/haptic';
export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params'; export { QueryParams, setupQueryParams, UrlToken } from './platform/query-params';
export { DeepLinker } from './navigation/deep-linker'; export { DeepLinker } from './navigation/deep-linker';
export { NavController } from './navigation/nav-controller'; export { NavController } from './navigation/nav-controller';
@ -163,6 +165,7 @@ export class IonicModule {
App, App,
Events, Events,
Form, Form,
Haptic,
GestureController, GestureController,
Keyboard, Keyboard,
LoadingController, LoadingController,

109
src/util/haptic.ts Normal file
View File

@ -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);
}
}