Touch scrolling #143

Open
wants to merge 7 commits into
from
View
@@ -44,8 +44,7 @@ return declare([List, _StoreMixin], {
this.inherited(arguments);
var self = this;
// check visibility on scroll events
- listen(this.bodyNode, "scroll",
- miscUtil.throttleDelayed(function(event){ self._processScroll(event); },
+ listen(this.bodyNode, "scroll", miscUtil.throttleDelayed(function(event){ self._processScroll(event); },
null, this.pagingDelay));
},
@@ -132,7 +131,7 @@ return declare([List, _StoreMixin], {
// if total is 0, IE quirks mode can't handle 0px height for some reason, I don't know why, but we are setting display: none for now
preloadNode.style.display = "none";
}
- self._processScroll(); // recheck the scroll position in case the query didn't fill the screen
+ self._processScroll({}); // recheck the scroll position in case the query didn't fill the screen
// can remove the loading node now
return trs;
});
@@ -163,14 +162,14 @@ return declare([List, _StoreMixin], {
},
lastScrollTop: 0,
- _processScroll: function(){
+ _processScroll: function(event){
// summary:
- // Checks to make sure that everything in the viewable area has been
+ // Checks to make sure that everything in te viewable area has been
// downloaded, and triggering a request for the necessary data when needed.
var grid = this,
scrollNode = grid.bodyNode,
transform = grid.contentNode.style.webkitTransform,
- visibleTop = scrollNode.scrollTop + (transform ? -transform.match(/translate[\w]*\(.*?,(.*?)px/)[1] : 0),
+ visibleTop = event && event.pseudoTouch ? scrollNode.instantScrollTop : scrollNode.scrollTop,
visibleBottom = scrollNode.offsetHeight + visibleTop,
priorPreload, preloadNode, preload = grid.preload,
lastScrollTop = grid.lastScrollTop,
View
@@ -1,12 +1,16 @@
-define(["dojo/_base/declare", "dojo/on"],
-function(declare, on){
+define(["dojo/_base/declare", "dojo/on", "dojo/has", "put-selector/put"],
+function(declare, on, has, put){
+ var userAgent = navigator.userAgent;
+ // have to do some sniffing to guess if it has native overflow touch scrolling and accelerated transforms
+ has.add("touch-scrolling", document.documentElement.style.WebkitOverflowScrolling !== undefined || parseFloat(userAgent.split("Android ")[1]) >= 4);
+ has.add("accelerated-transform", !!userAgent.match(/like Mac/));
var
bodyTouchListener, // stores handle to body touch handler once connected
- timerRes = 15, // ms between drag velocity measurements and animation "ticks"
+ timerRes = 10, // ms between drag velocity measurements and animation "ticks"
touches = 0, // records number of touches on document
current = {}, // records info for widget currently being scrolled
glide = {}, // records info for widgets that are in "gliding" state
- glideThreshold = 1; // speed (in px) below which to stop glide
+ glideThreshold = 0.021; // speed (in px/ms) below which to stop glide
function updatetouchcount(evt){
touches = evt.touches.length;
@@ -22,17 +26,39 @@ function(declare, on){
clearTimeout(g.timer);
delete glide[id];
}
-
// check "global" touches count (which hasn't counted this touch yet)
if(touches > 0){ return; } // ignore multitouch gestures
-
+ if(has("touch-scrolling")){
+ // reset these prior to measurements
+ this.instantScrollLeft = 0;
+ this.instantScrollTop = 0;
+ }else{
+ // if the scrolling height and width is bigger than the area, than we add scrollbars in each direction
+ if(this.scrollHeight > this.offsetHeight){
+ var scrollbarYNode = this.scrollbarYNode;
+ if(!scrollbarYNode){
+ scrollbarYNode = this.scrollbarYNode = put(this.parentNode, "div.dgrid-touch-scrollbar-y");
+ scrollbarYNode.style.height = this.offsetHeight * this.offsetHeight / this.scrollHeight + "px";
+ scrollbarYNode.style.top = this.offsetTop + "px";
+ }
+ }
+ if(this.scrollWidth > this.offsetWidth){
+ var scrollbarXNode = this.scrollbarXNode;
+ if(!scrollbarXNode){
+ scrollbarXNode = this.scrollbarXNode = put(this.parentNode, "div.dgrid-touch-scrollbar-x");
+ scrollbarXNode.style.width = this.offsetWidth * this.offsetWidth / this.scrollWidth + "px";
+ scrollbarXNode.style.left = this.offsetLeft + "px";
+ }
+ }
+ // remove the fade class if we are reusing the scrollbar
+ put(this.parentNode, '!dgrid-touch-scrollbar-fade');
+ }
t = evt.touches[0];
current = {
widget: evt.widget,
node: this,
- startX: this.scrollLeft + t.pageX,
- startY: this.scrollTop + t.pageY,
- timer: setTimeout(calcTick, timerRes)
+ startX: (this.instantScrollLeft || this.scrollLeft) + t.pageX,
+ startY: (this.instantScrollTop || this.scrollTop) + t.pageY
};
}
function ontouchmove(evt){
@@ -41,79 +67,156 @@ function(declare, on){
t = evt.touches[0];
// snuff event and scroll the area
- evt.preventDefault();
- evt.stopPropagation();
- this.scrollLeft = current.startX - t.pageX;
- this.scrollTop = current.startY - t.pageY;
+ if(!has("touch-scrolling")){
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+
+ scroll(this, current.startX - t.pageX, current.startY - t.pageY);
+ calcVelocity();
}
function ontouchend(evt){
if(touches != 1 || !current){ return; }
- current.timer && clearTimeout(current.timer);
startGlide(current);
current = null;
}
-
+ function scroll(node, x, y){
+ // do the actual scrolling
+ var hasTouchScrolling = has("touch-scrolling");
+ x = Math.min(Math.max(0.01, x), node.scrollWidth - node.offsetWidth);
+ y = Math.min(Math.max(0.01, y), node.scrollHeight - node.offsetHeight);
+ if(!hasTouchScrolling && has("accelerated-transform")){
+ // we have hardward acceleration of transforms, so we will do the fast scrolling
+ // by setting the transform style with a translate3d
+ var transformNode = node.firstChild;
+ var lastScrollLeft = node.scrollLeft;
+ var lastScrollTop = node.scrollTop;
+ // store the current scroll position
+ node.instantScrollLeft = x;
+ node.instantScrollTop = y;
+ // set the style transform
+ transformNode.style.WebkitTransform = "translate3d(" + (node.scrollLeft - x) + "px,"
+ + (node.scrollTop - y) + "px,0)";
+ // now every half a second actually update the scroll position so that the scroll
+ // monitors (like OnDemandList) receive events and scroll positions to work with
+ if(!node._scrollWaiting){
+ node._scrollWaiting = true;
+ setTimeout(function(){
+ node._scrollWaiting = false;
+ // reset the transform since we are updating the actual scroll position
+ transformNode.style.WebkitTransform = "translate3d(0,0,0)";
+ // get the latest effective scroll position
+ node.scrollLeft = node.instantScrollLeft;
+ node.scrollTop = node.instantScrollTop;
+ // reset these so they aren't used anymore
+ node.instantScrollLeft = 0;
+ node.instantScrollTop = 0;
+ }, 500);
+ }
+ }else{
+ // update scroll position immediately (note we may be using browser's touch scroll
+ var scrollPrefix = hasTouchScrolling ? "instantScroll" : "scroll";
+ node[scrollPrefix + "Left"] = x;
+ node[scrollPrefix + "Top"] = y;
+
+ if(hasTouchScrolling){
+ // if we are using browser's touch scroll, we fire our own scroll events
+ on.emit(node, "scroll", {
+ pseudoTouch: true
+ });
+ }
+ }
+ if(!hasTouchScrolling){
+ // move the scrollbar
+ var scrollbarXNode = node.scrollbarXNode;
+ var scrollbarYNode = node.scrollbarYNode;
+ scrollbarXNode && (scrollbarXNode.style.WebkitTransform = "translate3d(" + (x * node.offsetWidth / node.scrollWidth) + "px,0,0)");
+ scrollbarYNode && (scrollbarYNode.style.WebkitTransform = "translate3d(0," + (y * node.offsetHeight / node.scrollHeight) + "px,0)");
+ }
+ }
// glide-related functions
- function calcTick(){
+ function calcVelocity(){
// Calculates current speed of touch drag
- var node, x, y;
+ var node, x, y, now;
if(!current){ return; } // no currently-scrolling widget; abort
node = current.node;
- x = node.scrollLeft;
- y = node.scrollTop;
+ x = node.instantScrollLeft || node.scrollLeft;
+ y = node.instantScrollTop || node.scrollTop;
+ now = new Date().getTime();
if("prevX" in current){
// calculate velocity using previous reference point
- current.velX = x - current.prevX;
- current.velY = y - current.prevY;
+ var duration = now - current.prevTime;
+ current.velX = (x - current.prevX) / duration;
+ current.velY = (y - current.prevY) / duration;
+
+ }
+ if(!(current.prevTime - now > -150)){ // make sure it is far enough back that we can get a good estimate
+ // set previous reference point for next iteration
+ current.prevX = x;
+ current.prevY = y;
+ current.prevTime = now;
}
- // set previous reference point for next iteration
- current.prevX = x;
- current.prevY = y;
- current.timer = setTimeout(calcTick, timerRes);
}
-
+ var lastGlideTime;
function startGlide(info){
// starts glide operation when drag ends
var id = info.widget.id, g;
- if(!info.velX && !info.velY){ return; } // no glide to perform
+ if(!info.velX && !info.velY){
+ fadeScrollBars(info.node);
+ return;
+ } // no glide to perform
g = glide[id] = info; // reuse object for widget/node/vel properties
g.calcFunc = function(){ calcGlide(id); }
+ lastGlideTime = new Date().getTime();
g.timer = setTimeout(g.calcFunc, timerRes);
}
function calcGlide(id){
// performs glide and decelerates according to widget's glideDecel method
var g = glide[id], x, y, node, widget,
- vx, vy, nvx, nvy; // old and new velocities
-
+ vx, vy, nvx, nvy, // old and new velocities
+ now = new Date().getTime(),
+ sinceLastGlide = now - lastGlideTime;
if(!g){ return; }
-
node = g.node;
widget = g.widget;
- x = node.scrollLeft;
- y = node.scrollTop;
+ // we use instantScroll... so that OnDemandList has something to pull from to get the current value (needed for ios5 with touch scrolling)
+ x = node.instantScrollLeft || node.scrollLeft;
+ y = node.instantScrollTop || node.scrollTop;
+ // note that velocity is measured in pixels per millisecond
vx = g.velX;
vy = g.velY;
- nvx = widget.glideDecel(vx);
- nvy = widget.glideDecel(vy);
+ nvx = widget.glideDecel(vx, sinceLastGlide);
+ nvy = widget.glideDecel(vy, sinceLastGlide);
+ var continueGlide;
if(Math.abs(nvx) >= glideThreshold || Math.abs(nvy) >= glideThreshold){
// still above stop threshold; update scroll positions
- node.scrollLeft += nvx;
- node.scrollTop += nvy;
- if(node.scrollLeft != x || node.scrollTop != y){
+ scroll(node, x + nvx * sinceLastGlide, y + nvy * sinceLastGlide); // for each dimension multiply the velocity (px/ms) by the ms elapsed
+ if((node.instantScrollLeft || node.scrollLeft) != x || (node.instantScrollTop || node.scrollTop) != y){
// still scrollable; update velocities and schedule next tick
+ continueGlide = true;
g.velX = nvx;
g.velY = nvy;
g.timer = setTimeout(g.calcFunc, timerRes);
}
}
+ if(!continueGlide){
+ fadeScrollBars(node);
+
+ }
+ lastGlideTime = now;
+ }
+ function fadeScrollBars(node){
+ // add the fade class so that scrollbar fades to transparent
+ put(node.parentNode, '.dgrid-touch-scrollbar-fade');
}
return declare([], {
+ pagingDelay: 500,
startup: function(){
var node = this.touchNode || this.containerNode || this.domNode,
widget = this;
@@ -131,12 +234,15 @@ function(declare, on){
"touchstart,touchend,touchcancel", updatetouchcount);
}
},
- glideDecel: function(n){
+ // friction: Float
+ // This is the friction deceleration measured in pixels/milliseconds^2
+ friction: 0.0006,
+ glideDecel: function(n, sinceLastGlide){
// summary:
// Deceleration algorithm. Given a number representing velocity,
// returns a new velocity to impose for the next "tick".
// (Don't forget that velocity can be positive or negative!)
- return n * 0.9; // Number
+ return n + (n > 0 ? -sinceLastGlide : sinceLastGlide) * this.friction; // Number
}
});
});
View
@@ -79,19 +79,40 @@ html.has-quirks .dgrid-header-hidden .dgrid-cell {
}
.dgrid-content {
- position: relative;
+ /*position: relative;*/
height: 99%;
}
.dgrid-scroller {
overflow-x: auto;
overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
position: absolute;
top: 0px;
margin-top: 25px; /* this will be adjusted programmatically to fit below the header*/
bottom: 0px;
width: 100%;
}
+.dgrid-touch-scrollbar-y, .dgrid-touch-scrollbar-x {
+ position: absolute;
+ background-color: rgba(88,88,88,0.97);
+ opacity: 0.7;
+ border: 1px solid rgba(88,88,88,1);
+ border-radius: 3px;
+ -webkit-box-shadow: 0 0 1px rgba(88,88,88,0.4); /* the border's aren't anti-aliased on android, so this smooths it out a bit*/
+}
+.dgrid-touch-scrollbar-y {
+ right: 1px;
+ width: 3px;
+}
+.dgrid-touch-scrollbar-x {
+ bottom: 1px;
+ height: 3px;
+}
+.dgrid-touch-scrollbar-fade .dgrid-touch-scrollbar-y, .dgrid-touch-scrollbar-fade .dgrid-touch-scrollbar-x {
+ -webkit-transition: opacity 0.3s ease-out 0.3s;
+ opacity: 0;
+}
.dgrid-loading {
position: relative;
Oops, something went wrong.