Akryum / vue-virtual-scroller

⚡️ Blazing fast scrolling for any amount of data
https://vue-virtual-scroller-demo.netlify.app
9.74k stars 914 forks source link

Implement scroll snaping #801

Open patchthecode opened 1 year ago

patchthecode commented 1 year ago

Clear and concise description of the problem

When we scroll there is no snapping with this library. This lets the list scroll until decelerated, but many times for visual purposes, it would be nice that it decelerates with the object in the center of the screen.

Suggested solution

Im not sure how to implement this with this library, but with regular html and css, its as simple as giving the parent div the

scoll-snap-type: x;

and giving the child div

scroll-snap-align: center; // or the other options

Alternative

No response

Additional context

related issue. but dead.. no replies or comments for years... https://github.com/Akryum/vue-virtual-scroller/issues/543

Validations

phil294 commented 1 year ago

Here's my workaround based on a bit of testing and tweaking - you might have to adjust it to your needs. It works great for my purposes, even feels a bit "bouncy".

<template>
    <recycle-scroller :items="items" key-field="i" size-field="scroll_height" :buffer="0" :emit-update="true" @update="scroller_updated" ref="commits_scroller_ref" tabindex="-1"/>
</template>
<script>
// ...
let scroll_item_offset = 0
let debouncer = 0
let ignore_next_scroll_event = false
const commits_scroller_updated = (start_index, end_index) => {
    if(ignore_next_scroll_event) {
        ignore_next_scroll_event = false
        return
    }
    window.clearTimeout(debouncer)
    debouncer = window.setTimeout(() => {
        // +1 because vue-virtual-scroller's start index is always one too far up it seems, but
        // check for !=0 because when we're at the very top, we want to stay there.
        // And once we have scrolled, we need to go to at least index 3 because
        // `scroller.scrollToItem(2)` actually results in the scroller to jump to 0 again.
        // Not sure if these tweaks (+1, !=0 and 3) are row height or container dependent, so
        // if it doesn't work for you, you'll have to tweak this line.
        const new_scroll_item_offset = start_index > 0 ? Math.max(3, start_index + 1) : 0
        // To avoid jumping back and forth while keeping the scroll bar pressed down with mouse:
        if(new_scroll_item_offset === scroll_item_offset)
            return
        scroll_item_offset = new_scroll_item_offset
        commits_scroller_ref.value?.scrollToItem(scroll_item_offset)
        // To avoid endless loops:
        ignore_next_scroll_event = true
    }, 70)
}
bitbytebit1 commented 1 year ago

Here's my solution, only tested in horizontal mode

export default function scrollSnap () {
  let element: HTMLElement
  const start = {
    scrollY: 0,
    scrollX: 0,
    touchX: 0,
    touchY: 0,
    time: 0,
  }
  let scrollingDirection: null | 'vertical' | 'horizontal' = null

  function onTouchStart (event: TouchEvent) {
    start.touchX = event.touches[0].clientX
    start.touchY = event.touches[0].clientY
    start.scrollX = element.scrollLeft
    start.scrollY = element.scrollTop
    start.time = Date.now()
    scrollingDirection = null
  }

  function onTouchMove (event: TouchEvent) {
    const touchY = event.touches[0].pageY
    const touchX = event.touches[0].pageX
    const distanceY = start.touchY - touchY
    const distanceX = start.touchX - touchX

    if (scrollingDirection === null) {
      scrollingDirection = Math.abs(distanceX) > Math.abs(distanceY) ? 'horizontal' : 'vertical'
    }

    if (Math.abs(distanceX) > Math.abs(distanceY)) {
      event.preventDefault()
    }
    if (scrollingDirection === 'horizontal') {
      event.preventDefault()
      element.scrollLeft = start.scrollX + distanceX
    } else if (scrollingDirection === 'vertical') {
      // event.preventDefault()
      element.scrollTop = start.scrollY + distanceY
    }
  }

  function onTouchEnd (event: TouchEvent) {
    const elapsedTime = Date.now() - start.time

    // Vertical velocity calculation
    const distanceY = element.scrollTop - start.scrollY
    const velocityY = distanceY / elapsedTime

    // Multiplier to control the effect of the swipe velocity on scrolling
    const multiplier = 150

    // Predicted end position for vertical scroll
    const predictedEndY = element.scrollTop + (velocityY * multiplier)

    // Horizontal snap logic
    const distanceX = element.scrollLeft - start.scrollX
    const velocityX = distanceX / elapsedTime
    const thresholdVelocity = 0.5

    let targetPositionX
    if (Math.abs(velocityX) > thresholdVelocity) {
      targetPositionX = velocityX > 0 ? Math.ceil(element.scrollLeft / element.clientWidth) : Math.floor(element.scrollLeft / element.clientWidth)
    } else {
      targetPositionX = Math.round(element.scrollLeft / element.clientWidth)
    }

    element.scrollTo({
      top: predictedEndY,
      left: targetPositionX * element.clientWidth,
      behavior: 'smooth',
    })
    scrollingDirection = null
  }

  function bind (el: HTMLElement) {
    element = el
    element.addEventListener('touchstart', onTouchStart)
    element.addEventListener('touchmove', onTouchMove)
    element.addEventListener('touchend', onTouchEnd)
  }

  function unbind (element: HTMLElement) {
    element.removeEventListener('touchstart', onTouchStart)
    element.removeEventListener('touchmove', onTouchMove)
    element.removeEventListener('touchend', onTouchEnd)
  }

  return {
    bind,
    unbind,
  }
}
srackhall commented 8 months ago

Excuse me, have you found a good solution so far?

I may need this feature at the moment, but the virtual window has caused a benchmark change in CSS, making it difficult to implement.

I tried the methods of the two above, but they did not achieve the expected results.