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

View File

@ -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 = {

View File

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

View File

@ -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';

View File

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

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