K
K
kyrsquir2020-09-20 21:00:35
JavaScript
kyrsquir, 2020-09-20 21:00:35

How to improve touchmove performance in the presence of a scrollbar in Chrome on Android?

Pilyu touch interface for phones with dragging. When there is a scrollbar on the page, it touchmovefires only a couple of times per second, resulting in terribly jerky dragging. However, the problem does not reproduce if there is no scrollbar on the page, as well as when testing in the chrome emulator and when connecting the developer console to chrome in a real android.

If added touch-events: noneto CSS, then the problem goes away, but native scrolling also stops working. Attempts to invent a bicycle in the form of JS scrolling and manually rendering the visible part of the list by Vue run into an internal Vue rendering error, which is also not yet clear what to do.

Demo:
https://codepen.io/kyrsquir/full/QWNZzav

Vue component code:

<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>

Answer the question

In order to leave comments, you need to log in

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question