Схема полосы прокрутки на основе React

React.js

Увидев заголовок, вы должны спросить:

Зачем делать еще одно колесо?IScrollРазве это не легко? ЕстьBetter-ScrollА?

Обе эти библиотеки хороши, и я обычно ими пользуюсь по двум причинам:

  1. Не соответствует парадигме React:ui = f(state)

Обе библиотеки являются кроссплатформенными и обе работают напрямую.domДа, кроссплатформенность не плоха, это однозначно хорошо, но в мире React, чтобы разобраться с синхронизацией состояния, обычно управляется состоянием или атрибутами, хотя можно обернуть две вышеуказанные библиотеки с React, чтобы обеспечить версия React, но она всегда кажется далеко не идеальной.

  1. Необоснованный спрос на продукцию

Команда предоставляет коммерческие продукты на стороне ПК для стороны B, что требует хорошего интерактивного опыта.

Продукт сказал: Можно ли изменить полосу прокрутки системы?

Я сказал: вы можете изменить это, Chrome может изменить это, но FireFox не может изменить это,

Продукт говорит: может ли мышь увеличиваться при наведении?

Я сказал: попробую, Эдег уже поддерживает, Хром работает тяжело, а другие вроде не работают

Продукт сказал: Посмотрите на полосу прокрутки в таблице здесь, вы можете вывести ее в сторону браузера?

Я сказал: Блин, эта форма внутри, там несколько гор от браузера, это отдельный компонент

Продукт говорит: Как мне перетащить страницу, чтобы страница прокручивалась, не перетаскивая полосу прокрутки

Я сказал: сенсорный экран на мобильном терминале в порядке, на ПК можно использовать тачпад

Продукт говорит: Почему моя сенсорная панель не работает

Я сказал: да, на Mac вам, кажется, нужно установить драйвер для сенсорной панели TinkPad, вы можете использовать колесико мыши.

Подводя итог, вы понимаете наши требования к продукту? Вот моя первая мысль:

Если хочешь расти, то перед лицом необоснованных требований продакт-менеджеров, если хочешь отказаться, то должен отказаться с чистой совестью

На самом деле отказаться легко, всегда есть причина, и стоимость самая низкая, но я всегда чувствую, что этого точно можно добиться, но к сожалению временные затраты немного высоки, так много багов, и это не может быть изменено два дня? Одного-двух дней может не хватить, так что не могу сказать, что есть какие-то проблемы

Конечно, я все равно отказался от продукта и сосредоточился на исправлении ошибок блокировки.Потом, воспользовавшись выходными, я хорошо придумал, что делать с этой полосой прокрутки.Это было неожиданностью для продукта, хотя он не понимала, как это жалко...

Цели дизайна

  1. Близок к родному, прост в использовании, легко переключается с полосы прокрутки по умолчанию на новую полосу прокрутки.

Родное письмо:

<div className="container"
    style={{
        width: 500,
        height: 400,
        overflow:'auto'
    }}
    onScroll={onScroll}
>
    <div className="content" ref="content" style={{
        width: 1000,
        height: 800,
    }}>
        {content}
    </div>
</div>

Только нужно изменить этикетку контейнера, после замены:

<Scroll className="container"
    style={{
        width: 500,
        height: 400,
        overflow:'auto'
    }}
    onScroll={onScroll}
>
    <div className="content" ref="content" style={{
        width: 1000,
        height: 800,
    }}>
        {content}
    </div>
</Scroll>
  1. onScrollИнтерфейс соответствует оригиналу, не затрагивая исходную бизнес-логику
export interface IScrollEvent {
    target: {
        scrollLeft: number;
        scrollTop: number;
    };
}
export interface IScrollProps{
    /**
     * 滚动条距离左侧的距离
     */
    scrollLeft?: number;
    /**
     * 滚动条距离顶部距离
     */
    scrollTop?: number;
    onScroll?: (e: IScrollEvent) => void;
}
  1. служба поддержкиscrollLeftа такжеscrollTopСвойство изменяет положение полосы прокрутки, а экземпляр объекта также предоставляет следующие свойства, совместимые с собственным API DOM.
export interface IScroll {
    scrollLeft: number;
    scrollTop: number;
    /**
     * 滚动到指定位置
    */
    scrollTo: (left: number, top: number) => void;
    /**
     * 滚动相对距离
    */
    scrollBy: (left: number, top: number) => void;
    /**
     * 重新计算滚动区域
    */
    refresh: () => void;
}

проблемы, с которыми столкнулись

  1. Как заставить элементы двигаться

можно использовать绝对定位илиtransform ,transformпредпочтительнее, потому что он может поддерживатьgpuУскорение, обработка анимации прокрутки будет лучше

  1. Как поддерживать перетаскивание

Конечно мониторингmousedown,mousemove,mouseup, рассчитать направление движения мыши и относительное расстояние, а затем определить положение элемента, которое мобильный терминал должен отслеживатьtouchstart, touchmove, touchendмероприятие

  1. Как поддерживать сенсорную панель

Проверьте это, сенсорная панель ПК может пойтиonwheelСобытие — это событие прокрутки колесика мыши, которое может поддерживать сенсорную панель, и мой ThinkPad также может его поддерживать.

  1. Как сделать анимацию прокрутки

Если использовать только полосы прокрутки, то без анимации можно обойтись, но при перетаскивании обязательно должна быть анимация прокрутки, как в CSS3ease-in ease-outанимация, вы можете использоватьsetIntervalилиrequestAnimationFrameapi, вы можете сначала сделать линейное движение с равномерным замедлением, а давайте поговорим о других анимациях.

  1. если поддерживаетсяui = f(state)Парадигма, частое изменение состояния, повторный рендеринг, есть ли проблемы с производительностью?

По сравнению с прямой модификацией dom производительность определенно снижена, но находится в пределах допустимого.

Вышеупомянутые проблемы в основном решаемы, и проблем с блокировкой нет.Следующее является реализацией:

выполнить

Перетаскивать

Перетаскивание мышиonMouseDown onMouseMove onMouseUpСобытия, общий процесс выглядит следующим образом:

  1. onMouseDownЗапишите исходное положение мыши в событииpointStart,дляdocumentрегистрmousemoveа такжеmouseupмероприятие
  2. onMouseMoveМышь перемещается в событии, записываем текущую позицию точки мышиEnd, минусpopointEnd - pointStartПолучить смещение мыши, установитьsecrollLeft, страница прокручивается.
  3. onMouseUpсобытие, получить мышь即时速度, если скорость0, то движение прекращается, если скорость больше0, выполнить анимацию прокрутки, удалитьdocumentизmousemoveа такжеmouseupмероприятие

Нет дополнения к корневому узлу области прокруткиmousemoveа такжеmouseupмероприятие, датьdocumentс мышьюmousemoveа такжеmouseupсобытие, потому что диапазон движения мыши может превышать область прокрутки, если область прокрутки превышена, эти два события больше не будут выполняться

Мгновенный расчет скорости

После того, как мышь поднята, вам нужно знать скорость движения, а затем тормозить на этой скорости, поэтому вам нужно рассчитать即时速度, здесь нельзя использовать среднюю скорость.

рассчитать即时速度надо знать距离а также时间, после щелчка мышьюsetInvervalтаймер, каждый100msЗапишите положение и отметку времени мыши.После того, как мышь будет поднята, завершите расчет, получите текущую позицию и время и сделайте разницу с исторической позицией и отметкой времени, чтобы получить окончательный результат.100msСкорость в пределах , рассчитывается следующим образом:

/**
 * 启动即时速度计算
 */
startCaclRealV = () => {
    const me = this;
    const t = _REAL_VELOCITY_TIMESPAN;
    const timer = setInterval(() => {
        if (!me.isDraging) {
            clearInterval(timer);
            return;
        }
        if (!me.lastPos) {
            me.lastTime = Date.now();
            me.lastPos = me.endPoint;
            return;
        }
        me.lastTime = Date.now();
        me.lastPos = me.endPoint;
    }, t);
    return {
        destroy() {
            clearInterval(timer);
        },
    }
}
/**
 * 计算即时速度
 */
caclRealV = () => {
    const me = this;
    if (!me.lastPos) {
        return {
            realXVelocity:0,
            realYVelocity:0
        }
    }
    const time = (Date.now() - me.lastTime) / 1000;
    const xdist = Math.abs(me.endPoint.x - me.lastPos.x);
    const ydist = Math.abs(me.endPoint.y - me.lastPos.y);
    return {
        realXVelocity:caclVelocity(xdist, time),
        realYVelocity:caclVelocity(ydist, time),
    }
}

анимация прокрутки

После поднятия мыши即时速度Начните движение замедления, здесь вы можете использовать функцию замедления для расчета положения, установитьscrollLeft, здесь я использовал движение равномерного замедления, используяrequestAnimationFrameВыполните цикл анимации, используяtransform: translate3d(0,${indicateTop}px,0)Установите смещение, вы можете запустить ускорение пгу,

Ссылка на код:

import { TDirection, TPoint } from './types'

/**
 * 动画执行函数
 * @param v 速度 像素/秒
 * @param a 减速度 像素/秒平方
 * @param onMove 回调函数,返回移动距离
 * @param onEnd 回调函数,终止动画
 */
export const animate = (v: number, a: number, onMove: (dist) => boolean, onEnd: () => void): { destroy: () => void } => {
    const t = 16;// ms
    const start = Date.now();
    return loopByFrame(t, () => {
        const time = (Date.now() - start) / 1000;
        if (time === 0) {
            return true;
        }
        const dist = move(v, a, time);
        if (dist === 0) {
            return false;
        }
        return onMove(dist);
    }, onEnd);
}

/**
 * 利用 requestAnimationFrame 执行动画循环
 * @param duration 动画时间间隔,使用 requestAnimationFrame 不需要设置
 * @param onMove 动画执行函数
 * @param onEnd 动画终止函数
 */
export const loopByFrame = (duration = 16, onMove = () => true, onEnd = () => void 0): { destroy: () => void } => {

    let animateFrame;
    function step(func, end = () => void 0) {
        if (!func) {
            end();
            return;
        }

        if (!func()) {
            destroy();
            end();
            return;
        }

        animateFrame = window.requestAnimationFrame(() => {
            step(func, end);
        });
    }
    function destroy() {
        if (animateFrame) {
            window.cancelAnimationFrame(animateFrame);
        }
    }

    step(onMove, onEnd);

    return {
        destroy,
    }
}

/**
 * 利用 setInterval 执行函数循环
 * @param duration 时间间隔
 * @param cb 回调函数
 * @param onEnd 终止函数
 */
export const loopByInterval = (duration = 16, cb = () => true, onEnd = () => void 0): { destroy: () => void } => {
    const timer = setInterval(() => {
        if (!cb()) {
            clearInterval(timer);
            onEnd();
        }
    }, duration);
    return {
        destroy() {
            clearInterval(timer);
            onEnd();
        },
    }
}

/**
 * 计算以速度 v ,减速度 a,运动 time 时间的距离
 * @param v 速度
 * @param a 减速度
 * @param time 时间
 */
export const move = (v: number, a: number, time: number) => {
    // 获取下一时刻速度,如果速度为 0 终止
    const nextV = caclNextVelocity(v, a, time);
    if (nextV <= 0) {
        return 0;
    }
    // 计算下一刻的距离
    const dist = caclDist(v, time, a);
    return dist;
}

/**
 * 计算滚动方向,暂时只支持横向滚动
 * @param start 起始点
 * @param end 终点
 */
export const caclDirection = (start: TPoint, end: TPoint): TDirection => {
    const xLen = (end.x - start.x);
    const yLen = (end.y - start.y);
    if (Math.abs(xLen) > Math.abs(yLen)) {
        return xLen > 0 ? 'right' : 'left';
    } else {
        return yLen > 0 ? 'bottom' : 'top';
    }
}

/**
 * 减速直线运动公式,计算距离
 * @param v 速度
 * @param t 时间 单位秒
 * @param a 加速度
 */
export const caclDist = (v: number, t: number, a: number) => {
    return v * t - (a * t * t) / 2;
}

/**
 * 计算速度
 * @param v0 初始速度
 * @param a 加速度
 * @param t 时间
 */
export const caclNextVelocity = (v0: number, a: number, t: number) => {
    return v0 - a * t;
}

/**
 * 计算速度
 * @param dist 距离 单位像素
 * @param time 时间 单位秒
 */
export const caclVelocity = (dist: number, time: number) => {
    if (time <= 0) {
        return 0;
    }
    return dist / time;
}

Синхронизация полосы прокрутки

Вы можете использовать этот режим, когда вам нужно синхронизировать несколько полос прокрутки на странице, например, в нашей системе необходимо синхронизировать заголовок, тело и панель инструментов.

Синхронизация полосы прокрутки Первоначально мы использовали собственную полосу прокрутки системы.scrollLeftАтрибуты синхронизируются, но будет сильно лагать, сейчас б/уScrollкомпоненты, используя CSS3transformДля синхронизации эффект гораздо лучше.

Образец кода:

import React,{Component} from 'react';

class Demo extends Component{
    constructor(props,context){
        super(props,context);
        const me=this;
        me.state={
            scrollLeft:0,
            scrollTop:0,
        }
    }
    onScroll=(e)=>{
        const me=this;
        me.setState({
            scrollLeft:e.target.scrollLeft,
            scrollTop:e.target.scrollTop
        });        
    }
    render(){
        const me=this;
        const {
            scrollLeft,
            scrollTop
        }=me.state;
        return (            
            <Scroll
                scrollLeft={scrollLeft} 
                scrollTop={scrollTop}
                className="container"
                style={{ width: 500, height: 400,}}
                onScroll={me.onScroll}
            >
                <div className="content" ref="content" 
                    style={{width: 1000,height: 800}}>
                    
                </div>
            </Scroll>      
             <Scroll
                scrollLeft={scrollLeft}
                scrollTop={scrollTop}
                className="container"
                style={{width: 500,height: 400,}}
                onScroll={me.onScroll}
            >
                <div className="content" ref="content" 
                    style={{width: 1000,height: 800,}}>
                    
                </div>
            </Scroll>            
        )
    }
}

Показать результаты

滚动列表

滚动同步

滚动同步

наконец

В этой статье делается попытка реализовать полосу прокрутки отдельно, чтобы решить проблему непоследовательного поведения полос прокрутки различных браузеров на стороне ПК. Она совместима с собственным API и может обеспечить плавное переключение. Общая сложность реализации умеренная. В основном необходимо обратить внимание на реализацию анимации плавности, и есть некоторые проблемы.Есть ли лучший способ?

  1. 即时速度Расчет, есть ли лучший способ?
  2. Текстовая анимация использует равномерное замедление линейного движения, последующие действия не могут обеспечить множество потребностей в анимации?
  3. Мобильный терминал в настоящее время не поддерживается, и можно ли поддерживать мобильный?