Skip to content

Commit ca04123

Browse files
committed
fix: improve sheet scrolling to feel more native
1 parent 03e02a8 commit ca04123

1 file changed

Lines changed: 54 additions & 18 deletions

File tree

apps/mobile/v1/src/components/SheetsViewer.tsx

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -400,13 +400,20 @@ export function SheetsViewer({ content, theme }: SheetsViewerProps) {
400400
let scrollY = 0;
401401
let velocityX = 0;
402402
let velocityY = 0;
403+
let lastX = 0;
404+
let lastY = 0;
403405
let lastTime = 0;
404406
let momentumId = null;
407+
let isTracking = false;
405408
406409
// Pinch zoom state
407410
let initialPinchDistance = 0;
408411
let currentZoom = 1;
409412
413+
// iOS-like deceleration rate (0.998 is close to UIScrollView)
414+
const DECELERATION = 0.985;
415+
const MIN_VELOCITY = 0.1;
416+
410417
function doScroll() {
411418
const sheetId = getSheetId();
412419
if (unitId && sheetId) {
@@ -424,13 +431,15 @@ export function SheetsViewer({ content, theme }: SheetsViewerProps) {
424431
}
425432
426433
function momentumScroll() {
427-
if (Math.abs(velocityX) < 0.5 && Math.abs(velocityY) < 0.5) {
434+
if (Math.abs(velocityX) < MIN_VELOCITY && Math.abs(velocityY) < MIN_VELOCITY) {
428435
momentumId = null;
429436
return;
430437
}
431438
432-
velocityX *= 0.95;
433-
velocityY *= 0.95;
439+
// Apply iOS-like deceleration
440+
velocityX *= DECELERATION;
441+
velocityY *= DECELERATION;
442+
434443
scrollX = Math.max(0, scrollX + velocityX);
435444
scrollY = Math.max(0, scrollY + velocityY);
436445
doScroll();
@@ -444,41 +453,53 @@ export function SheetsViewer({ content, theme }: SheetsViewerProps) {
444453
}
445454
446455
overlay.addEventListener('touchstart', function(e) {
456+
// Stop any ongoing momentum
447457
if (momentumId) {
448458
cancelAnimationFrame(momentumId);
449459
momentumId = null;
450460
}
451461
452462
if (e.touches.length === 1) {
463+
isTracking = true;
453464
startX = e.touches[0].pageX;
454465
startY = e.touches[0].pageY;
455-
lastTime = Date.now();
466+
lastX = startX;
467+
lastY = startY;
468+
lastTime = performance.now();
456469
velocityX = 0;
457470
velocityY = 0;
458471
} else if (e.touches.length === 2) {
472+
isTracking = false;
459473
initialPinchDistance = getPinchDistance(e.touches);
460474
}
461475
}, { passive: true });
462476
463477
overlay.addEventListener('touchmove', function(e) {
464-
if (e.touches.length === 1) {
478+
if (e.touches.length === 1 && isTracking) {
465479
const currentX = e.touches[0].pageX;
466480
const currentY = e.touches[0].pageY;
467-
const now = Date.now();
468-
const dt = Math.max(1, now - lastTime);
469-
470-
const deltaX = (startX - currentX) * 1.5;
471-
const deltaY = (startY - currentY) * 1.5;
472-
473-
velocityX = deltaX * (16 / dt);
474-
velocityY = deltaY * (16 / dt);
481+
const now = performance.now();
482+
const dt = now - lastTime;
483+
484+
// Direct 1:1 scrolling during drag
485+
const deltaX = lastX - currentX;
486+
const deltaY = lastY - currentY;
487+
488+
// Track velocity with time-weighted smoothing
489+
if (dt > 0) {
490+
const newVelX = (deltaX / dt) * 16; // normalize to ~60fps
491+
const newVelY = (deltaY / dt) * 16;
492+
// Smooth velocity to prevent jitter
493+
velocityX = velocityX * 0.4 + newVelX * 0.6;
494+
velocityY = velocityY * 0.4 + newVelY * 0.6;
495+
}
475496
476497
scrollX = Math.max(0, scrollX + deltaX);
477498
scrollY = Math.max(0, scrollY + deltaY);
478499
doScroll();
479500
480-
startX = currentX;
481-
startY = currentY;
501+
lastX = currentX;
502+
lastY = currentY;
482503
lastTime = now;
483504
} else if (e.touches.length === 2 && initialPinchDistance > 0) {
484505
const newDistance = getPinchDistance(e.touches);
@@ -498,10 +519,25 @@ export function SheetsViewer({ content, theme }: SheetsViewerProps) {
498519
}, { passive: true });
499520
500521
overlay.addEventListener('touchend', function(e) {
501-
if (e.touches.length === 0 && (Math.abs(velocityX) > 1 || Math.abs(velocityY) > 1)) {
502-
momentumId = requestAnimationFrame(momentumScroll);
522+
if (e.touches.length === 0 && isTracking) {
523+
isTracking = false;
524+
// Only start momentum if we have meaningful velocity
525+
if (Math.abs(velocityX) > 0.5 || Math.abs(velocityY) > 0.5) {
526+
momentumId = requestAnimationFrame(momentumScroll);
527+
}
528+
}
529+
if (e.touches.length < 2) {
530+
initialPinchDistance = 0;
531+
}
532+
}, { passive: true });
533+
534+
// Handle touch cancel (e.g., incoming call)
535+
overlay.addEventListener('touchcancel', function(e) {
536+
isTracking = false;
537+
if (momentumId) {
538+
cancelAnimationFrame(momentumId);
539+
momentumId = null;
503540
}
504-
initialPinchDistance = 0;
505541
}, { passive: true });
506542
}, 800);
507543

0 commit comments

Comments
 (0)