perf(menu): flickering

This commit is contained in:
Manu Mtz.-Almeida
2018-08-03 00:29:38 +02:00
parent 69f5cc863f
commit 8253b04604
5 changed files with 247 additions and 259 deletions

View File

@@ -339,10 +339,8 @@ export class Animator {
* Play the animation.
*/
play(opts?: PlayOptions) {
const self = this;
// If the animation was already invalidated (it did finish), do nothing
if (self._destroyed) {
if (this._destroyed) {
return;
}
@@ -352,14 +350,14 @@ export class Animator {
// if there is a duration, then it'll stage all animations at the
// FROM property and transition duration, wait a few frames, then
// kick off the animation by setting the TO property for each animation
self._isAsync = self._hasDuration(opts);
this._isAsync = this._hasDuration(opts);
// ensure all past transition end events have been cleared
self._clearAsync();
this._clearAsync();
// recursively kicks off the correct progress step for each child animation
// ******** DOM WRITE ****************
self._playInit(opts);
this._playInit(opts);
// doubling up RAFs since this animation was probably triggered
// from an input event, and just having one RAF would have this code
@@ -367,7 +365,7 @@ export class Animator {
// input event probably already did way too much work for one frame
raf(() => {
raf(() => {
self._playDomInspect(opts);
this._playDomInspect(opts);
});
});
}
@@ -430,30 +428,29 @@ export class Animator {
* ROOT ANIMATION
*/
_playDomInspect(opts: PlayOptions | undefined) {
const self = this;
// fire off all the "before" function that have DOM READS in them
// elements will be in the DOM, however visibily hidden
// so we can read their dimensions if need be
// ******** DOM READ ****************
// ******** DOM WRITE ****************
self._beforeAnimation();
this._beforeAnimation();
// for the root animation only
// set the async TRANSITION END event
// and run onFinishes when the transition ends
const dur = self.getDuration(opts);
if (self._isAsync) {
self._asyncEnd(dur, true);
const dur = this.getDuration(opts);
if (this._isAsync) {
this._asyncEnd(dur, true);
}
// ******** DOM WRITE ****************
self._playProgress(opts);
this._playProgress(opts);
if (self._isAsync && !this._destroyed) {
if (this._isAsync && !this._destroyed) {
// this animation has a duration so we need another RAF
// for the CSS TRANSITION properties to kick in
raf(() => {
self._playToStep(1);
this._playToStep(1);
});
}
}
@@ -721,7 +718,7 @@ export class Animator {
} else {
for (j = 0; j < elements.length; j++) {
// ******** DOM WRITE ****************
(elements[j].style as any)[prop] = val;
elements[j].style.setProperty(prop, val);
}
}
}
@@ -759,7 +756,6 @@ export class Animator {
for (const { style } of elements) {
if (dur > 0) {
// ******** DOM WRITE ****************
style.transform = '';
style.transitionDuration = durString;
// each animation can have a different easing
@@ -768,7 +764,7 @@ export class Animator {
style.transitionTimingFunction = easing;
}
} else {
style.transform = 'none';
style.transitionDuration = '0';
}
}
}

View File

@@ -68,10 +68,7 @@ ion-backdrop {
touch-action: manipulation;
}
.menu-content-open ion-pane,
.menu-content-open .ion-pane,
.menu-content-open ion-content,
.menu-content-open .toolbar {
.menu-content-open {
// the containing element itself should be clickable but
// everything inside of it should not clickable when menu is open
pointer-events: none;

View File

@@ -1,52 +1,8 @@
import { QueueApi } from '@stencil/core';
import { PanRecognizer } from './recognizers';
import { gestureController } from './gesture-controller';
import { PointerEvents } from './pointer-events';
export interface GestureDetail {
type: string;
startX: number;
startY: number;
startTimeStamp: number;
currentX: number;
currentY: number;
velocityX: number;
velocityY: number;
deltaX: number;
deltaY: number;
timeStamp: number;
event: UIEvent;
data?: any;
}
export type GestureCallback = (detail?: GestureDetail) => boolean | void;
export interface Gesture {
setDisabled(disabled: boolean): void;
destroy(): void;
}
export interface GestureConfig {
el: Node;
disableScroll?: boolean;
queue: QueueApi;
direction?: 'x' | 'y';
gestureName: string;
gesturePriority?: number;
passive?: boolean;
maxAngle?: number;
threshold?: number;
canStart?: GestureCallback;
onWillStart?: (_: GestureDetail) => Promise<void>;
onStart?: GestureCallback;
onMove?: GestureCallback;
onEnd?: GestureCallback;
notCaptured?: GestureCallback;
}
import { createPointerEvents } from './pointer-events';
import { createPanRecognizer } from './recognizers';
export function createGesture(config: GestureConfig): Gesture {
const finalConfig = {
@@ -60,11 +16,6 @@ export function createGesture(config: GestureConfig): Gesture {
...config
};
let hasCapturedPan = false;
let hasStartedPan = false;
let hasFiredStart = true;
let isMoveQueued = false;
const canStart = finalConfig.canStart;
const onWillStart = finalConfig.onWillStart;
const onStart = finalConfig.onStart;
@@ -90,7 +41,7 @@ export function createGesture(config: GestureConfig): Gesture {
data: undefined
};
const pointerEvents = new PointerEvents(
const pointerEvents = createPointerEvents(
finalConfig.el,
pointerDown,
pointerMove,
@@ -100,13 +51,18 @@ export function createGesture(config: GestureConfig): Gesture {
}
);
const pan = new PanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle);
const pan = createPanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle);
const gesture = gestureController.createGesture({
name: config.gestureName,
priority: config.gesturePriority,
disableScroll: config.disableScroll
});
let hasCapturedPan = false;
let hasStartedPan = false;
let hasFiredStart = true;
let isMoveQueued = false;
function pointerDown(ev: UIEvent): boolean {
const timeStamp = now(ev);
if (hasStartedPan || !hasFiredStart) {
@@ -146,14 +102,14 @@ export function createGesture(config: GestureConfig): Gesture {
if (hasCapturedPan) {
if (!isMoveQueued && hasFiredStart) {
isMoveQueued = true;
calcGestureData(ev);
calcGestureData(detail, ev);
queue.write(fireOnMove);
}
return;
}
// gesture is currently being detected
calcGestureData(ev);
calcGestureData(detail, ev);
if (pan.detect(detail.currentX, detail.currentY)) {
if (pan.isGesture()) {
if (!tryToCapturePan()) {
@@ -175,28 +131,6 @@ export function createGesture(config: GestureConfig): Gesture {
}
}
function calcGestureData(ev: UIEvent) {
const prevX = detail.currentX;
const prevY = detail.currentY;
const prevT = detail.timeStamp;
updateDetail(ev, detail);
const currentX = detail.currentX;
const currentY = detail.currentY;
const timestamp = detail.timeStamp = now(ev);
const timeDelta = timestamp - prevT;
if (timeDelta > 0 && timeDelta < 100) {
const velocityX = (currentX - prevX) / timeDelta;
const velocityY = (currentY - prevY) / timeDelta;
detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
}
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.event = ev;
}
function tryToCapturePan(): boolean {
if (gesture && !gesture.capture()) {
return false;
@@ -256,7 +190,7 @@ export function createGesture(config: GestureConfig): Gesture {
if (!tmpHasFiredStart) {
return;
}
calcGestureData(ev);
calcGestureData(detail, ev);
// Try to capture press
if (tmpHasCaptured) {
@@ -274,7 +208,7 @@ export function createGesture(config: GestureConfig): Gesture {
return {
setDisabled(disabled: boolean) {
pointerEvents.disabled = disabled;
pointerEvents.setDisabled(disabled);
},
destroy() {
gesture.destroy();
@@ -283,6 +217,28 @@ export function createGesture(config: GestureConfig): Gesture {
};
}
function calcGestureData(detail: GestureDetail, ev: UIEvent) {
const prevX = detail.currentX;
const prevY = detail.currentY;
const prevT = detail.timeStamp;
updateDetail(ev, detail);
const currentX = detail.currentX;
const currentY = detail.currentY;
const timestamp = detail.timeStamp = now(ev);
const timeDelta = timestamp - prevT;
if (timeDelta > 0 && timeDelta < 100) {
const velocityX = (currentX - prevX) / timeDelta;
const velocityY = (currentY - prevY) / timeDelta;
detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
}
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.event = ev;
}
function updateDetail(ev: any, detail: GestureDetail) {
// get X coordinates for either a mouse click
// or a touch depending on the given event
@@ -306,3 +262,46 @@ function updateDetail(ev: any, detail: GestureDetail) {
function now(ev: UIEvent) {
return ev.timeStamp || Date.now();
}
export interface GestureDetail {
type: string;
startX: number;
startY: number;
startTimeStamp: number;
currentX: number;
currentY: number;
velocityX: number;
velocityY: number;
deltaX: number;
deltaY: number;
timeStamp: number;
event: UIEvent;
data?: any;
}
export type GestureCallback = (detail?: GestureDetail) => boolean | void;
export interface Gesture {
setDisabled(disabled: boolean): void;
destroy(): void;
}
export interface GestureConfig {
el: Node;
disableScroll?: boolean;
queue: QueueApi;
direction?: 'x' | 'y';
gestureName: string;
gesturePriority?: number;
passive?: boolean;
maxAngle?: number;
threshold?: number;
canStart?: GestureCallback;
onWillStart?: (_: GestureDetail) => Promise<void>;
onStart?: GestureCallback;
onMove?: GestureCallback;
onEnd?: GestureCallback;
notCaptured?: GestureCallback;
}

View File

@@ -2,141 +2,134 @@ import { addEventListener } from './listener';
const MOUSE_WAIT = 2000;
export class PointerEvents {
export function createPointerEvents(
el: Node,
pointerDown: any,
pointerMove: any,
pointerUp: any,
options: EventListenerOptions
) {
private rmTouchStart?: () => void;
private rmTouchMove?: () => void;
private rmTouchEnd?: () => void;
private rmTouchCancel?: () => void;
let rmTouchStart: (() => void) | undefined;
let rmTouchMove: (() => void) | undefined;
let rmTouchEnd: (() => void) | undefined;
let rmTouchCancel: (() => void) | undefined;
let rmMouseStart: (() => void) | undefined;
let rmMouseMove: (() => void) | undefined;
let rmMouseUp: (() => void) | undefined;
let lastTouchEvent = 0;
private rmMouseStart?: () => void;
private rmMouseMove?: () => void;
private rmMouseUp?: () => void;
private bindTouchEnd: any;
private bindMouseUp: any;
private lastTouchEvent = 0;
constructor(
private el: Node,
private pointerDown: any,
private pointerMove: any,
private pointerUp: any,
private options: EventListenerOptions
) {
this.bindTouchEnd = this.handleTouchEnd.bind(this);
this.bindMouseUp = this.handleMouseUp.bind(this);
}
set disabled(disabled: boolean) {
if (disabled) {
if (this.rmTouchStart) {
this.rmTouchStart();
}
if (this.rmMouseStart) {
this.rmMouseStart();
}
this.rmTouchStart = this.rmMouseStart = undefined;
this.stop();
} else {
if (!this.rmTouchStart) {
this.rmTouchStart = addEventListener(this.el, 'touchstart', this.handleTouchStart.bind(this), this.options);
}
if (!this.rmMouseStart) {
this.rmMouseStart = addEventListener(this.el, 'mousedown', this.handleMouseDown.bind(this), this.options);
}
}
}
stop() {
this.stopTouch();
this.stopMouse();
}
destroy() {
this.disabled = true;
this.pointerUp = this.pointerMove = this.pointerDown = undefined;
}
private handleTouchStart(ev: any) {
this.lastTouchEvent = Date.now() + MOUSE_WAIT;
if (!this.pointerDown(ev, POINTER_EVENT_TYPE_TOUCH)) {
function handleTouchStart(ev: any) {
lastTouchEvent = Date.now() + MOUSE_WAIT;
if (!pointerDown(ev)) {
return;
}
if (!this.rmTouchMove && this.pointerMove) {
this.rmTouchMove = addEventListener(this.el, 'touchmove', this.pointerMove, this.options);
if (!rmTouchMove && pointerMove) {
rmTouchMove = addEventListener(el, 'touchmove', pointerMove, options);
}
if (!this.rmTouchEnd) {
this.rmTouchEnd = addEventListener(this.el, 'touchend', this.bindTouchEnd, this.options);
if (!rmTouchEnd) {
rmTouchEnd = addEventListener(el, 'touchend', handleTouchEnd, options);
}
if (!this.rmTouchCancel) {
this.rmTouchCancel = addEventListener(this.el, 'touchcancel', this.bindTouchEnd, this.options);
if (!rmTouchCancel) {
rmTouchCancel = addEventListener(el, 'touchcancel', handleTouchEnd, options);
}
}
private handleMouseDown(ev: any) {
if (this.lastTouchEvent > Date.now()) {
function handleMouseDown(ev: any) {
if (lastTouchEvent > Date.now()) {
console.debug('mousedown event dropped because of previous touch');
return;
}
if (!this.pointerDown(ev, POINTER_EVENT_TYPE_MOUSE)) {
if (!pointerDown(ev)) {
return;
}
if (!this.rmMouseMove && this.pointerMove) {
this.rmMouseMove = addEventListener(getDocument(this.el), 'mousemove', this.pointerMove, this.options);
if (!rmMouseMove && pointerMove) {
rmMouseMove = addEventListener(getDocument(el), 'mousemove', pointerMove, options);
}
if (!this.rmMouseUp) {
this.rmMouseUp = addEventListener(getDocument(this.el), 'mouseup', this.bindMouseUp, this.options);
if (!rmMouseUp) {
rmMouseUp = addEventListener(getDocument(el), 'mouseup', handleMouseUp, options);
}
}
private handleTouchEnd(ev: any) {
this.stopTouch();
if (this.pointerUp) {
this.pointerUp(ev, POINTER_EVENT_TYPE_TOUCH);
function handleTouchEnd(ev: any) {
stopTouch();
if (pointerUp) {
pointerUp(ev);
}
}
private handleMouseUp(ev: any) {
this.stopMouse();
if (this.pointerUp) {
this.pointerUp(ev, POINTER_EVENT_TYPE_MOUSE);
function handleMouseUp(ev: any) {
stopMouse();
if (pointerUp) {
pointerUp(ev);
}
}
private stopTouch() {
if (this.rmTouchMove) {
this.rmTouchMove();
function stopTouch() {
if (rmTouchMove) {
rmTouchMove();
}
if (this.rmTouchEnd) {
this.rmTouchEnd();
if (rmTouchEnd) {
rmTouchEnd();
}
if (this.rmTouchCancel) {
this.rmTouchCancel();
if (rmTouchCancel) {
rmTouchCancel();
}
this.rmTouchMove = this.rmTouchEnd = this.rmTouchCancel = undefined;
rmTouchMove = rmTouchEnd = rmTouchCancel = undefined;
}
private stopMouse() {
if (this.rmMouseMove) {
this.rmMouseMove();
function stopMouse() {
if (rmMouseMove) {
rmMouseMove();
}
if (this.rmMouseUp) {
this.rmMouseUp();
if (rmMouseUp) {
rmMouseUp();
}
this.rmMouseMove = this.rmMouseUp = undefined;
rmMouseMove = rmMouseUp = undefined;
}
function stop() {
stopTouch();
stopMouse();
}
function setDisabled(disabled: boolean) {
if (disabled) {
if (rmTouchStart) {
rmTouchStart();
}
if (rmMouseStart) {
rmMouseStart();
}
rmTouchStart = rmMouseStart = undefined;
stop();
} else {
if (!rmTouchStart) {
rmTouchStart = addEventListener(el, 'touchstart', handleTouchStart, options);
}
if (!rmMouseStart) {
rmMouseStart = addEventListener(el, 'mousedown', handleMouseDown, options);
}
}
}
function destroy() {
setDisabled(true);
pointerUp = pointerMove = pointerDown = undefined;
}
return {
setDisabled,
stop,
destroy
};
}
function getDocument(node: Node) {
return node instanceof Document ? node : node.ownerDocument;
}
export const POINTER_EVENT_TYPE_MOUSE = 1;
export const POINTER_EVENT_TYPE_TOUCH = 2;
export interface PointerEventsConfig {
element?: HTMLElement;
pointerDown: (ev: any) => boolean;

View File

@@ -1,63 +1,66 @@
export class PanRecognizer {
private startX!: number;
private startY!: number;
private dirty = false;
private threshold: number;
private maxCosine: number;
private isDirX: boolean;
private isPan = 0;
constructor(direction: string, threshold: number, maxAngle: number) {
const radians = maxAngle * (Math.PI / 180);
this.isDirX = direction === 'x';
this.maxCosine = Math.cos(radians);
this.threshold = threshold * threshold;
}
start(x: number, y: number) {
this.startX = x;
this.startY = y;
this.isPan = 0;
this.dirty = true;
}
detect(x: number, y: number): boolean {
if (!this.dirty) {
return false;
}
const deltaX = (x - this.startX);
const deltaY = (y - this.startY);
const distance = deltaX * deltaX + deltaY * deltaY;
if (distance < this.threshold) {
return false;
}
const hypotenuse = Math.sqrt(distance);
const cosine = (this.isDirX ? deltaX : deltaY) / hypotenuse;
if (cosine > this.maxCosine) {
this.isPan = 1;
} else if (cosine < -this.maxCosine) {
this.isPan = -1;
} else {
this.isPan = 0;
}
this.dirty = false;
return true;
}
isGesture(): boolean {
return this.isPan !== 0;
}
getDirection(): number {
return this.isPan;
}
export interface PanRecognizer {
start(x: number, y: number): void;
detect(x: number, y: number): boolean;
isGesture(): boolean;
getDirection(): number;
}
export function createPanRecognizer(direction: string, thresh: number, maxAngle: number): PanRecognizer {
const radians = maxAngle * (Math.PI / 180);
const isDirX = direction === 'x';
const maxCosine = Math.cos(radians);
const threshold = thresh * thresh;
let startX = 0;
let startY = 0;
let dirty = false;
let isPan = 0;
return {
start(x: number, y: number) {
startX = x;
startY = y;
isPan = 0;
dirty = true;
},
detect(x: number, y: number): boolean {
if (!dirty) {
return false;
}
const deltaX = (x - startX);
const deltaY = (y - startY);
const distance = deltaX * deltaX + deltaY * deltaY;
if (distance < threshold) {
return false;
}
const hypotenuse = Math.sqrt(distance);
const cosine = (isDirX ? deltaX : deltaY) / hypotenuse;
if (cosine > maxCosine) {
isPan = 1;
} else if (cosine < -maxCosine) {
isPan = -1;
} else {
isPan = 0;
}
dirty = false;
return true;
},
isGesture(): boolean {
return isPan !== 0;
},
getDirection(): number {
return isPan;
}
};
}