perf(activator): improve activator response

This commit is contained in:
Adam Bradley
2016-11-15 17:43:51 -06:00
committed by Manu Mtz.-Almeida
parent 06938b64a5
commit bb800339f8
4 changed files with 107 additions and 49 deletions

View File

@ -7,11 +7,29 @@ export class Activator {
protected _css: string; protected _css: string;
protected _queue: HTMLElement[] = []; protected _queue: HTMLElement[] = [];
protected _active: HTMLElement[] = []; protected _active: HTMLElement[] = [];
protected _activeRafDefer: Function;
constructor(protected app: App, config: Config) { constructor(protected app: App, config: Config) {
this._css = config.get('activatedClass') || 'activated'; this._css = config.get('activatedClass') || 'activated';
} }
clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// a click happened, so immediately deactive all activated elements
this._clearDeferred();
this._queue.length = 0;
for (var i = 0; i < this._active.length; i++) {
this._active[i].classList.remove(this._css);
}
this._active.length = 0;
// then immediately activate this element
if (activatableEle && activatableEle.parentNode) {
this._active.push(activatableEle);
activatableEle.classList.add(this._css);
}
}
downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// the user just pressed down // the user just pressed down
if (this.disableActivated(ev)) { if (this.disableActivated(ev)) {
@ -21,7 +39,7 @@ export class Activator {
// queue to have this element activated // queue to have this element activated
this._queue.push(activatableEle); this._queue.push(activatableEle);
rafFrames(6, () => { this._activeRafDefer = rafFrames(6, () => {
let activatableEle: HTMLElement; let activatableEle: HTMLElement;
for (let i = 0; i < this._queue.length; i++) { for (let i = 0; i < this._queue.length; i++) {
activatableEle = this._queue[i]; activatableEle = this._queue[i];
@ -31,18 +49,22 @@ export class Activator {
} }
} }
this._queue.length = 0; this._queue.length = 0;
this._clearDeferred();
}); });
} }
upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// the user was pressing down, then just let up // the user was pressing down, then just let up
upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
this._clearDeferred();
rafFrames(CLEAR_STATE_DEFERS, () => { rafFrames(CLEAR_STATE_DEFERS, () => {
this.clearState(); this.clearState();
}); });
} }
clearState() {
// all states should return to normal // all states should return to normal
clearState() {
if (!this.app.isEnabled()) { if (!this.app.isEnabled()) {
// the app is actively disabled, so don't bother deactivating anything. // the app is actively disabled, so don't bother deactivating anything.
// this makes it easier on the GPU so it doesn't have to redraw any // this makes it easier on the GPU so it doesn't have to redraw any
@ -57,18 +79,28 @@ export class Activator {
} }
} }
deactivate() {
// remove the active class from all active elements // remove the active class from all active elements
deactivate() {
this._clearDeferred();
this._queue.length = 0; this._queue.length = 0;
rafFrames(2, () => { rafFrames(2, () => {
for (var i = 0; i < this._active.length; i++) { for (var i = 0; i < this._active.length; i++) {
this._active[i].classList.remove(this._css); this._active[i].classList.remove(this._css);
} }
this._active = []; this._active.length = 0;
}); });
} }
_clearDeferred() {
// Clear any active deferral
if (this._activeRafDefer) {
this._activeRafDefer();
this._activeRafDefer = null;
}
}
disableActivated(ev: any) { disableActivated(ev: any) {
if (ev.defaultPrevented) { if (ev.defaultPrevented) {
return true; return true;

View File

@ -13,25 +13,21 @@ export class RippleActivator extends Activator {
super(app, config); super(app, config);
} }
clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
this.downAction(ev, activatableEle, startCoord);
this.upAction(ev, activatableEle, startCoord);
}
downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
if (this.disableActivated(ev)) { if (this.disableActivated(ev) || !activatableEle || !activatableEle.parentNode) {
return; return;
} }
// queue to have this element activated this._active.push(activatableEle);
this._queue.push(activatableEle);
for (var i = 0; i < this._queue.length; i++) { var j = activatableEle.childElementCount;
var queuedEle = this._queue[i];
if (queuedEle && queuedEle.parentNode) {
this._active.push(queuedEle);
// DOM WRITE
queuedEle.classList.add(this._css);
var j = queuedEle.childElementCount;
while (j--) { while (j--) {
var rippleEle: any = queuedEle.children[j]; var rippleEle: any = activatableEle.children[j];
if (rippleEle.classList.contains('button-effect')) { if (rippleEle.classList.contains('button-effect')) {
// DOM READ // DOM READ
var clientRect = activatableEle.getBoundingClientRect(); var clientRect = activatableEle.getBoundingClientRect();
@ -42,9 +38,9 @@ export class RippleActivator extends Activator {
break; break;
} }
} }
}
} // DOM WRITE
this._queue = []; activatableEle.classList.add(this._css);
} }
upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
@ -53,6 +49,7 @@ export class RippleActivator extends Activator {
while (i--) { while (i--) {
var rippleEle: any = activatableEle.children[i]; var rippleEle: any = activatableEle.children[i];
if (rippleEle.classList.contains('button-effect')) { if (rippleEle.classList.contains('button-effect')) {
// DOM WRITE
this.startRippleEffect(rippleEle, activatableEle, startCoord); this.startRippleEffect(rippleEle, activatableEle, startCoord);
break; break;
} }
@ -63,6 +60,10 @@ export class RippleActivator extends Activator {
} }
startRippleEffect(rippleEle: any, activatableEle: HTMLElement, startCoord: PointerCoordinates) { startRippleEffect(rippleEle: any, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
if (!startCoord) {
return;
}
let clientPointerX = (startCoord.x - rippleEle.$left); let clientPointerX = (startCoord.x - rippleEle.$left);
let clientPointerY = (startCoord.y - rippleEle.$top); let clientPointerY = (startCoord.y - rippleEle.$top);
@ -82,6 +83,7 @@ export class RippleActivator extends Activator {
diameter = Math.round(diameter); diameter = Math.round(diameter);
// Reset ripple // Reset ripple
// DOM WRITE
rippleEle.style.opacity = ''; rippleEle.style.opacity = '';
rippleEle.style[CSS.transform] = `translate3d(${clientPointerX}px, ${clientPointerY}px, 0px) scale(0.001)`; rippleEle.style[CSS.transform] = `translate3d(${clientPointerX}px, ${clientPointerY}px, 0px) scale(0.001)`;
rippleEle.style[CSS.transition] = ''; rippleEle.style[CSS.transition] = '';
@ -106,14 +108,12 @@ export class RippleActivator extends Activator {
} }
deactivate() { deactivate() {
// remove the active class from all active elements
this._queue = [];
rafFrames(2, () => { rafFrames(2, () => {
for (var i = 0; i < this._active.length; i++) { for (var i = 0; i < this._active.length; i++) {
// DOM WRITE
this._active[i].classList.remove(this._css); this._active[i].classList.remove(this._css);
} }
this._active = []; this._active.length = 0;
}); });
} }

View File

@ -73,20 +73,20 @@ export class TapClick {
if (!this.startCoord) { if (!this.startCoord) {
return; return;
} }
if (this.usePolyfill && type === PointerEventType.TOUCH && this.app.isEnabled()) {
this.handleTapPolyfill(ev);
}
if (this.activator) { if (this.activator) {
let activatableEle = getActivatableTarget(ev.target); let activatableEle = getActivatableTarget(ev.target);
if (activatableEle) { if (activatableEle) {
this.activator.upAction(ev, activatableEle, this.startCoord); this.activator.upAction(ev, activatableEle, this.startCoord);
} }
} }
if (this.usePolyfill && type === PointerEventType.TOUCH && this.app.isEnabled()) {
this.handleTapPolyfill(ev);
}
this.startCoord = null; this.startCoord = null;
} }
pointerCancel(ev: UIEvent) { pointerCancel(ev: UIEvent) {
console.debug('pointerCancel from ' + ev.type + ' ' + Date.now()); console.debug(`pointerCancel from ${ev.type} ${Date.now()}`);
this.startCoord = null; this.startCoord = null;
this.activator && this.activator.clearState(); this.activator && this.activator.clearState();
this.pointerEvents.stop(); this.pointerEvents.stop();
@ -103,9 +103,18 @@ export class TapClick {
} }
if (preventReason !== null) { if (preventReason !== null) {
console.debug('click prevent ' + preventReason + ' ' + Date.now()); // darn, there was a reason to prevent this click, let's not allow it
console.debug(`click prevent ${preventReason} ${Date.now()}`);
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
} else if (this.activator) {
// cool, a click is gonna happen, let's tell the activator
// so the element can get the given "active" style
const activatableEle = getActivatableTarget(ev.target);
if (activatableEle) {
this.activator.clickAction(ev, activatableEle, this.startCoord);
}
} }
} }
@ -117,7 +126,7 @@ export class TapClick {
let endCoord = pointerCoord(ev); let endCoord = pointerCoord(ev);
if (hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) { if (hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) {
console.debug('click from touch prevented by pointer moved'); console.debug(`click from touch prevented by pointer moved`);
return; return;
} }
// prevent native mouse click events for XX amount of time // prevent native mouse click events for XX amount of time
@ -125,11 +134,11 @@ export class TapClick {
if (this.app.isScrolling()) { if (this.app.isScrolling()) {
// do not fire off a click event while the app was scrolling // do not fire off a click event while the app was scrolling
console.debug('click from touch prevented by scrolling ' + Date.now()); console.debug(`click from touch prevented by scrolling ${Date.now()}`);
} else { } else {
// dispatch a mouse click event // dispatch a mouse click event
console.debug('create click from touch ' + Date.now()); console.debug(`create click from touch ${Date.now()}`);
let clickEvent: any = document.createEvent('MouseEvents'); let clickEvent: any = document.createEvent('MouseEvents');
clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null); clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null);

View File

@ -36,17 +36,31 @@ export const cancelRaf = window.cancelAnimationFrame.bind(window);
export const nativeTimeout = window[window['Zone']['__symbol__']('setTimeout')]['bind'](window); export const nativeTimeout = window[window['Zone']['__symbol__']('setTimeout')]['bind'](window);
export const clearNativeTimeout = window[window['Zone']['__symbol__']('clearTimeout')]['bind'](window); export const clearNativeTimeout = window[window['Zone']['__symbol__']('clearTimeout')]['bind'](window);
/**
* Run a function in an animation frame after waiting `framesToWait` frames.
*
* @param framesToWait number how many frames to wait
* @param callback Function the function call to defer
* @return Function a function to call to cancel the wait
*/
export function rafFrames(framesToWait: number, callback: Function) { export function rafFrames(framesToWait: number, callback: Function) {
framesToWait = Math.ceil(framesToWait); framesToWait = Math.ceil(framesToWait);
let rafId: any;
let timeoutId: any;
if (framesToWait < 2) { if (framesToWait < 2) {
nativeRaf(callback); rafId = nativeRaf(callback);
} else { } else {
nativeTimeout(() => { timeoutId = nativeTimeout(() => {
nativeRaf(callback); rafId = nativeRaf(callback);
}, (framesToWait - 1) * 16.6667); }, (framesToWait - 1) * 16.6667);
} }
return function() {
clearNativeTimeout(timeoutId);
cancelRaf(raf);
};
} }
// TODO: DRY rafFrames and zoneRafFrames // TODO: DRY rafFrames and zoneRafFrames
@ -210,11 +224,14 @@ export function pointerCoord(ev: any): PointerCoordinates {
} }
export function hasPointerMoved(threshold: number, startCoord: PointerCoordinates, endCoord: PointerCoordinates) { export function hasPointerMoved(threshold: number, startCoord: PointerCoordinates, endCoord: PointerCoordinates) {
let deltaX = (startCoord.x - endCoord.x); if (startCoord && endCoord) {
let deltaY = (startCoord.y - endCoord.y); const deltaX = (startCoord.x - endCoord.x);
let distance = deltaX * deltaX + deltaY * deltaY; const deltaY = (startCoord.y - endCoord.y);
const distance = deltaX * deltaX + deltaY * deltaY;
return distance > (threshold * threshold); return distance > (threshold * threshold);
} }
return false;
}
export function isActive(ele: HTMLElement) { export function isActive(ele: HTMLElement) {
return !!(ele && (document.activeElement === ele)); return !!(ele && (document.activeElement === ele));