mirror of https://github.com/espruino/BangleApps
Merge pull request #3296 from halemmerich/kineticscroll
kineticscroll - Better scrolling behaviourpull/3369/head
commit
2ce749851d
|
@ -1,2 +1,4 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Minor code improvements
|
0.02: Minor code improvements
|
||||||
|
0.03: Better scrolling behaviour
|
||||||
|
|
||||||
|
|
|
@ -9,28 +9,35 @@
|
||||||
remove = function()
|
remove = function()
|
||||||
select = function(idx, touch)
|
select = function(idx, touch)
|
||||||
}
|
}
|
||||||
|
|
||||||
returns {
|
returns {
|
||||||
scroll: int // current scroll amount
|
scroll: int // current scroll amount
|
||||||
draw: function() // draw all
|
draw: function() // draw all
|
||||||
drawItem : function(idx) // draw specific item
|
drawItem : function(idx) // draw specific item
|
||||||
isActive : function() // is this scroller still active?
|
isActive : function() // is this scroller still active?
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
if (!options) return Bangle.setUI(); // remove existing handlers
|
if (!options) return Bangle.setUI(); // remove existing handlers
|
||||||
|
|
||||||
const MAX_VELOCITY=100;
|
const SPEED=100;
|
||||||
|
const LAST_DRAG_WAIT=150;
|
||||||
|
const MIN_VELOCITY=0.1;
|
||||||
|
|
||||||
let scheduledDraw;
|
let scheduledDraw;
|
||||||
|
|
||||||
let velocity = 0;
|
let velocity = 0;
|
||||||
let accDy = 0;
|
let accDy = 0;
|
||||||
let scheduledBrake = setInterval(()=>{velocity*=0.9;}, 50);
|
let direction = 0;
|
||||||
|
|
||||||
|
let lastTouchedDrag = 0;
|
||||||
let lastDragStart = 0;
|
let lastDragStart = 0;
|
||||||
|
|
||||||
let R = Bangle.appRect;
|
let R = Bangle.appRect;
|
||||||
let menuScrollMin = 0|options.scrollMin;
|
let menuScrollMin = 0|options.scrollMin;
|
||||||
let menuScrollMax = options.h*options.c - R.h;
|
let menuScrollMax = options.h*options.c - R.h;
|
||||||
if (menuScrollMax<menuScrollMin) menuScrollMax=menuScrollMin;
|
if (menuScrollMax<menuScrollMin) menuScrollMax=menuScrollMin;
|
||||||
|
|
||||||
const touchHandler = (_,e)=>{
|
const touchHandler = (_,e)=>{
|
||||||
if (e.y<R.y-4) return;
|
if (e.y<R.y-4) return;
|
||||||
velocity = 0;
|
velocity = 0;
|
||||||
|
@ -39,7 +46,6 @@
|
||||||
if ((menuScrollMin<0 || i>=0) && i<options.c){
|
if ((menuScrollMin<0 || i>=0) && i<options.c){
|
||||||
let yAbs = (e.y + rScroll - R.y);
|
let yAbs = (e.y + rScroll - R.y);
|
||||||
let yInElement = yAbs - i*options.h;
|
let yInElement = yAbs - i*options.h;
|
||||||
print("selected");
|
|
||||||
options.select(i, {x:e.x, y:yInElement});
|
options.select(i, {x:e.x, y:yInElement});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -51,30 +57,33 @@
|
||||||
for (var i=a;i<=b;i++)
|
for (var i=a;i<=b;i++)
|
||||||
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
|
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
|
||||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||||
}
|
};
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
let dy = velocity;
|
if (velocity > MIN_VELOCITY){
|
||||||
if (s.scroll - dy > menuScrollMax){
|
if (!scheduledDraw)
|
||||||
dy = s.scroll - menuScrollMax;
|
scheduledDraw = setTimeout(draw, 0);
|
||||||
|
velocity *= 1-((Date.now() - lastTouchedDrag) / 8000);
|
||||||
|
if (velocity <= MIN_VELOCITY){
|
||||||
|
velocity = 0;
|
||||||
|
} else {
|
||||||
|
s.scroll -= velocity * direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.scroll > menuScrollMax){
|
||||||
|
s.scroll = menuScrollMax;
|
||||||
velocity = 0;
|
velocity = 0;
|
||||||
}
|
}
|
||||||
if (s.scroll - dy < menuScrollMin){
|
if (s.scroll < menuScrollMin){
|
||||||
dy = s.scroll - menuScrollMin;
|
s.scroll = menuScrollMin;
|
||||||
velocity = 0;
|
velocity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
s.scroll -= dy;
|
|
||||||
|
|
||||||
let oldScroll = rScroll;
|
let oldScroll = rScroll;
|
||||||
rScroll = s.scroll &~1;
|
rScroll = s.scroll &~1;
|
||||||
let d = oldScroll-rScroll;
|
let d = oldScroll-rScroll;
|
||||||
|
|
||||||
if (Math.abs(velocity) > 0.01)
|
|
||||||
scheduledDraw = setTimeout(draw,0);
|
|
||||||
else
|
|
||||||
scheduledDraw = undefined;
|
|
||||||
|
|
||||||
if (!d) {
|
if (!d) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +92,7 @@
|
||||||
let y = Math.max(R.y2-(1-d), R.y);
|
let y = Math.max(R.y2-(1-d), R.y);
|
||||||
g.setClipRect(R.x,y,R.x2,R.y2);
|
g.setClipRect(R.x,y,R.x2,R.y2);
|
||||||
let i = YtoIdx(y);
|
let i = YtoIdx(y);
|
||||||
|
|
||||||
for (y = idxToY(i);y < R.y2;y+=options.h) {
|
for (y = idxToY(i);y < R.y2;y+=options.h) {
|
||||||
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
|
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
|
||||||
i++;
|
i++;
|
||||||
|
@ -93,69 +102,64 @@
|
||||||
g.setClipRect(R.x,R.y,R.x2,y);
|
g.setClipRect(R.x,R.y,R.x2,y);
|
||||||
let i = YtoIdx(y);
|
let i = YtoIdx(y);
|
||||||
y = idxToY(i);
|
y = idxToY(i);
|
||||||
|
|
||||||
for (y = idxToY(i);y > R.y-options.h;y-=options.h) {
|
for (y = idxToY(i);y > R.y-options.h;y-=options.h) {
|
||||||
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
|
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||||
|
scheduledDraw = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragHandler = e=>{
|
const dragHandler = e=>{
|
||||||
if ((velocity <0 && e.dy>0) || (velocity > 0 && e.dy<0)){
|
let now=Date.now();
|
||||||
velocity *= -1;
|
direction = Math.sign(e.dy);
|
||||||
accDy = 5 * velocity;
|
s.scroll -= e.dy;
|
||||||
}
|
|
||||||
//velocity += e.dy * (Date.now() - lastDrag);
|
|
||||||
if (e.b > 0){
|
if (e.b > 0){
|
||||||
if (!lastDragStart){
|
// Finger touches the display or direction has been reversed
|
||||||
lastDragStart = Date.now();
|
lastTouchedDrag = now;
|
||||||
|
if (!lastDragStart || (accDy * direction < 0 && e.dy * direction > 0)){
|
||||||
|
lastDragStart = lastTouchedDrag;
|
||||||
velocity = 0;
|
velocity = 0;
|
||||||
accDy = 0;
|
accDy = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
accDy += e.dy;
|
accDy += e.dy;
|
||||||
}
|
} else {
|
||||||
velocity = accDy / (Date.now() - lastDragStart) * MAX_VELOCITY;
|
// Finger has left the display, only start scrolling kinetically when the last drag event is close enough
|
||||||
|
if (now - lastTouchedDrag < LAST_DRAG_WAIT){
|
||||||
if (lastDragStart && e.b == 0){
|
velocity = direction * accDy / (now - lastDragStart) * SPEED;
|
||||||
accDy = 0;
|
}
|
||||||
lastDragStart = 0;
|
lastDragStart = 0;
|
||||||
}
|
}
|
||||||
|
draw();
|
||||||
velocity = E.clip(velocity,-MAX_VELOCITY,MAX_VELOCITY);
|
|
||||||
//lastDrag=Date.now();
|
|
||||||
if (!scheduledDraw){
|
|
||||||
scheduledDraw = setTimeout(draw,0);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let uiOpts = {
|
let uiOpts = {
|
||||||
mode : "custom",
|
mode : "custom",
|
||||||
back : options.back,
|
back : options.back,
|
||||||
drag : dragHandler,
|
drag : dragHandler,
|
||||||
touch : touchHandler,
|
touch : touchHandler,
|
||||||
redraw : uiDraw
|
redraw : uiDraw
|
||||||
}
|
};
|
||||||
|
|
||||||
if (options.remove) uiOpts.remove = () => {
|
if (options.remove) uiOpts.remove = () => {
|
||||||
if (scheduledDraw)
|
if (scheduledDraw)
|
||||||
clearTimeout(scheduledDraw);
|
clearTimeout(scheduledDraw);
|
||||||
clearInterval(scheduledBrake);
|
|
||||||
options.remove();
|
options.remove();
|
||||||
}
|
};
|
||||||
|
|
||||||
Bangle.setUI(uiOpts);
|
Bangle.setUI(uiOpts);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function idxToY(i) {
|
function idxToY(i) {
|
||||||
return i*options.h + R.y - rScroll;
|
return i*options.h + R.y - rScroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
function YtoIdx(y) {
|
function YtoIdx(y) {
|
||||||
return Math.floor((y + rScroll - R.y)/options.h);
|
return Math.floor((y + rScroll - R.y)/options.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
let s = {
|
let s = {
|
||||||
scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
|
scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
|
||||||
draw : () => {
|
draw : () => {
|
||||||
|
@ -165,14 +169,16 @@
|
||||||
for (let i=a;i<=b;i++)
|
for (let i=a;i<=b;i++)
|
||||||
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
|
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
|
||||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||||
}, drawItem : i => {
|
},
|
||||||
|
drawItem : i => {
|
||||||
let y = idxToY(i);
|
let y = idxToY(i);
|
||||||
g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2));
|
g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2));
|
||||||
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
|
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
|
||||||
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
|
||||||
}, isActive : () => Bangle.uiRedraw == uiDraw
|
},
|
||||||
|
isActive : () => Bangle.uiRedraw == uiDraw
|
||||||
};
|
};
|
||||||
|
|
||||||
let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
|
let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
|
||||||
s.draw(); // draw the full scroller
|
s.draw(); // draw the full scroller
|
||||||
g.flip(); // force an update now to make this snappier
|
g.flip(); // force an update now to make this snappier
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
(function(){E.showScroller=function(c){function k(a){return a*c.h+b.y-l}function h(a){return Math.floor((a+l-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,m=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,n=0|c.scrollMin,r=c.h*c.c-b.h;r<n&&(r=n);const t=()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);for(var a=h(b.y),d=Math.min(h(b.y2),c.c-1);a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},u=()=>{var a=e;f.scroll-a>r&&
|
(function(){E.showScroller=function(c){function k(b){return b*c.h+a.y-l}function h(b){return Math.floor((b+l-a.y)/c.h)}if(!c)return Bangle.setUI();let n,f=0,p=0,q=0,t=0,u=0,a=Bangle.appRect,m=0|c.scrollMin,r=c.h*c.c-a.h;r<m&&(r=m);const v=()=>{g.reset().clearRect(a).setClipRect(a.x,a.y,a.x2,a.y2);for(var b=h(a.y),d=Math.min(h(a.y2),c.c-1);b<=d;b++)c.draw(b,{x:a.x,y:k(b),w:a.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},w=()=>{.1<f&&(n||(n=setTimeout(w,0)),f*=
|
||||||
(a=f.scroll-r,e=0);f.scroll-a<n&&(a=f.scroll-n,e=0);f.scroll-=a;a=l;l=f.scroll&-2;a-=l;p=.01<Math.abs(e)?setTimeout(u,0):void 0;if(a){g.reset().setClipRect(b.x,b.y,b.x2,b.y2).scroll(0,a);if(0>a){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=h(a);for(a=k(d);a<b.y2;a+=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d++}else for(a=Math.min(b.y+a,b.y2),g.setClipRect(b.x,b.y,b.x2,a),d=h(a),k(d),a=k(d);a>b.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-
|
1-(Date.now()-t)/8E3,.1>=f?f=0:e.scroll-=f*q);e.scroll>r&&(e.scroll=r,f=0);e.scroll<m&&(e.scroll=m,f=0);var b=l;l=e.scroll&-2;if(b-=l){g.reset().setClipRect(a.x,a.y,a.x2,a.y2).scroll(0,b);if(0>b){b=Math.max(a.y2-(1-b),a.y);g.setClipRect(a.x,b,a.x2,a.y2);var d=h(b);for(b=k(d);b<a.y2;b+=c.h)c.draw(d,{x:a.x,y:b,w:a.w,h:c.h}),d++}else for(b=Math.min(a.y+b,a.y2),g.setClipRect(a.x,a.y,a.x2,b),d=h(b),k(d),b=k(d);b>a.y-c.h;b-=c.h)c.draw(d,{x:a.x,y:b,w:a.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-
|
||||||
1)}};let v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0<a.dy||0<e&&0>a.dy)e*=-1,m=5*e;0<a.b&&(q||(q=Date.now(),m=e=0),m+=a.dy);e=m/(Date.now()-q)*100;q&&0==a.b&&(q=m=0);e=E.clip(e,-100,100);p||(p=setTimeout(u,0))},touch:(a,d)=>{if(!(d.y<b.y-4)&&(m=e=0,a=h(d.y),(0>n||0<=a)&&a<c.c)){let x=d.y+l-b.y-a*c.h;print("selected");c.select(a,{x:d.x,y:x})}},redraw:t};c.remove&&(v.remove=()=>{p&&clearTimeout(p);clearInterval(w);c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,
|
1);n=void 0}};let x={mode:"custom",back:c.back,drag:b=>{let d=Date.now();q=Math.sign(b.dy);e.scroll-=b.dy;if(0<b.b){t=d;if(!u||0>p*q&&0<b.dy*q)u=t,p=f=0;p+=b.dy}else 150>d-t&&(f=q*p/(d-u)*100),u=0;w()},touch:(b,d)=>{d.y<a.y-4||(p=f=0,b=h(d.y),(0>m||0<=b)&&b<c.c&&c.select(b,{x:d.x,y:d.y+l-a.y-b*c.h}))},redraw:v};c.remove&&(x.remove=()=>{n&&clearTimeout(n);c.remove()});Bangle.setUI(x);let e={scroll:E.clip(0|c.scroll,m,r),draw:()=>{g.reset().clearRect(a).setClipRect(a.x,a.y,a.x2,a.y2);var b=h(a.y);let d=
|
||||||
n,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=h(b.y);let d=Math.min(h(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=k(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.uiRedraw==t},l=f.scroll&-2;f.draw();g.flip();return f}})()
|
Math.min(h(a.y2),c.c-1);for(;b<=d;b++)c.draw(b,{x:a.x,y:k(b),w:a.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:b=>{let d=k(b);g.reset().setClipRect(a.x,Math.max(d,a.y),a.x2,Math.min(d+c.h,a.y2));c.draw(b,{x:a.x,y:d,w:a.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.uiRedraw==v},l=e.scroll&-2;e.draw();g.flip();return e}})()
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "kineticscroll",
|
{ "id": "kineticscroll",
|
||||||
"name": "Kinetic Scroll",
|
"name": "Kinetic Scroll",
|
||||||
"shortName":"Kinetic Scroll",
|
"shortName":"Kinetic Scroll",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "Replacement for the system scroller with kinetic scrolling.",
|
"description": "Replacement for the system scroller with kinetic scrolling.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
|
|
Loading…
Reference in New Issue