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

View File

@ -94,6 +94,8 @@ $picker-ios-option-offset-y: (($picker-ios-height - $picker-io
margin: 0;
padding: $picker-ios-option-padding;
height: 4.6rem;
font-size: $picker-ios-option-font-size;
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-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-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;
@ -82,14 +81,13 @@ $picker-md-option-selected-color: color($colors-md, primary) !defaul
pointer-events: none;
}
.picker-md .picker-opts .button-effect {
display: none;
}
.picker-md .picker-opt {
margin: 0;
padding: $picker-md-option-padding;
height: 4.3rem;
font-size: $picker-md-option-font-size;
line-height: $picker-md-option-height;
@ -102,14 +100,9 @@ $picker-md-option-selected-color: color($colors-md, primary) !defaul
pointer-events: auto;
}
.picker-md .picker-opt .button-inner {
transition: 200ms;
}
.picker-md .picker-prefix,
.picker-md .picker-suffix,
.picker-md .picker-opt-selected {
font-size: $picker-md-option-selected-font-size;
.picker-md .picker-opt.picker-opt-selected {
color: $picker-md-option-selected-color;
}

View File

@ -88,41 +88,38 @@ ion-picker-cmp {
white-space: nowrap;
}
// contain property is supported by Chrome
.picker-opt {
position: absolute;
top: 0;
left: 0;
display: block;
overflow: hidden;
flex: 1;
width: 100%;
}
.picker-opt .button-inner {
display: block;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
color: #000;
transition: opacity 150ms ease-in-out;
will-change: transform;
contain: strict;
}
.picker-opt.picker-opt-disabled {
pointer-events: none;
}
.picker-opt-disabled .button-inner {
.picker-opt-disabled {
opacity: 0;
}
.picker-opts-left .button-inner {
.picker-opts-left .picker-opt {
justify-content: flex-start;
}
.picker-opts-right .button-inner {
.picker-opts-right .picker-opt {
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-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-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;
@ -96,14 +95,12 @@ $picker-wp-option-selected-color: color($colors-wp, primary) !defaul
pointer-events: none;
}
.picker-wp .picker-opts .button-effect {
display: none;
}
.picker-wp .picker-opt {
margin: 0;
padding: $picker-wp-option-padding;
height: 4.2rem;
font-size: $picker-wp-option-font-size;
line-height: $picker-wp-option-height;
@ -116,15 +113,9 @@ $picker-wp-option-selected-color: color($colors-wp, primary) !defaul
pointer-events: auto;
}
.picker-wp .picker-opt .button-inner {
transition: 200ms;
}
.picker-wp .picker-prefix,
.picker-wp .picker-suffix,
.picker-wp .picker-opt-selected {
font-size: $picker-wp-option-selected-font-size;
color: $picker-wp-option-selected-color;
}

View File

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