Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Touch scrolling #143

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions OnDemandList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},

Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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,
Expand Down
184 changes: 145 additions & 39 deletions TouchScroll.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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){
Expand All @@ -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;
Expand All @@ -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
}
});
});
23 changes: 22 additions & 1 deletion css/dgrid.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading