From bfe533f811b16eb83ffa1e9712465a7d5b7e8c17 Mon Sep 17 00:00:00 2001 From: Manuel Mtz-Almeida Date: Wed, 29 Mar 2017 14:36:49 +0200 Subject: [PATCH] fix(content): scroll listener is auto enabled fixes #10938 --- src/components/content/content.ts | 64 +++++++++++++++++++++++++------ src/util/events.ts | 4 +- src/util/scroll-view.ts | 56 ++++++++++++--------------- 3 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/components/content/content.ts b/src/components/content/content.ts index a9235064cbb..bb0e74b9967 100644 --- a/src/components/content/content.ts +++ b/src/components/content/content.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core'; import { App } from '../app/app'; import { Config } from '../../config/config'; @@ -125,7 +125,7 @@ export { ScrollEvent } from '../../util/scroll-view'; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }) -export class Content extends Ion implements OnDestroy { +export class Content extends Ion implements OnDestroy, AfterViewInit { /** @internal */ _cTop: number; /** @internal */ @@ -311,18 +311,37 @@ export class Content extends Ion implements OnDestroy { /** * @output {ScrollEvent} Emitted when the scrolling first starts. + * If it is used programatically, scroll events must be enabled explicitally with `enableScrollListener()`: + * + * ```ts + * content.ionScrollStart((ev) => onEvent(ev)); + * content.enableScrollListener(); + * ``` + * */ @Output() ionScrollStart: EventEmitter = new EventEmitter(); /** * @output {ScrollEvent} Emitted on every scroll event. - */ - @Output() ionScroll: EventEmitter = new EventEmitter(); + * If it is used programatically, scroll events must be enabled explicitally with `enableScrollListener()`: + * + * ```ts + * content.ionScroll((ev) => onEvent(ev)); + * content.enableScrollListener(); + * ``` + * + */ @Output() ionScroll: EventEmitter = new EventEmitter(); /** * @output {ScrollEvent} Emitted when scrolling ends. - */ - @Output() ionScrollEnd: EventEmitter = new EventEmitter(); + * If it is used programatically, scroll events must be enabled explicitally with `enableScrollListener()`: + * + * ```ts + * content.ionScrollEnd((ev) => onEvent(ev)); + * content.enableScrollListener(); + * ``` + * + */ @Output() ionScrollEnd: EventEmitter = new EventEmitter(); constructor( @@ -348,7 +367,8 @@ export class Content extends Ion implements OnDestroy { // goal is to completely remove this when iOS // fully supports scroll events // listen to JS scroll events - this._scroll = new ScrollView(_plt, _dom, config.getBoolean('virtualScrollEventAssist')); + const jsScroll = config.getBoolean('virtualScrollEventAssist'); + this._scroll = new ScrollView(_app, _plt, _dom, jsScroll); while (navCtrl) { if (isTabs(navCtrl)) { @@ -383,7 +403,7 @@ export class Content extends Ion implements OnDestroy { /** * @hidden */ - enableScrollListener() { + ngAfterViewInit() { assert(this.getFixedElement(), 'fixed element was not found'); assert(this.getScrollElement(), 'scroll element was not found'); @@ -398,9 +418,6 @@ export class Content extends Ion implements OnDestroy { // subscribe to every scroll move scroll.onScroll = (ev) => { - // remind the app that it's currently scrolling - this._app.setScrolling(); - // emit to all of our other friends things be scrolling this.ionScroll.emit(ev); @@ -414,7 +431,30 @@ export class Content extends Ion implements OnDestroy { this.imgsUpdate(); }; - scroll.setEnabled(); + if (this.hasScrollObservers()) { + console.debug('Auto enabling scroll listener'); + this.enableScrollListener(); + } + } + + /** + * @hidden + */ + hasScrollObservers(): boolean { + return this.ionScroll.observers.length > 0 || + this.ionScrollStart.observers.length > 0 || + this.ionScrollEnd.observers.length > 0; + } + + /** + * Enables scroll events listening. By default, scroll events are disabled, so + * subscribing to `(ionScroll)`, `(ionScrollStart)` and `(ionScrollEnd)` will not fire any event. + * + * Listening for scroll events is an expensive operation, that runs at 60FPS. + * So it is a good idea to keep it disabled as long you are not using them. + */ + enableScrollListener() { + this._scroll.eventsEnabled = true; } /** diff --git a/src/util/events.ts b/src/util/events.ts index ce682e0b404..077c780b74d 100644 --- a/src/util/events.ts +++ b/src/util/events.ts @@ -137,8 +137,8 @@ export function setupEvents(plt: Platform, dom: DomController): Events { let contentEle = el.closest('.scroll-content'); if (contentEle) { var style = contentEle.style; - var scroll = new ScrollView(plt, dom, false); - scroll.init(contentEle, 0, 0); + var scroll = new ScrollView(null, plt, dom, false); + scroll._el = contentEle; // We need to stop scrolling if it's happening and scroll up style['WebkitBackfaceVisibility'] = 'hidden'; diff --git a/src/util/scroll-view.ts b/src/util/scroll-view.ts index d733d5f022a..bf60fe65f6b 100644 --- a/src/util/scroll-view.ts +++ b/src/util/scroll-view.ts @@ -1,5 +1,6 @@ import { assert } from './util'; +import { App } from '../components/app/app'; import { DomController, DomCallback } from '../platform/dom-controller'; import { Platform, EventListenerOptions } from '../platform/platform'; import { pointerCoord } from './dom'; @@ -12,11 +13,11 @@ export class ScrollView { onScroll: (ev: ScrollEvent) => void; onScrollEnd: (ev: ScrollEvent) => void; initialized: boolean = false; - enabled: boolean = false; + eventsEnabled: boolean = false; contentTop: number; contentBottom: number; - private _el: HTMLElement; + _el: HTMLElement; private _js: boolean; private _t: number = 0; private _l: number = 0; @@ -25,6 +26,7 @@ export class ScrollView { constructor( + private _app: App, private _plt: Platform, private _dom: DomController, virtualScrollEventAssist: boolean @@ -60,35 +62,19 @@ export class ScrollView { if (!this.initialized) { this.initialized = true; - - if (this.enabled) { - this.enable(); - } - } - } - - setEnabled() { - if (!this.enabled) { - this.enabled = true; - if (this.initialized) { - this.enable(); + if (this._js) { + this.enableJsScroll(); + } else { + this.enableNativeScrolling(); } } } - enable() { - assert(this.initialized, 'scroll must be initialized'); - assert(this.enabled, 'scroll-view must be enabled'); - assert(this._el, 'scroll-view, element can not be null'); - - if (this._js) { - this.enableJsScroll(); - } else { - this.enableNativeScrolling(); - } - } - private enableNativeScrolling() { + assert(this.onScrollStart, 'onScrollStart is not defined'); + assert(this.onScroll, 'onScroll is not defined'); + assert(this.onScrollEnd, 'onScrollEnd is not defined'); + this._js = false; if (!this._el) { return; @@ -101,6 +87,14 @@ export class ScrollView { const positions: number[] = []; function scrollCallback(scrollEvent: UIEvent) { + // remind the app that it's currently scrolling + self._app.setScrolling(); + + // if events are disabled, we do nothing + if (!self.eventsEnabled) { + return; + } + ev.timeStamp = scrollEvent.timeStamp; // Event.timeStamp is 0 in firefox if (!ev.timeStamp) { @@ -151,13 +145,12 @@ export class ScrollView { if (startPos !== endPos) { // compute relative movement between these two points - var timeOffset = (positions[endPos] - positions[startPos]); var movedTop = (positions[startPos - 2] - positions[endPos - 2]); var movedLeft = (positions[startPos - 1] - positions[endPos - 1]); - + var factor = FRAME_MS / (positions[endPos] - positions[startPos]); // based on XXms compute the movement to apply for each render step - ev.velocityY = ((movedTop / timeOffset) * FRAME_MS); - ev.velocityX = ((movedLeft / timeOffset) * FRAME_MS); + ev.velocityY = movedTop * factor; + ev.velocityX = movedLeft * factor; // figure out which direction we're scrolling ev.directionY = (movedTop > 0 ? 'up' : 'down'); @@ -546,11 +539,10 @@ export class ScrollView { this._endTmr && this._dom.cancel(this._endTmr); this._lsn && this._lsn(); - this.onScrollStart = this.onScroll = this.onScrollEnd = null; - let ev = this.ev; ev.domWrite = ev.contentElement = ev.fixedElement = ev.scrollElement = ev.headerElement = null; this._lsn = this._el = this._dom = this.ev = ev = null; + this.onScrollStart = this.onScroll = this.onScrollEnd = null; } }