perf(scroll): watchdog + simplification

This commit is contained in:
Manu Mtz.-Almeida
2018-03-02 16:04:27 +01:00
parent c1f1c28aef
commit 33a6274613
15 changed files with 183 additions and 265 deletions

View File

@ -42,20 +42,16 @@ import {
GestureCallback,
GestureDetail,
} from './components/gesture/gesture';
import {
App,
FrameworkDelegate as FrameworkDelegate2,
} from '.';
import {
PickerButton,
PickerColumn as PickerColumn2,
} from './components/picker/picker';
import {
ScrollCallback,
} from './components/scroll/scroll';
import {
SelectPopoverOption,
} from './components/select-popover/select-popover';
import {
FrameworkDelegate as FrameworkDelegate2,
} from '.';
import {
DomRenderFn,
HeaderFn,
@ -770,6 +766,7 @@ declare global {
export interface IonContentAttributes extends HTMLAttributes {
forceOverscroll?: boolean;
fullscreen?: boolean;
scrollEvents?: boolean;
}
}
}
@ -1240,7 +1237,7 @@ declare global {
}
namespace JSXElements {
export interface IonInputShimsAttributes extends HTMLAttributes {
app?: App;
}
}
}
@ -2706,9 +2703,7 @@ declare global {
export interface IonScrollAttributes extends HTMLAttributes {
forceOverscroll?: boolean;
mode?: string;
onionScroll?: ScrollCallback;
onionScrollEnd?: ScrollCallback;
onionScrollStart?: ScrollCallback;
scrollEvents?: boolean;
}
}
}

View File

@ -4,7 +4,6 @@ import { Config, NavEvent, OverlayController, PublicNav, PublicViewController }
import { getOrAppendElement } from '../../utils/helpers';
const rootNavs = new Map<number, HTMLIonNavElement>();
const ACTIVE_SCROLLING_TIME = 100;
let backButtonActions: BackButtonAction[] = [];
@Component({
@ -21,11 +20,9 @@ export class App {
private isDevice = false;
private deviceHacks = false;
private scrollTime = 0;
externalNavPromise: void | Promise<any> = null;
externalNavOccuring = false;
didScroll = false;
@Element() element: HTMLElement;
@Event() exitApp: EventEmitter<ExitAppEventDetail>;
@ -102,29 +99,6 @@ export class App {
return true;
}
/**
* Boolean if the app is actively scrolling or not.
* @return {boolean} returns true or false
*/
@Method()
isScrolling(): boolean {
const scrollTime = this.scrollTime;
if (scrollTime === 0) {
return false;
}
if (scrollTime < Date.now()) {
this.scrollTime = 0;
return false;
}
return true;
}
@Method()
setScrolling() {
this.scrollTime = Date.now() + ACTIVE_SCROLLING_TIME;
this.didScroll = true;
}
@Method()
getTopNavs(rootNavId = -1): PublicNav[] {
return getTopNavsImpl(rootNavId);
@ -142,7 +116,6 @@ export class App {
return null;
}
/**
* The back button event is triggered when the user presses the native
* platform's back button, also referred to as the "hardware" back button.
@ -244,7 +217,7 @@ export class App {
render() {
return [
<ion-platform />,
this.deviceHacks && <ion-input-shims app={this} />,
this.deviceHacks && <ion-input-shims />,
this.isDevice && <ion-tap-click />,
this.isDevice && <ion-status-tap />,
<slot></slot>

View File

@ -42,11 +42,6 @@ Returns an array of top level Navs
Returns whether the application is enabled or not
#### isScrolling()
Boolean if the app is actively scrolling or not.
#### registerBackButtonAction()
The back button event is triggered when the user presses the native
@ -69,9 +64,6 @@ This API is not meant for public usage and could
change at any time
#### setScrolling()
#### updateExternalNavOccuring()
Updates whether an external navigation event is occuring

View File

@ -35,6 +35,9 @@ export class Content {
*/
@Prop() forceOverscroll: boolean;
@Prop() scrollEvents = false;
@Listen('body:ionNavChanged')
onNavChanged() {
this.resize();
@ -128,7 +131,10 @@ export class Content {
this.resize();
return [
<ion-scroll mode={this.mode} forceOverscroll={this.forceOverscroll}>
<ion-scroll
mode={this.mode}
scrollEvents={this.scrollEvents}
forceOverscroll={this.forceOverscroll}>
<slot></slot>
</ion-scroll>,
<slot name='fixed'></slot>

View File

@ -34,6 +34,11 @@ and footers. This effect can easily be seen by setting the toolbar
to transparent.
#### scrollEvents
boolean
## Attributes
#### force-overscroll
@ -54,6 +59,11 @@ and footers. This effect can easily be seen by setting the toolbar
to transparent.
#### scroll-events
boolean
## Methods
#### scrollByPoint()

View File

@ -69,6 +69,13 @@
</ion-footer>
<script>
const content = document.getElementById('content');
content.scrollEvents = true;
content.addEventListener('ionScrollStart', () => console.log('scroll start'));
content.addEventListener('ionScroll', (ev) => console.log('scroll', ev.detail));
content.addEventListener('ionScrollEnd', () => console.log('scroll end'));
function toggleFullscreen() {
const content = document.getElementById('content');
content.fullscreen = !content.fullscreen;

View File

@ -1,5 +1,5 @@
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, State, Watch } from '@stencil/core';
import { DomController, ScrollDetail } from '../../index';
import { DomController } from '../../index';
const enum Position {
Top = 'top',
@ -106,10 +106,9 @@ export class InfiniteScroll {
this.scrollEl = null;
}
@Listen('ionScroll', {enabled: false})
protected onScroll(ev: CustomEvent) {
@Listen('scroll', {enabled: false})
protected onScroll() {
const scrollEl = this.scrollEl;
const detail = ev.detail as ScrollDetail;
if (!scrollEl || !this.canStart()) {
return 1;
}
@ -119,7 +118,7 @@ export class InfiniteScroll {
// if there is no height of this element then do nothing
return 2;
}
const scrollTop = detail.scrollTop;
const scrollTop = scrollEl.scrollTop;
const scrollHeight = scrollEl.scrollHeight;
const height = scrollEl.offsetHeight;
const threshold = this.thrPc ? (height * this.thrPc) : this.thrPx;
@ -221,7 +220,7 @@ export class InfiniteScroll {
private enableScrollEvents(shouldListen: boolean) {
if (this.scrollEl) {
this.enableListener(this, 'ionScroll', shouldListen, this.scrollEl);
this.enableListener(this, 'scroll', shouldListen, this.scrollEl);
}
}

View File

@ -1,11 +1,15 @@
import { App } from '../../..';
const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
export default function enableInputBlurring(app: App) {
export default function enableInputBlurring() {
console.debug('Input: enableInputBlurring');
let focused = true;
let didScroll = false;
function onScroll() {
didScroll = true;
}
function onFocusin() {
focused = true;
@ -13,8 +17,8 @@ export default function enableInputBlurring(app: App) {
function onTouchend(ev: any) {
// if app did scroll return early
if (app.didScroll) {
app.didScroll = false;
if (didScroll) {
didScroll = false;
return;
}
const active = document.activeElement as HTMLElement;
@ -49,10 +53,12 @@ export default function enableInputBlurring(app: App) {
}, 50);
}
document.addEventListener('ionScrollStart', onScroll);
document.addEventListener('focusin', onFocusin, true);
document.addEventListener('touchend', onTouchend, false);
return () => {
document.removeEventListener('ionScrollStart', onScroll, true);
document.removeEventListener('focusin', onFocusin, true);
document.removeEventListener('touchend', onTouchend, false);
};

View File

@ -1,5 +1,5 @@
import { Component, Listen, Prop } from '@stencil/core';
import { App, Config } from '../..';
import { Config } from '../..';
import enableHideCaretOnScroll from './hacks/hide-caret';
import enableInputBlurring from './hacks/input-blurring';
@ -24,7 +24,6 @@ export class InputShims {
private scrollAssistMap = new WeakMap<HTMLElement, Function>();
@Prop({context: 'config'}) config: Config;
@Prop() app: App;
componentDidLoad() {
this.keyboardHeight = this.config.getNumber('keyboardHeight', 290);
@ -33,7 +32,7 @@ export class InputShims {
const inputBlurring = this.config.getBoolean('inputBlurring', true);
if (inputBlurring && INPUT_BLURRING) {
enableInputBlurring(this.app);
enableInputBlurring();
}
const scrollPadding = this.config.getBoolean('scrollPadding', true);

View File

@ -5,20 +5,6 @@
<!-- Auto Generated Below -->
## Properties
#### app
## Attributes
#### app
----------------------------------------------

View File

@ -21,19 +21,9 @@ Note, the does not disable the system bounce on iOS. That is an OS level setting
string
#### onionScroll
#### onionScrollEnd
#### onionScrollStart
#### scrollEvents
boolean
## Attributes
@ -52,26 +42,17 @@ Note, the does not disable the system bounce on iOS. That is an OS level setting
string
#### onion-scroll
#### onion-scroll-end
#### onion-scroll-start
#### scroll-events
boolean
## Events
#### ionScroll
Emitted while scrolling.
Emitted while scrolling. This event is disabled by default.
Look at the property: `scrollEvents`
#### ionScrollEnd

View File

@ -14,9 +14,6 @@ ion-scroll {
contain: size style layout;
margin: 0 !important; // scss-lint:disable all
padding: 0 !important; // scss-lint:disable all
.focus-input {
@include margin(0);
@include padding(0);

View File

@ -1,6 +1,5 @@
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop } from '@stencil/core';
import { Config, DomController, GestureDelegate, GestureDetail } from '../../index';
import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import { Config, DomController, GestureDetail } from '../../index';
@Component({
tag: 'ion-scroll',
@ -14,19 +13,14 @@ import { Config, DomController, GestureDelegate, GestureDetail } from '../../ind
})
export class Scroll {
private gesture: GestureDelegate;
private positions: number[] = [];
private tmr: any;
private queued = false;
private app: HTMLIonAppElement;
private watchDog: any;
private isScrolling = false;
private detail: ScrollDetail = {};
private lastScroll = 0;
private detail: ScrollDetail;
private queued = false;
@Element() private el: HTMLElement;
@Prop({ connect: 'ion-gesture-controller'}) gestureCtrl: HTMLIonGestureControllerElement;
@Prop({ context: 'config'}) config: Config;
@Prop({ context: 'enableListener'}) enableListener: EventListenerEnable;
@Prop({ context: 'dom' }) dom: DomController;
@Prop({ context: 'isServer' }) isServer: boolean;
@ -40,69 +34,76 @@ export class Scroll {
*/
@Prop({mutable: true}) forceOverscroll: boolean;
@Prop() onionScrollStart: ScrollCallback;
@Prop() onionScroll: ScrollCallback;
@Prop() onionScrollEnd: ScrollCallback;
@Prop() scrollEvents = false;
/**
* Emitted when the scroll has started.
*/
@Event() ionScrollStart: EventEmitter;
@Event() ionScrollStart: EventEmitter<ScrollBaseDetail>;
/**
* Emitted while scrolling.
* Emitted while scrolling. This event is disabled by default.
* Look at the property: `scrollEvents`
*/
@Event({bubbles: false}) ionScroll: EventEmitter;
@Event({bubbles: false}) ionScroll: EventEmitter<ScrollDetail>;
/**
* Emitted when the scroll has ended.
*/
@Event() ionScrollEnd: EventEmitter;
@Event() ionScrollEnd: EventEmitter<ScrollBaseDetail>;
componentWillLoad() {
return this.gestureCtrl.create({
name: 'scroll',
priority: 100,
disableScroll: false,
}).then((gesture) => {
this.gesture = gesture;
});
constructor() {
// Detail is used in a hot loop in the scroll event, by allocating it here
// V8 will be able to inline any read/write to it since it's a monomorphic class.
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
this.detail = {
positions: [],
scrollTop: 0,
scrollLeft: 0,
type: 'scroll',
event: undefined,
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
data: undefined
};
}
componentDidLoad() {
componentWillLoad() {
if (this.isServer) {
return;
}
if (this.forceOverscroll === undefined) {
this.forceOverscroll = this.mode === 'ios' && ('ontouchstart' in window);
}
this.app = this.el.closest('ion-app');
}
componentDidUnload() {
this.gesture && this.gesture.destroy();
this.gesture = this.detail = this.detail.event = null;
}
// Native Scroll *************************
@Listen('scroll', { passive: true })
onNativeScroll() {
if (!this.queued) {
onScroll(ev: UIEvent) {
const timeStamp = Date.now();
const didStart = !this.isScrolling;
this.lastScroll = timeStamp;
if (didStart) {
this.onScrollStart();
}
if (!this.queued && this.scrollEvents) {
this.queued = true;
this.dom.read(timeStamp => {
this.queued = false;
this.onScroll(timeStamp);
this.detail.event = ev;
updateScrollDetail(this.detail, this.el, timeStamp, didStart);
this.ionScroll.emit(this.detail);
});
}
}
@Listen('touchend', {passive: true, capture: true })
onTouchEnd(_ev: TouchEvent) {
this.refocus();
}
@Method()
scrollToTop(duration: number): Promise<void> {
return this.scrollToPoint(0, 0, duration);
@ -212,115 +213,29 @@ export class Scroll {
return promise;
}
private onScroll(timeStamp: number) {
const detail = this.detail;
const positions = this.positions;
const el = this.el;
if (this.app) {
this.app.setScrolling();
}
detail.timeStamp = timeStamp;
// get the current scrollTop
// ******** DOM READ ****************
detail.scrollTop = el.scrollTop;
// get the current scrollLeft
// ******** DOM READ ****************
detail.scrollLeft = el.scrollLeft;
if (!this.isScrolling) {
// currently not scrolling, so this is a scroll start
private onScrollStart() {
this.isScrolling = true;
// remember the start positions
detail.startY = detail.scrollTop;
detail.startX = detail.scrollLeft;
// new scroll, so do some resets
detail.velocityY = detail.velocityX = detail.deltaY = detail.deltaX = positions.length = 0;
// emit only on the first scroll event
if (this.onionScrollStart) {
this.onionScrollStart(detail);
}
this.gesture.capture();
this.ionScrollStart.emit(detail);
}
detail.deltaY = (detail.scrollTop - detail.startY);
detail.deltaX = (detail.scrollLeft - detail.startX);
// actively scrolling
positions.push(detail.scrollTop, detail.scrollLeft, detail.timeStamp);
// move pointer to position measured 100ms ago
const timeRange = timeStamp - 100;
let startPos = positions.length - 1;
while (startPos > 0 && positions[startPos] > timeRange) {
startPos -= 3;
}
if (startPos > 3) {
// compute relative movement between these two points
const frequency = 1 / (positions[startPos] - timeStamp);
const movedY = positions[startPos - 1] - detail.scrollLeft;
const movedX = positions[startPos - 2] - detail.scrollTop;
// based on XXms compute the movement to apply for each render step
// velocity = space/time = s*(1/t) = s*frequency
detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
} else {
detail.velocityX = 0;
detail.velocityY = 0;
}
clearTimeout(this.tmr);
this.tmr = setTimeout(() => {
// haven't scrolled in a while, so it's a scrollend
this.isScrolling = false;
this.dom.read(timeStamp => {
if (!this.isScrolling) {
this.onEnd(timeStamp);
}
this.ionScrollStart.emit({
isScrolling: true
});
}, 80);
// emit on each scroll event
if (this.onionScroll) {
this.onionScroll(detail);
} else {
this.ionScroll.emit(detail);
// watchdog
this.watchDog = setInterval(() => {
if (this.lastScroll < Date.now() - 120) {
this.onScrollEnd();
}
}, 100);
}
private onEnd(timeStamp: number) {
const detail = this.detail;
private onScrollEnd() {
detail.timeStamp = timeStamp;
// emit that the scroll has ended
this.gesture.release();
if (this.onionScrollEnd) {
this.onionScrollEnd(detail);
}
this.ionScrollEnd.emit(detail);
this.refocus();
}
private refocus() {
// TODO: renable when desktop testing is done
// setTimeout(() => {
// if (document.activeElement === document.body) {
// (this.el.querySelector('.focus-input') as HTMLElement).focus();
// }
// });
clearInterval(this.watchDog);
this.watchDog = null;
this.isScrolling = false;
this.ionScrollEnd.emit({
isScrolling: false
});
}
hostData() {
@ -339,18 +254,67 @@ export class Scroll {
</div>
];
}
}
export interface ScrollDetail extends GestureDetail {
// ******** DOM READ ****************
function updateScrollDetail(
detail: ScrollDetail,
el: HTMLElement,
timeStamp: number,
didStart: boolean
) {
const scrollTop = el.scrollTop;
const scrollLeft = el.scrollLeft;
const positions = detail.positions;
if (didStart) {
// remember the start positions
detail.startTimeStamp = timeStamp;
detail.startY = scrollTop;
detail.startX = scrollLeft;
positions.length = 0;
}
detail.timeStamp = timeStamp;
detail.currentY = detail.scrollTop = scrollTop;
detail.currentX = detail.scrollLeft = scrollLeft;
detail.deltaY = scrollTop - detail.startY;
detail.deltaX = scrollLeft - detail.startX;
// actively scrolling
positions.push(scrollTop, scrollLeft, timeStamp);
// move pointer to position measured 100ms ago
const timeRange = timeStamp - 100;
let startPos = positions.length - 1;
while (startPos > 0 && positions[startPos] > timeRange) {
startPos -= 3;
}
if (startPos > 3) {
// compute relative movement between these two points
const frequency = 1 / (positions[startPos] - timeStamp);
const movedX = positions[startPos - 1] - scrollLeft;
const movedY = positions[startPos - 2] - scrollTop;
// based on XXms compute the movement to apply for each render step
// velocity = space/time = s*(1/t) = s*frequency
detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
} else {
detail.velocityX = 0;
detail.velocityY = 0;
}
}
export interface ScrollDetail extends GestureDetail, ScrollBaseDetail {
positions?: number[];
scrollTop?: number;
scrollLeft?: number;
scrollHeight?: number;
scrollWidth?: number;
contentHeight?: number;
contentWidth?: number;
contentTop?: number;
contentBottom?: number;
}
export interface ScrollBaseDetail {
isScrolling?: boolean;
}
export interface ScrollCallback {

View File

@ -80,7 +80,7 @@ export {
} from './components/router/router-utils';
export { Row } from './components/row/row';
export { Reorder } from './components/reorder/reorder';
export { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll';
export { Scroll } from './components/scroll/scroll';
export { Searchbar } from './components/searchbar/searchbar';
export { Segment } from './components/segment/segment';
export { SegmentButton } from './components/segment-button/segment-button';

View File

@ -44,9 +44,12 @@ export function isCheckedProperty(a: any, b: any): boolean {
return (a == b); // tslint:disable-line
}
export function assert(bool: boolean, msg: string) {
if (!bool) {
console.error(msg);
export function assert(actual: any, reason: string) {
if (!actual) {
const message = 'ASSERT: ' + reason;
console.error(message);
debugger; // tslint:disable-line
throw new Error(message);
}
}
@ -143,7 +146,7 @@ export function getPageElement(el: HTMLElement) {
if (tabs) {
return tabs;
}
const page = el.closest('ion-page,.ion-page,page-inner');
const page = el.closest('ion-app,ion-page,.ion-page,page-inner');
if (page) {
return page;
}