mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 20:33:32 +08:00
feat(haptic): add haptic/taptic support to toggle/range/picker
This commit is contained in:
@ -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) {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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
109
src/util/haptic.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user