From bb800339f87c627569d3cdf1fe08ecbf49f8b88f Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Tue, 15 Nov 2016 17:43:51 -0600 Subject: [PATCH] perf(activator): improve activator response --- src/components/tap-click/activator.ts | 42 ++++++++++++++++--- src/components/tap-click/ripple.ts | 58 +++++++++++++-------------- src/components/tap-click/tap-click.ts | 25 ++++++++---- src/util/dom.ts | 31 ++++++++++---- 4 files changed, 107 insertions(+), 49 deletions(-) diff --git a/src/components/tap-click/activator.ts b/src/components/tap-click/activator.ts index 99fb82d31e..8c7c93d0ae 100644 --- a/src/components/tap-click/activator.ts +++ b/src/components/tap-click/activator.ts @@ -7,11 +7,29 @@ export class Activator { protected _css: string; protected _queue: HTMLElement[] = []; protected _active: HTMLElement[] = []; + protected _activeRafDefer: Function; constructor(protected app: App, config: Config) { 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) { // the user just pressed down if (this.disableActivated(ev)) { @@ -21,7 +39,7 @@ export class Activator { // queue to have this element activated this._queue.push(activatableEle); - rafFrames(6, () => { + this._activeRafDefer = rafFrames(6, () => { let activatableEle: HTMLElement; for (let i = 0; i < this._queue.length; i++) { activatableEle = this._queue[i]; @@ -31,18 +49,22 @@ export class Activator { } } this._queue.length = 0; + this._clearDeferred(); }); } + // the user was pressing down, then just let up upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { - // the user was pressing down, then just let up + this._clearDeferred(); + rafFrames(CLEAR_STATE_DEFERS, () => { this.clearState(); }); } + // all states should return to normal clearState() { - // all states should return to normal + if (!this.app.isEnabled()) { // 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 @@ -57,18 +79,28 @@ export class Activator { } } + // remove the active class from all active elements deactivate() { - // remove the active class from all active elements + this._clearDeferred(); + this._queue.length = 0; rafFrames(2, () => { for (var i = 0; i < this._active.length; i++) { 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) { if (ev.defaultPrevented) { return true; diff --git a/src/components/tap-click/ripple.ts b/src/components/tap-click/ripple.ts index 89b05d18cf..7ef673d750 100644 --- a/src/components/tap-click/ripple.ts +++ b/src/components/tap-click/ripple.ts @@ -13,38 +13,34 @@ export class RippleActivator extends Activator { 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) { - if (this.disableActivated(ev)) { + if (this.disableActivated(ev) || !activatableEle || !activatableEle.parentNode) { return; } - // queue to have this element activated - this._queue.push(activatableEle); + this._active.push(activatableEle); - for (var i = 0; i < this._queue.length; i++) { - 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--) { - var rippleEle: any = queuedEle.children[j]; - if (rippleEle.classList.contains('button-effect')) { - // DOM READ - var clientRect = activatableEle.getBoundingClientRect(); - rippleEle.$top = clientRect.top; - rippleEle.$left = clientRect.left; - rippleEle.$width = clientRect.width; - rippleEle.$height = clientRect.height; - break; - } - } + var j = activatableEle.childElementCount; + while (j--) { + var rippleEle: any = activatableEle.children[j]; + if (rippleEle.classList.contains('button-effect')) { + // DOM READ + var clientRect = activatableEle.getBoundingClientRect(); + rippleEle.$top = clientRect.top; + rippleEle.$left = clientRect.left; + rippleEle.$width = clientRect.width; + rippleEle.$height = clientRect.height; + break; } } - this._queue = []; + + // DOM WRITE + activatableEle.classList.add(this._css); } upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { @@ -53,6 +49,7 @@ export class RippleActivator extends Activator { while (i--) { var rippleEle: any = activatableEle.children[i]; if (rippleEle.classList.contains('button-effect')) { + // DOM WRITE this.startRippleEffect(rippleEle, activatableEle, startCoord); break; } @@ -63,6 +60,10 @@ export class RippleActivator extends Activator { } startRippleEffect(rippleEle: any, activatableEle: HTMLElement, startCoord: PointerCoordinates) { + if (!startCoord) { + return; + } + let clientPointerX = (startCoord.x - rippleEle.$left); let clientPointerY = (startCoord.y - rippleEle.$top); @@ -82,6 +83,7 @@ export class RippleActivator extends Activator { diameter = Math.round(diameter); // Reset ripple + // DOM WRITE rippleEle.style.opacity = ''; rippleEle.style[CSS.transform] = `translate3d(${clientPointerX}px, ${clientPointerY}px, 0px) scale(0.001)`; rippleEle.style[CSS.transition] = ''; @@ -106,14 +108,12 @@ export class RippleActivator extends Activator { } deactivate() { - // remove the active class from all active elements - this._queue = []; - rafFrames(2, () => { for (var i = 0; i < this._active.length; i++) { + // DOM WRITE this._active[i].classList.remove(this._css); } - this._active = []; + this._active.length = 0; }); } diff --git a/src/components/tap-click/tap-click.ts b/src/components/tap-click/tap-click.ts index 222e2e3991..0b81f3ed27 100644 --- a/src/components/tap-click/tap-click.ts +++ b/src/components/tap-click/tap-click.ts @@ -73,20 +73,20 @@ export class TapClick { if (!this.startCoord) { return; } - if (this.usePolyfill && type === PointerEventType.TOUCH && this.app.isEnabled()) { - this.handleTapPolyfill(ev); - } if (this.activator) { let activatableEle = getActivatableTarget(ev.target); if (activatableEle) { this.activator.upAction(ev, activatableEle, this.startCoord); } } + if (this.usePolyfill && type === PointerEventType.TOUCH && this.app.isEnabled()) { + this.handleTapPolyfill(ev); + } this.startCoord = null; } pointerCancel(ev: UIEvent) { - console.debug('pointerCancel from ' + ev.type + ' ' + Date.now()); + console.debug(`pointerCancel from ${ev.type} ${Date.now()}`); this.startCoord = null; this.activator && this.activator.clearState(); this.pointerEvents.stop(); @@ -103,9 +103,18 @@ export class TapClick { } 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.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); 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; } // prevent native mouse click events for XX amount of time @@ -125,11 +134,11 @@ export class TapClick { if (this.app.isScrolling()) { // 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 { // 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'); clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null); diff --git a/src/util/dom.ts b/src/util/dom.ts index d0e0a383d2..7161333534 100644 --- a/src/util/dom.ts +++ b/src/util/dom.ts @@ -36,17 +36,31 @@ export const cancelRaf = window.cancelAnimationFrame.bind(window); export const nativeTimeout = window[window['Zone']['__symbol__']('setTimeout')]['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) { framesToWait = Math.ceil(framesToWait); + let rafId: any; + let timeoutId: any; if (framesToWait < 2) { - nativeRaf(callback); + rafId = nativeRaf(callback); } else { - nativeTimeout(() => { - nativeRaf(callback); + timeoutId = nativeTimeout(() => { + rafId = nativeRaf(callback); }, (framesToWait - 1) * 16.6667); } + + return function() { + clearNativeTimeout(timeoutId); + cancelRaf(raf); + }; } // TODO: DRY rafFrames and zoneRafFrames @@ -210,10 +224,13 @@ export function pointerCoord(ev: any): PointerCoordinates { } export function hasPointerMoved(threshold: number, startCoord: PointerCoordinates, endCoord: PointerCoordinates) { - let deltaX = (startCoord.x - endCoord.x); - let deltaY = (startCoord.y - endCoord.y); - let distance = deltaX * deltaX + deltaY * deltaY; - return distance > (threshold * threshold); + if (startCoord && endCoord) { + const deltaX = (startCoord.x - endCoord.x); + const deltaY = (startCoord.y - endCoord.y); + const distance = deltaX * deltaX + deltaY * deltaY; + return distance > (threshold * threshold); + } + return false; } export function isActive(ele: HTMLElement) {