perf(animations): throttle calls to requestAnimationFrame

Closes #582
This commit is contained in:
Andy Joslin
2014-02-17 17:25:31 -05:00
parent 97f4f6eace
commit d056ad1fef
12 changed files with 193 additions and 195 deletions

View File

@@ -110,14 +110,12 @@ angular.module('ionic.ui.sideMenu', ['ionic.service.gesture', 'ionic.service.vie
getTranslateX: function() {
return $scope.sideMenuContentTranslateX || 0;
},
setTranslateX: function(amount) {
window.rAF(function() {
$element[0].style.webkitTransform = 'translate3d(' + amount + 'px, 0, 0)';
$timeout(function() {
$scope.sideMenuContentTranslateX = amount;
});
setTranslateX: ionic.animationFrameThrottle(function(amount) {
$element[0].style.webkitTransform = 'translate3d(' + amount + 'px, 0, 0)';
$timeout(function() {
$scope.sideMenuContentTranslateX = amount;
});
},
}),
enableAnimation: function() {
//this.el.classList.add(this.animateClass);
$scope.animationEnabled = true;

View File

@@ -158,7 +158,7 @@ angular.module('ionic.ui.viewState', ['ionic.service.view', 'ionic.service.gestu
*/
.directive('asyncVisible', function() {
return function($scope, $element) {
window.rAF(function() {
ionic.requestAnimationFrame(function() {
$element[0].classList.remove('invisible');
});
};

View File

@@ -19,32 +19,4 @@ describe('Ionic SideMenuDelegate Service', function() {
expect(sc).not.toBe(undefined);
});
/*
it('Should open and close', function() {
var scope = rootScope.$new();
var el = compile('<side-menus><div side-menu-content></div><side-menu side="left"></side-menu><side-menu side="right"></side-menu></side-menus>')(scope);
var sc = del.getSideMenuController(scope);
window.rAF = function( callback ){
window.setTimeout(callback, 1);
};
del.openLeft(scope);
timeout.flush();
expect(sc.isOpen()).toBe(true);
expect(sc.getOpenAmount()).toBe(100);
del.close(scope);
expect(sc.isOpen()).toBe(false);
expect(sc.getOpenAmount()).toBe(0);
del.openRight(scope);
expect(sc.isOpen()).toBe(true);
expect(sc.getOpenAmount()).toBe(-100);
del.close(scope);
expect(sc.isOpen()).toBe(false);
expect(sc.getOpenAmount()).toBe(0);
});
*/
});

View File

@@ -4,4 +4,4 @@ window.ionic = {
controllers: {},
views: {},
version: '{{ VERSION }}'
};
};

View File

@@ -3,14 +3,58 @@
var readyCallbacks = [],
domReady = function() {
for(var x=0; x<readyCallbacks.length; x++) {
window.rAF(readyCallbacks[x]);
ionic.requestAnimationFrame(readyCallbacks[x]);
}
readyCallbacks = [];
document.removeEventListener('DOMContentLoaded', domReady);
};
document.addEventListener('DOMContentLoaded', domReady);
// From the man himself, Mr. Paul Irish.
// The requestAnimationFrame polyfill
// Put it on window just to preserve its context
// without having to use .call
window._rAF = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 16);
};
})();
ionic.DomUtil = {
//Call with proper context
requestAnimationFrame: function(cb) {
window._rAF(cb);
},
/*
* When given a callback, if that callback is called 100 times between
* animation frames, Throttle will make it only call the last of 100tha call
*
* It returns a function, which will then call the passed in callback. The
* passed in callback will receive the context the returned function is called with.
*
* @example
* this.setTranslateX = ionic.animationFrameThrottle(function(x) {
* this.el.style.webkitTransform = 'translate3d(' + x + 'px, 0, 0)';
* })
*/
animationFrameThrottle: function(cb) {
var args, isQueued, context;
return function() {
args = arguments;
context = this;
if (!isQueued) {
isQueued = true;
ionic.requestAnimationFrame(function() {
cb.apply(context, args);
isQueued = false;
});
}
};
},
/*
* Find an element's offset, then add it to the offset of the parent
@@ -26,7 +70,7 @@
ready: function(cb) {
if(document.readyState === "complete") {
window.rAF(cb);
ionic.requestAnimationFrame(cb);
} else {
readyCallbacks.push(cb);
}
@@ -106,4 +150,8 @@
return true;
}
};
//Shortcuts
ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame;
ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle;
})(window.ionic);

View File

@@ -26,7 +26,7 @@ function androidKeyboardFix() {
window.innerHeight < rememberedDeviceHeight) {
document.body.classList.add('hide-footer');
//Wait for next frame so document.activeElement is set
window.rAF(handleKeyboardChange);
ionic.requestAnimationFrame(handleKeyboardChange);
} else {
//Otherwise we have a keyboard close or a *really* weird resize
document.body.classList.remove('hide-footer');

View File

@@ -1,20 +1,9 @@
(function(window, document, ionic) {
'use strict';
// From the man himself, Mr. Paul Irish.
// The requestAnimationFrame polyfill
window.rAF = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 16);
};
})();
// Ionic CSS polyfills
ionic.CSS = {};
(function() {
var d = document.createElement('div');
var keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform',
@@ -42,7 +31,7 @@
clickEvent.initMouseEvent('click', true, true, window,
1, 0, 0, c.x, c.y,
false, false, false, false, 0, null);
ele.dispatchEvent(clickEvent);
if(ele.tagName === 'INPUT' || ele.tagName === 'TEXTAREA' || ele.tagName === 'SELECT') {
@@ -85,9 +74,9 @@
while(ele) {
// climb up the DOM looking to see if the tapped element is, or has a parent, of one of these
if( ele.tagName === "INPUT" ||
ele.tagName === "A" ||
ele.tagName === "BUTTON" ||
ele.tagName === "TEXTAREA" ||
ele.tagName === "A" ||
ele.tagName === "BUTTON" ||
ele.tagName === "TEXTAREA" ||
ele.tagName === "SELECT" ) {
return ionic.tapElement(ele, e);
@@ -121,8 +110,8 @@
// remember the last time this label was clicked to it can prevent a second label ghostclick
e.target.control.labelLastTap = Date.now();
// The input's click event will propagate so don't bother letting this label's click
// propagate cuz it causes double clicks. However, do NOT e.preventDefault(), because
// The input's click event will propagate so don't bother letting this label's click
// propagate cuz it causes double clicks. However, do NOT e.preventDefault(), because
// the native layer still needs to click the input which the label controls
console.debug('preventGhostClick', 'label stopPropagation');
e.stopPropagation();
@@ -144,7 +133,7 @@
// remember the last time this element was clicked
e.target.lastClick = Date.now();
// remember the coordinates of this click so if a tap or click in the
// remember the coordinates of this click so if a tap or click in the
// same area quickly happened again we can ignore it
recordCoordinates(e);
}
@@ -157,8 +146,8 @@
function blurActive() {
var ele = document.activeElement;
if(ele && (ele.tagName === "INPUT" ||
ele.tagName === "TEXTAREA" ||
if(ele && (ele.tagName === "INPUT" ||
ele.tagName === "TEXTAREA" ||
ele.tagName === "SELECT")) {
// using a timeout to prevent funky scrolling while a keyboard hides
setTimeout(function(){

View File

@@ -17,75 +17,71 @@
* so that the header text size is maximized and aligned
* correctly as long as possible.
*/
align: function(titleSelector) {
var _this = this;
align: ionic.animationFrameThrottle(function(titleSelector) {
window.rAF(ionic.proxy(function() {
// Find the titleEl element
var titleEl = this.el.querySelector(titleSelector || '.title');
if(!titleEl) {
return;
}
// Find the titleEl element
var titleEl = this.el.querySelector(titleSelector || '.title');
if(!titleEl) {
return;
var i, c, childSize;
var childNodes = this.el.childNodes;
var leftWidth = 0;
var rightWidth = 0;
var isCountingRightWidth = true;
// Compute how wide the left children are
// Skip all titles (there may still be two titles, one leaving the dom)
// Once we encounter a titleEl, realize we are now counting the right-buttons, not left
for(i = 0; i < childNodes.length; i++) {
c = childNodes[i];
if (c.tagName && c.tagName.toLowerCase() == 'h1') {
isCountingRightWidth = false;
continue;
}
var i, c, childSize;
var childNodes = this.el.childNodes;
var leftWidth = 0;
var rightWidth = 0;
var isCountingRightWidth = true;
// Compute how wide the left children are
// Skip all titles (there may still be two titles, one leaving the dom)
// Once we encounter a titleEl, realize we are now counting the right-buttons, not left
for(i = 0; i < childNodes.length; i++) {
c = childNodes[i];
if (c.tagName && c.tagName.toLowerCase() == 'h1') {
isCountingRightWidth = false;
continue;
}
childSize = null;
if(c.nodeType == 3) {
childSize = ionic.DomUtil.getTextBounds(c);
} else if(c.nodeType == 1) {
childSize = c.getBoundingClientRect();
}
if(childSize) {
if (isCountingRightWidth) {
rightWidth += childSize.width;
} else {
leftWidth += childSize.width;
}
childSize = null;
if(c.nodeType == 3) {
childSize = ionic.DomUtil.getTextBounds(c);
} else if(c.nodeType == 1) {
childSize = c.getBoundingClientRect();
}
if(childSize) {
if (isCountingRightWidth) {
rightWidth += childSize.width;
} else {
leftWidth += childSize.width;
}
}
}
var margin = Math.max(leftWidth, rightWidth) + 10;
var margin = Math.max(leftWidth, rightWidth) + 10;
// Size and align the header titleEl based on the sizes of the left and
// right children, and the desired alignment mode
if(this.alignTitle == 'center') {
if(margin > 10) {
titleEl.style.left = margin + 'px';
titleEl.style.right = margin + 'px';
}
if(titleEl.offsetWidth < titleEl.scrollWidth) {
if(rightWidth > 0) {
titleEl.style.right = (rightWidth + 5) + 'px';
}
}
} else if(this.alignTitle == 'left') {
titleEl.classList.add('titleEl-left');
if(leftWidth > 0) {
titleEl.style.left = (leftWidth + 15) + 'px';
}
} else if(this.alignTitle == 'right') {
titleEl.classList.add('titleEl-right');
// Size and align the header titleEl based on the sizes of the left and
// right children, and the desired alignment mode
if(this.alignTitle == 'center') {
if(margin > 10) {
titleEl.style.left = margin + 'px';
titleEl.style.right = margin + 'px';
}
if(titleEl.offsetWidth < titleEl.scrollWidth) {
if(rightWidth > 0) {
titleEl.style.right = (rightWidth + 15) + 'px';
titleEl.style.right = (rightWidth + 5) + 'px';
}
}
}, this));
}
} else if(this.alignTitle == 'left') {
titleEl.classList.add('titleEl-left');
if(leftWidth > 0) {
titleEl.style.left = (leftWidth + 15) + 'px';
}
} else if(this.alignTitle == 'right') {
titleEl.classList.add('titleEl-right');
if(rightWidth > 0) {
titleEl.style.right = (rightWidth + 15) + 'px';
}
}
})
});
})(ionic);

View File

@@ -62,41 +62,39 @@
};
};
SlideDrag.prototype.drag = function(e) {
var _this = this, buttonsWidth;
SlideDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
var buttonsWidth;
window.rAF(function() {
// We really aren't dragging
if(!_this._currentDrag) {
return;
// We really aren't dragging
if(!this._currentDrag) {
return;
}
// Check if we should start dragging. Check if we've dragged past the threshold,
// or we are starting from the open state.
if(!this._isDragging &&
((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
(Math.abs(this._currentDrag.startOffsetX) > 0)))
{
this._isDragging = true;
}
if(this._isDragging) {
buttonsWidth = this._currentDrag.buttonsWidth;
// Grab the new X point, capping it at zero
var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
// If the new X position is past the buttons, we need to slow down the drag (rubber band style)
if(newX < -buttonsWidth) {
// Calculate the new X position, capped at the top of the buttons
newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
}
// Check if we should start dragging. Check if we've dragged past the threshold,
// or we are starting from the open state.
if(!_this._isDragging &&
((Math.abs(e.gesture.deltaX) > _this.dragThresholdX) ||
(Math.abs(_this._currentDrag.startOffsetX) > 0)))
{
_this._isDragging = true;
}
if(_this._isDragging) {
buttonsWidth = _this._currentDrag.buttonsWidth;
// Grab the new X point, capping it at zero
var newX = Math.min(0, _this._currentDrag.startOffsetX + e.gesture.deltaX);
// If the new X position is past the buttons, we need to slow down the drag (rubber band style)
if(newX < -buttonsWidth) {
// Calculate the new X position, capped at the top of the buttons
newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
}
_this._currentDrag.content.style.webkitTransform = 'translate3d(' + newX + 'px, 0, 0)';
_this._currentDrag.content.style.webkitTransition = 'none';
}
});
};
this._currentDrag.content.style.webkitTransform = 'translate3d(' + newX + 'px, 0, 0)';
this._currentDrag.content.style.webkitTransition = 'none';
}
});
SlideDrag.prototype.end = function(e, doneCallback) {
var _this = this;
@@ -133,7 +131,7 @@
// e.target.removeEventListener('webkitTransitionEnd', onRestingAnimationEnd);
// };
window.rAF(function() {
ionic.requestAnimationFrame(function() {
// var currentX = parseFloat(_this._currentDrag.content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0;
// if(currentX !== restingPoint) {
// _this._currentDrag.content.classList.add(ITEM_SLIDING_CLASS);
@@ -206,53 +204,49 @@
this._moveElement(e);
};
ReorderDrag.prototype.drag = function(e) {
var _this = this;
ReorderDrag.prototype.drag = ionic.animationFrameThrottle(function(e) {
// We really aren't dragging
if(!this._currentDrag) {
return;
}
window.rAF(function() {
// We really aren't dragging
if(!_this._currentDrag) {
return;
var scrollY = 0;
var pageY = e.gesture.center.pageY;
//If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary
if (this.scrollView) {
var container = this.scrollEl;
scrollY = this.scrollView.getValues().top;
var containerTop = container.offsetTop;
var pixelsPastTop = containerTop - pageY + this._currentDrag.elementHeight/2;
var pixelsPastBottom = pageY + this._currentDrag.elementHeight/2 - containerTop - container.offsetHeight;
if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) {
this.scrollView.scrollBy(null, -pixelsPastTop);
}
var scrollY = 0;
var pageY = e.gesture.center.pageY;
//If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary
if (_this.scrollView) {
var container = _this.scrollEl;
scrollY = _this.scrollView.getValues().top;
var containerTop = container.offsetTop;
var pixelsPastTop = containerTop - pageY + _this._currentDrag.elementHeight/2;
var pixelsPastBottom = pageY + _this._currentDrag.elementHeight/2 - containerTop - container.offsetHeight;
if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) {
_this.scrollView.scrollBy(null, -pixelsPastTop);
}
if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) {
if (scrollY < _this.scrollView.getScrollMax().top) {
_this.scrollView.scrollBy(null, pixelsPastBottom);
}
if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) {
if (scrollY < this.scrollView.getScrollMax().top) {
this.scrollView.scrollBy(null, pixelsPastBottom);
}
}
}
// Check if we should start dragging. Check if we've dragged past the threshold,
// or we are starting from the open state.
if(!_this._isDragging && Math.abs(e.gesture.deltaY) > _this.dragThresholdY) {
_this._isDragging = true;
}
// Check if we should start dragging. Check if we've dragged past the threshold,
// or we are starting from the open state.
if(!this._isDragging && Math.abs(e.gesture.deltaY) > this.dragThresholdY) {
this._isDragging = true;
}
if(_this._isDragging) {
_this._moveElement(e);
if(this._isDragging) {
this._moveElement(e);
_this._currentDrag.currentY = scrollY + pageY - _this._currentDrag.placeholder.parentNode.offsetTop;
this._currentDrag.currentY = scrollY + pageY - this._currentDrag.placeholder.parentNode.offsetTop;
_this._reorderItems();
}
});
};
this._reorderItems();
}
});
// When an item is dragged, we need to reorder any items for sorting purposes
ReorderDrag.prototype._reorderItems = function() {

View File

@@ -22,7 +22,7 @@
alert: function(message) {
var _this = this;
window.rAF(function() {
ionic.requestAnimationFrame(function() {
_this.setTitle(message);
_this.el.classList.add('active');
});

View File

@@ -624,11 +624,12 @@ ionic.views.Scroll = ionic.views.View.inherit({
if ('ontouchstart' in window) {
container.addEventListener("touchstart", function(e) {
console.log('touchstart scrollview');
if (e.defaultPrevented || shouldIgnorePress(e)) {
return;
}
self.doTouchStart(e.touches, e.timeStamp);
e.preventDefault();
// e.preventDefault();
}, false);
document.addEventListener("touchmove", function(e) {

View File

@@ -59,9 +59,9 @@
getTranslateX: function() {
return parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[0]);
},
setTranslateX: function(x) {
setTranslateX: ionic.animationFrameThrottle(function(x) {
this.el.style.webkitTransform = 'translate3d(' + x + 'px, 0, 0)';
}
})
});
})(ionic);