Пилю тач интерфейс для телефонов с драггингом. Когда на странице есть скроллбар
touchmove
срабатывает всего пару раз в секунду, в результате чего драггинг получается ужасно дёрганый. Однако проблема не воспроизводится если скроллбара на странице нет, а также при тестировании в хромовском эмуляторе и при подключении консолью разработчика к хрому в реальном андроиде.
Если добавить
touch-events: none
в CSS, то проблема уходит, но перестаёт работать и нативный скроллинг. Попытки изобретения велосипеда в виде JS-скроллинга и ручного перендеринга видимой части списка силами Vue упираются во внутреннюю ошибка рендеринга Vue, с которой тоже пока непонятно что делать.
Демо:
https://codepen.io/kyrsquir/full/QWNZzav
Код компонента Vue:
<template>
<div class="container">
<div
:class="containerClass"
:style="containerStyle"
@touchend="touchEndHandler"
@touchmove="touchMoveHandler"
@touchstart="touchStartHandler"
@transitionend="transitionEndHandler"
class="scroller-container"
>
<svg viewBox="0 0 100 20" always-swipeable="true" class="drag-handle">
<polyline points="27,10 73,10" stroke-linecap="round"></polyline>
</svg>
<div class="scrollbox" ref="scrollbox">
<div :key="item" class="item" v-for="item in items">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
deltaY: 0,
isDraggingByHandle: false,
isTransitioning: false,
items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
offsetHeight: 0,
scrollboxHeight: 0,
scrollTop: 0,
touchAction: null,
verticalStates: [
{
translateY: 0,
},
{
translateY: window.innerHeight - 200,
},
],
verticalStateIndex: 0,
}
},
computed: {
activeVerticalState() {
return this.verticalStates[this.verticalStateIndex]
},
containerClass() {
return {
'show-scrollbar': this.isScrollable,
transition: this.isTransitioning,
}
},
containerStyle() {
return {
transform: `translateY(${this.translateY}px)`,
}
},
isAnySwipe() {
return this.isDraggingByHandle || !this.isScrollable
},
isExpanded() {
return this.verticalStateIndex === 0
},
isScrollable() {
return this.isExpanded && this.scrollHeight > this.offsetHeight
},
isSwipeDown() {
return this.deltaY > 0 && (this.isAnySwipe || this.scrollTop === 0)
},
isSwipeUp() {
return (
this.deltaY < 0 &&
(this.isAnySwipe || this.offsetHeight + this.scrollTop === this.scrollHeight)
)
},
scrollbox() {
return this.$refs.scrollbox
},
translateY() {
let translateY = this.activeVerticalState.translateY
if (this.touchAction === 'verticalSwipe' && (this.isSwipeDown || this.isSwipeUp)) {
translateY = translateY + this.deltaY
}
return translateY
},
},
mounted() {
this.updateScrollboxData()
},
methods: {
touchStartHandler: function({touches, target}) {
this.updateScrollboxData()
this.isDraggingByHandle = Boolean(target.getAttribute('always-swipeable'))
this.touchStartCoordinates = touches[0]
this.touchAction = 'tap'
},
touchMoveHandler: function(event) {
this.updateScrollboxData()
this.deltaY = event.touches[0].clientY - this.touchStartCoordinates.clientY
// promote touchAction to swipe or scroll depending on deltas and other variables
if (this.touchAction === 'tap') {
if (this.isSwipeDown || this.isSwipeUp) {
this.touchAction = 'verticalSwipe'
} else {
this.touchAction = 'scroll'
}
}
},
touchEndHandler: function() {
switch (this.touchAction) {
case 'tap':
if (!this.isExpanded) {
this.verticalStateIndex = Math.max(this.verticalStateIndex - 1, 0)
this.isTransitioning = true
}
break
case 'verticalSwipe':
if (this.isSwipeDown) {
this.verticalStateIndex = Math.min(
this.verticalStateIndex + 1,
this.verticalStates.length - 1
)
} else if (this.isSwipeUp) {
this.verticalStateIndex = Math.max(this.verticalStateIndex - 1, 0)
}
this.isTransitioning = true
this.deltaY = 0
break
}
},
transitionEndHandler() {
this.touchAction = null
this.isTransitioning = false
this.updateScrollboxData()
},
updateScrollboxData() {
const {scrollHeight, offsetHeight, scrollTop} = this.scrollbox
this.offsetHeight = offsetHeight
this.scrollHeight = scrollHeight
this.scrollTop = scrollTop
},
},
}
</script>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
margin-top: 5vh;
overflow: hidden;
.scroller-container {
pointer-events: all;
width: 90vw;
height: 100%;
&.transition {
transition: transform 0.15s ease-out;
}
.drag-handle {
stroke-width: 5px;
stroke: #bfbfc0;
width: 100%;
height: 30px;
background: pink;
}
.scrollbox {
overflow-y: hidden;
pointer-events: none;
background: green;
height: 85vh;
.item {
margin-bottom: 20px;
height: 150px;
font-size: 36px;
background: yellow;
}
}
&.show-scrollbar .scrollbox {
overflow-y: scroll;
pointer-events: all;
}
}
}
</style>