perf(picker): improves performance of picker and datepicker

This commit is contained in:
Manu Mtz.-Almeida
2016-11-04 22:22:27 +01:00
parent 909293a735
commit fc2ee6472f
7 changed files with 194 additions and 156 deletions

View File

@ -203,3 +203,7 @@ linters:
StringQuotes: StringQuotes:
enabled: true enabled: true
style: double_quotes style: double_quotes
PropertySpelling:
extra_properties:
- contain

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, EventEmitter, Input, HostListener, Output, QueryList, Renderer, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, HostListener, NgZone, Output, QueryList, Renderer, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { cancelRaf, pointerCoord, raf } from '../../util/dom'; import { CSS, cancelRaf, pointerCoord, nativeRaf } from '../../util/dom';
import { clamp, isNumber, isPresent, isString } from '../../util/util'; import { clamp, isNumber, isPresent, isString } from '../../util/util';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
@ -11,7 +11,7 @@ import { PickerOptions, PickerColumn, PickerColumnOption } from './picker-option
import { Haptic } from '../../util/haptic'; import { Haptic } from '../../util/haptic';
import { UIEventManager } from '../../util/ui-event-manager'; import { UIEventManager } from '../../util/ui-event-manager';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { Debouncer, NativeRafDebouncer } from '../../util/debouncer';
/** /**
* @private * @private
@ -21,14 +21,9 @@ import { ViewController } from '../../navigation/view-controller';
template: template:
'<div *ngIf="col.prefix" class="picker-prefix" [style.width]="col.prefixWidth">{{col.prefix}}</div>' + '<div *ngIf="col.prefix" class="picker-prefix" [style.width]="col.prefixWidth">{{col.prefix}}</div>' +
'<div class="picker-opts" #colEle [style.width]="col.optionsWidth">' + '<div class="picker-opts" #colEle [style.width]="col.optionsWidth">' +
'<button *ngFor="let o of col.options; let i=index" [style.transform]="o._trans" ' + '<button *ngFor="let o of col.options; let i=index"' +
'[style.transitionDuration]="o._dur" ' + '[class.picker-opt-disabled]="o.disabled" ' +
'[style.webkitTransform]="o._trans" ' + 'class="picker-opt" disable-activated (click)="optClick($event, i)">' +
'[style.webkitTransitionDuration]="o._dur" ' +
'[class.picker-opt-selected]="col.selectedIndex === i" [class.picker-opt-disabled]="o.disabled" ' +
'(click)="optClick($event, i)" ' +
'type="button" ' +
'ion-button="picker-opt">' +
'{{o.text}}' + '{{o.text}}' +
'</button>' + '</button>' +
'</div>' + '</div>' +
@ -53,15 +48,24 @@ export class PickerColumnCmp {
minY: number; minY: number;
maxY: number; maxY: number;
rotateFactor: number; rotateFactor: number;
scaleFactor: number;
lastIndex: number; lastIndex: number;
lastTempIndex: number; lastTempIndex: number;
receivingEvents: boolean = false; decelerateFunc: Function;
events: UIEventManager = new UIEventManager(); debouncer: Debouncer = new NativeRafDebouncer();
events: UIEventManager = new UIEventManager(false);
@Output() ionChange: EventEmitter<any> = new EventEmitter(); @Output() ionChange: EventEmitter<any> = new EventEmitter();
constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizer, private _haptic: Haptic) { constructor(
config: Config,
private elementRef: ElementRef,
private _sanitizer: DomSanitizer,
private _zone: NgZone,
private _haptic: Haptic) {
this.rotateFactor = config.getNumber('pickerRotateFactor', 0); this.rotateFactor = config.getNumber('pickerRotateFactor', 0);
this.scaleFactor = config.getNumber('pickerScaleFactor', 1);
this.decelerateFunc = this.decelerate.bind(this);
} }
ngAfterViewInit() { ngAfterViewInit() {
@ -81,7 +85,8 @@ export class PickerColumnCmp {
elementRef: this.elementRef, elementRef: this.elementRef,
pointerDown: this.pointerStart.bind(this), pointerDown: this.pointerStart.bind(this),
pointerMove: this.pointerMove.bind(this), pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerEnd.bind(this) pointerUp: this.pointerEnd.bind(this),
capture: true
}); });
} }
@ -91,24 +96,28 @@ export class PickerColumnCmp {
pointerStart(ev: UIEvent): boolean { pointerStart(ev: UIEvent): boolean {
console.debug('picker, pointerStart', ev.type, this.startY); console.debug('picker, pointerStart', ev.type, this.startY);
this._haptic.gestureSelectionStart();
this.debouncer.debounce(() => {
// cancel any previous raf's that haven't fired yet // cancel any previous raf's that haven't fired yet
if (this.rafId) {
cancelRaf(this.rafId); cancelRaf(this.rafId);
this.rafId = null;
}
// remember where the pointer started from` // remember where the pointer started from`
this.startY = pointerCoord(ev).y; this.startY = pointerCoord(ev).y;
// reset everything // reset everything
this.receivingEvents = true;
this.velocity = 0; this.velocity = 0;
this.pos.length = 0; this.pos.length = 0;
this.pos.push(this.startY, Date.now()); this.pos.push(this.startY, Date.now());
let minY = (this.col.options.length - 1); let options = this.col.options;
let minY = (options.length - 1);
let maxY = 0; let maxY = 0;
for (var i = 0; i < options.length; i++) {
for (var i = 0; i < this.col.options.length; i++) { if (!options[i].disabled) {
if (!this.col.options[i].disabled) {
minY = Math.min(minY, i); minY = Math.min(minY, i);
maxY = Math.max(maxY, i); maxY = Math.max(maxY, i);
} }
@ -116,9 +125,7 @@ export class PickerColumnCmp {
this.minY = (minY * this.optHeight * -1); this.minY = (minY * this.optHeight * -1);
this.maxY = (maxY * this.optHeight * -1); this.maxY = (maxY * this.optHeight * -1);
});
this._haptic.gestureSelectionStart();
return true; return true;
} }
@ -126,15 +133,15 @@ export class PickerColumnCmp {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.debouncer.debounce(() => {
if (this.startY === null) { if (this.startY === null) {
return; return;
} }
let currentY = pointerCoord(ev).y;
var currentY = pointerCoord(ev).y;
this.pos.push(currentY, Date.now()); this.pos.push(currentY, Date.now());
// update the scroll position relative to pointer start position // update the scroll position relative to pointer start position
var y = this.y + (currentY - this.startY); let y = this.y + (currentY - this.startY);
if (y > this.minY) { if (y > this.minY) {
// scrolling up higher than scroll area // scrolling up higher than scroll area
@ -156,36 +163,38 @@ export class PickerColumnCmp {
if (currentIndex !== this.lastTempIndex) { if (currentIndex !== this.lastTempIndex) {
// Trigger a haptic event for physical feedback that the index has changed // Trigger a haptic event for physical feedback that the index has changed
this._haptic.gestureSelectionChanged(); this._haptic.gestureSelectionChanged();
}
this.lastTempIndex = currentIndex; this.lastTempIndex = currentIndex;
}
});
} }
pointerEnd(ev: UIEvent) { pointerEnd(ev: UIEvent) {
if (!this.receivingEvents) { this.debouncer.cancel();
if (this.startY === null) {
return; return;
} }
this.receivingEvents = false; console.debug('picker, pointerEnd', ev.type);
this.velocity = 0; this.velocity = 0;
if (this.bounceFrom > 0) { if (this.bounceFrom > 0) {
// bounce back up // bounce back up
this.update(this.minY, 100, true, true); this.update(this.minY, 100, true, true);
return;
} else if (this.bounceFrom < 0) { } else if (this.bounceFrom < 0) {
// bounce back down // bounce back down
this.update(this.maxY, 100, true, true); this.update(this.maxY, 100, true, true);
return;
}
} else if (this.startY !== null) { let endY = pointerCoord(ev).y;
var endY = pointerCoord(ev).y;
console.debug('picker, pointerEnd', ev.type, endY);
this.pos.push(endY, Date.now()); this.pos.push(endY, Date.now());
var endPos = (this.pos.length - 1); let endPos = (this.pos.length - 1);
var startPos = endPos; let startPos = endPos;
var timeRange = (Date.now() - 100); let timeRange = (Date.now() - 100);
// move pointer to position measured 100ms ago // move pointer to position measured 100ms ago
for (var i = endPos; i > 0 && this.pos[i] > timeRange; i -= 2) { for (var i = endPos; i > 0 && this.pos[i] > timeRange; i -= 2) {
@ -202,35 +211,30 @@ export class PickerColumnCmp {
} }
if (Math.abs(endY - this.startY) > 3) { if (Math.abs(endY - this.startY) > 3) {
ev.preventDefault();
ev.stopPropagation();
var y = this.y + (endY - this.startY); var y = this.y + (endY - this.startY);
this.update(y, 0, true, true); this.update(y, 0, true, true);
} }
}
this.startY = null; this.startY = null;
this.decelerate(); this.decelerate();
} }
decelerate() { decelerate() {
let y = 0; let y = 0;
cancelRaf(this.rafId);
if (isNaN(this.y) || !this.optHeight) { if (isNaN(this.y) || !this.optHeight) {
// fallback in case numbers get outta wack // fallback in case numbers get outta wack
this.update(y, 0, true, true); this.update(y, 0, true, true);
this._haptic.gestureSelectionEnd(); this._haptic.gestureSelectionEnd();
} else if (Math.abs(this.velocity) > 0) { } else if (Math.abs(this.velocity) > 0) {
// still decelerating // still decelerating
this.velocity *= DECELERATION_FRICTION; this.velocity *= DECELERATION_FRICTION;
// do not let it go slower than a velocity of 1 // do not let it go slower than a velocity of 1
this.velocity = (this.velocity > 0 ? Math.max(this.velocity, 1) : Math.min(this.velocity, -1)); this.velocity = (this.velocity > 0)
? Math.max(this.velocity, 1)
: Math.min(this.velocity, -1);
y = Math.round(this.y - this.velocity); y = Math.round(this.y - this.velocity);
@ -252,7 +256,7 @@ export class PickerColumnCmp {
if (notLockedIn) { if (notLockedIn) {
// isn't locked in yet, keep decelerating until it is // isn't locked in yet, keep decelerating until it is
this.rafId = raf(this.decelerate.bind(this)); this.rafId = nativeRaf(this.decelerateFunc);
} }
} else if (this.y % this.optHeight !== 0) { } else if (this.y % this.optHeight !== 0) {
@ -288,7 +292,10 @@ export class PickerColumnCmp {
// if there isn't a selected index, then just use the top y position // if there isn't a selected index, then just use the top y position
let y = (selectedIndex > -1) ? ((selectedIndex * this.optHeight) * -1) : 0; let y = (selectedIndex > -1) ? ((selectedIndex * this.optHeight) * -1) : 0;
if (this.rafId) {
cancelRaf(this.rafId); cancelRaf(this.rafId);
this.rafId = null;
}
this.velocity = 0; this.velocity = 0;
// so what y position we're at // so what y position we're at
@ -299,32 +306,69 @@ export class PickerColumnCmp {
// ensure we've got a good round number :) // ensure we've got a good round number :)
y = Math.round(y); y = Math.round(y);
this.col.selectedIndex = Math.max(Math.abs(Math.round(y / this.optHeight)), 0); let i, button, opt, optOffset, visible, translateX, translateY, translateZ, rotateX, transform, selected;
const parent = this.colEle.nativeElement;
const children = parent.children;
const length = children.length;
const selectedIndex = this.col.selectedIndex = Math.min(Math.max(Math.round(-y / this.optHeight), 0), length - 1);
for (var i = 0; i < this.col.options.length; i++) { const durationStr = (duration === 0) ? null : duration + 'ms';
var opt = <any>this.col.options[i]; const scaleStr = `scale(${this.scaleFactor})`;
var optTop = (i * this.optHeight);
var optOffset = (optTop + y);
var rotateX = (optOffset * this.rotateFactor); for (i = 0; i < length; i++) {
var translateX = 0; button = children[i];
var translateY = 0; opt = <any>this.col.options[i];
var translateZ = 0; optOffset = (i * this.optHeight) + y;
visible = true;
transform = '';
if (this.rotateFactor !== 0) { if (this.rotateFactor !== 0) {
translateX = 0; rotateX = optOffset * this.rotateFactor;
translateZ = 90; if (Math.abs(rotateX) > 90) {
if (rotateX > 90 || rotateX < -90) { visible = false;
translateX = -9999;
rotateX = 0;
}
} else { } else {
translateX = 0;
translateY = 0;
translateZ = 90;
transform = `rotateX(${rotateX}deg) `;
}
} else {
translateX = 0;
translateZ = 0;
translateY = optOffset; translateY = optOffset;
if (Math.abs(translateY) > 170) {
visible = false;
}
} }
opt._trans = this._sanitizer.bypassSecurityTrustStyle(`rotateX(${rotateX}deg) translate3d(${translateX}px,${translateY}px,${translateZ}px)`); selected = selectedIndex === i;
opt._dur = (duration > 0 ? duration + 'ms' : ''); if (visible) {
transform += `translate3d(0px,${translateY}px,${translateZ}px) `;
if (this.scaleFactor !== 1 && !selected) {
transform += scaleStr;
}
} else {
transform = 'translate3d(-9999px,0px,0px)';
}
// Update transition duration
if (duration !== opt._dur) {
opt._dur = duration;
button.style[CSS.transitionDuration] = durationStr;
}
// Update transform
if (transform !== opt._trans) {
opt._trans = transform;
button.style[CSS.transform] = transform;
}
// Update selected item
if (selected !== opt._selected) {
opt._selected = selected;
if (selected) {
button.classList.add(PICKER_OPT_SELECTED);
} else {
button.classList.remove(PICKER_OPT_SELECTED);
}
}
} }
if (saveY) { if (saveY) {
@ -340,7 +384,10 @@ export class PickerColumnCmp {
// new selected index has changed from the last index // new selected index has changed from the last index
// update the lastIndex and emit that it has changed // update the lastIndex and emit that it has changed
this.lastIndex = this.col.selectedIndex; this.lastIndex = this.col.selectedIndex;
this.ionChange.emit(this.col.options[this.col.selectedIndex]); var ionChange = this.ionChange;
if (ionChange.observers.length > 0) {
this._zone.run(ionChange.emit.bind(ionChange, this.col.options[this.col.selectedIndex]));
}
} }
} }
} }
@ -567,5 +614,6 @@ export class PickerCmp {
} }
let pickerIds = -1; let pickerIds = -1;
const PICKER_OPT_SELECTED = 'picker-opt-selected';
const DECELERATION_FRICTION = 0.97; const DECELERATION_FRICTION = 0.97;
const FRAME_MS = (1000 / 60); const FRAME_MS = (1000 / 60);

View File

@ -94,6 +94,8 @@ $picker-ios-option-offset-y: (($picker-ios-height - $picker-io
margin: 0; margin: 0;
padding: $picker-ios-option-padding; padding: $picker-ios-option-padding;
height: 4.6rem;
font-size: $picker-ios-option-font-size; font-size: $picker-ios-option-font-size;
line-height: $picker-ios-option-height; line-height: $picker-ios-option-height;

View File

@ -18,11 +18,10 @@ $picker-md-column-padding: 0 8px !default;
$picker-md-option-padding: 0 !default; $picker-md-option-padding: 0 !default;
$picker-md-option-text-color: $list-md-text-color !default; $picker-md-option-text-color: $list-md-text-color !default;
$picker-md-option-font-size: 18px !default; $picker-md-option-font-size: 22px !default;
$picker-md-option-height: 42px !default; $picker-md-option-height: 42px !default;
$picker-md-option-offset-y: (($picker-md-height - $picker-md-toolbar-height) / 2) - ($picker-md-option-height / 2) - 10 !default; $picker-md-option-offset-y: (($picker-md-height - $picker-md-toolbar-height) / 2) - ($picker-md-option-height / 2) - 10 !default;
$picker-md-option-selected-font-size: 22px !default;
$picker-md-option-selected-color: color($colors-md, primary) !default; $picker-md-option-selected-color: color($colors-md, primary) !default;
@ -82,14 +81,13 @@ $picker-md-option-selected-color: color($colors-md, primary) !defaul
pointer-events: none; pointer-events: none;
} }
.picker-md .picker-opts .button-effect {
display: none;
}
.picker-md .picker-opt { .picker-md .picker-opt {
margin: 0; margin: 0;
padding: $picker-md-option-padding; padding: $picker-md-option-padding;
height: 4.3rem;
font-size: $picker-md-option-font-size; font-size: $picker-md-option-font-size;
line-height: $picker-md-option-height; line-height: $picker-md-option-height;
@ -102,14 +100,9 @@ $picker-md-option-selected-color: color($colors-md, primary) !defaul
pointer-events: auto; pointer-events: auto;
} }
.picker-md .picker-opt .button-inner {
transition: 200ms;
}
.picker-md .picker-prefix, .picker-md .picker-prefix,
.picker-md .picker-suffix, .picker-md .picker-suffix,
.picker-md .picker-opt-selected { .picker-md .picker-opt.picker-opt-selected {
font-size: $picker-md-option-selected-font-size;
color: $picker-md-option-selected-color; color: $picker-md-option-selected-color;
} }

View File

@ -88,41 +88,38 @@ ion-picker-cmp {
white-space: nowrap; white-space: nowrap;
} }
// contain property is supported by Chrome
.picker-opt { .picker-opt {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
display: block;
overflow: hidden; overflow: hidden;
flex: 1;
width: 100%; width: 100%;
}
.picker-opt .button-inner {
display: block;
overflow: hidden;
text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: #000;
transition: opacity 150ms ease-in-out; will-change: transform;
contain: strict;
} }
.picker-opt.picker-opt-disabled { .picker-opt.picker-opt-disabled {
pointer-events: none; pointer-events: none;
} }
.picker-opt-disabled .button-inner { .picker-opt-disabled {
opacity: 0; opacity: 0;
} }
.picker-opts-left .button-inner { .picker-opts-left .picker-opt {
justify-content: flex-start; justify-content: flex-start;
} }
.picker-opts-right .button-inner { .picker-opts-right .picker-opt {
justify-content: flex-end; justify-content: flex-end;
} }

View File

@ -18,11 +18,10 @@ $picker-wp-column-padding: 0 4px !default;
$picker-wp-option-padding: 0 !default; $picker-wp-option-padding: 0 !default;
$picker-wp-option-text-color: $list-wp-text-color !default; $picker-wp-option-text-color: $list-wp-text-color !default;
$picker-wp-option-font-size: 18px !default; $picker-wp-option-font-size: 22px !default;
$picker-wp-option-height: 42px !default; $picker-wp-option-height: 42px !default;
$picker-wp-option-offset-y: (($picker-wp-height - $picker-wp-toolbar-height) / 2) - ($picker-wp-option-height / 2) - 10 !default; $picker-wp-option-offset-y: (($picker-wp-height - $picker-wp-toolbar-height) / 2) - ($picker-wp-option-height / 2) - 10 !default;
$picker-wp-option-selected-font-size: 22px !default;
$picker-wp-option-selected-color: color($colors-wp, primary) !default; $picker-wp-option-selected-color: color($colors-wp, primary) !default;
@ -96,14 +95,12 @@ $picker-wp-option-selected-color: color($colors-wp, primary) !defaul
pointer-events: none; pointer-events: none;
} }
.picker-wp .picker-opts .button-effect {
display: none;
}
.picker-wp .picker-opt { .picker-wp .picker-opt {
margin: 0; margin: 0;
padding: $picker-wp-option-padding; padding: $picker-wp-option-padding;
height: 4.2rem;
font-size: $picker-wp-option-font-size; font-size: $picker-wp-option-font-size;
line-height: $picker-wp-option-height; line-height: $picker-wp-option-height;
@ -116,15 +113,9 @@ $picker-wp-option-selected-color: color($colors-wp, primary) !defaul
pointer-events: auto; pointer-events: auto;
} }
.picker-wp .picker-opt .button-inner {
transition: 200ms;
}
.picker-wp .picker-prefix, .picker-wp .picker-prefix,
.picker-wp .picker-suffix, .picker-wp .picker-suffix,
.picker-wp .picker-opt-selected { .picker-wp .picker-opt-selected {
font-size: $picker-wp-option-selected-font-size;
color: $picker-wp-option-selected-color; color: $picker-wp-option-selected-color;
} }

View File

@ -29,6 +29,7 @@ export const MODE_IOS: any = {
pickerEnter: 'picker-slide-in', pickerEnter: 'picker-slide-in',
pickerLeave: 'picker-slide-out', pickerLeave: 'picker-slide-out',
pickerRotateFactor: -0.46, pickerRotateFactor: -0.46,
pickerScaleFactor: 1,
popoverEnter: 'popover-pop-in', popoverEnter: 'popover-pop-in',
popoverLeave: 'popover-pop-out', popoverLeave: 'popover-pop-out',
@ -72,6 +73,7 @@ export const MODE_MD: any = {
pickerEnter: 'picker-slide-in', pickerEnter: 'picker-slide-in',
pickerLeave: 'picker-slide-out', pickerLeave: 'picker-slide-out',
pickerRotateFactor: 0, pickerRotateFactor: 0,
pickerScaleFactor: 0.81,
popoverEnter: 'popover-md-pop-in', popoverEnter: 'popover-md-pop-in',
popoverLeave: 'popover-md-pop-out', popoverLeave: 'popover-md-pop-out',
@ -115,6 +117,7 @@ export const MODE_WP: any = {
pickerEnter: 'picker-slide-in', pickerEnter: 'picker-slide-in',
pickerLeave: 'picker-slide-out', pickerLeave: 'picker-slide-out',
pickerRotateFactor: 0, pickerRotateFactor: 0,
pickerScaleFactor: 0.81,
popoverEnter: 'popover-md-pop-in', popoverEnter: 'popover-md-pop-in',
popoverLeave: 'popover-md-pop-out', popoverLeave: 'popover-md-pop-out',