When you see the headline, be sure to ask:

Why build another wheel? IScroll doesn’t work well? And the better-Scroll?

There are only two reasons for doing this:

  1. Doesn’t fit the React paradigm:ui = f(state)

Both libraries are cross-platform and operate dom directly. Cross-platform is good, but in the React world, synchronization of states is usually controlled by states or properties. Although you can use React to cover both libraries and provide React versions, it is not always perfect.

  1. Unreasonable demand for products

The team provides commercial products from the PC side to the B side, requiring good interactive experience

Product says: can the scrollbar of the system be changed?

I said, you can change it, You can change Chrome, but you can’t change FireFox,

Product says: can the mouse Hover when bigger?

I said, “I’ll try, Edeg already supports it, Chrome will try, but the others don’t seem to.

The product says: Look at the scroll bar in the form here, can you bring it to the browser side

I said: Damn, this table is inside, a few mountains away from the browser, it’s a separate component

The product says: How can I drag and drop the page to scroll without dragging the scroll bar

I said: touch screen on mobile, you can use touch pad on PC

Product says: My trackpad doesn’t work

I said, “On a Mac, I think you need a driver for your TinkPad. You can use a mouse wheel.

Ok, to sum up, do you understand our product requirements? Let me first write a feeling of my own:

If you want to grow, say no to unreasonable demands from your product manager. Do so with a clean conscience

In fact, it’s easy to say no, there’s always a reason, and the cost is the lowest, but I always feel in my heart that this can definitely be done, but unfortunately the time cost is a little high, there are so many bugs, and I can’t change it. Why not do it in a day or two? Maybe a day or two. What’s the problem

Of course, I still refused the product, focused on fixing the bug of resistance, and then, taking advantage of the weekend, I had a good idea of how to do the scroll bar. If I promised the product, I would be embarrassed if I couldn’t do it, and when I did, IT was a surprise to the product, although he didn’t realize how bad it was…

Design goals

  1. Close to native, easy to use, easy to switch from the default scroll bar to a new scroll bar

Native writing:

<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>
Copy the code

Just change the container’s label and replace it with:

<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>
Copy the code
  1. onScrollThe interface is the same as the original interface, and the original service logic is not affected
export interface IScrollEvent {
    target: {
        scrollLeft: number;
        scrollTop: number;
    };
}
export interface IScrollProps{
    /** * The distance from the scroll bar to the left */scrollLeft? : number;/** * scrollbar distance from top distance */scrollTop? : number; onScroll? :(e: IScrollEvent) = > void;
}
Copy the code
  1. supportscrollLeftscrollTopProperty changes the scrollbar position, and the object instance also provides the following properties, compatible with the native DOM API
export interface IScroll {
    scrollLeft: number;
    scrollTop: number;
    /** ** scroll to the specified position */
    scrollTo: (left: number, top: number) = > void;
    /** * roll relative distance */
    scrollBy: (left: number, top: number) = > void;
    /** * recalculates the scroll area */
    refresh: (a)= > void;
}

Copy the code

Problems faced

  1. How do I move elements

You can use either absolute positioning or Transform, which is preferred because it can support GPU acceleration and handle scrolling animations better

  1. How to support drag and drop

Of course it’s going to listen for mousedown, Mousemove, mouseup, calculate the direction of the mouse movement, and the relative distance, and then determine the position of the element, and the mobile side is going to listen for touchStart, TouchMove, touchEnd events

  1. How to support touchpad

The PC trackpad can launch onWheel event, which is the mouse wheel scroll event. This can support trackpad, and my ThinkPad can also support it.

  1. How to do scroll animation

If you’re just using a scroll bar, you don’t need to animate it, but you need to have a scrolling animation, like ease-in ease-out animation in CSS3, you can use setInterval or requestAnimationFrame API, you can do a slow down in a straight line, We’ll talk about the other animations

  1. If the supportui = f(state)Paradigms, frequently changing state, re-rendering, are there performance issues?

There is definitely a performance discount compared to directly modifying the DOM, but within the acceptable range

The above problems are basically solvable, no blocking problems, the following is the implementation:

implementation

Drag and drop to move

The mouse drag and drop event is onMouseDown onMouseMove onMouseUp. The general process is as follows:

  1. onMouseDownEvent records the initial mouse positionpointStartfordocumentregisteredmousemovemouseupThe event
  2. onMouseMoveEvent mouse movement, record the current mouse position pointEnd, minuspopointEndpointStartGets the offset of the mouse, setsecrollLeft, the page scrolls.
  3. onMouseUpEvent to get the mouseInstant speed, if the velocity is0, then stop the movement if the velocity is greater than0, perform the scroll animation, removedocumentmousemovemouseupThe event

There are no mousemove and Mouseup events for the root of the scroll area, or mousemove and Mouseup events for the document, because the mouse may move beyond the scroll area, and these events are no longer executed if it does

Real-time velocity calculation

When the mouse is up, you need to know how fast it is moving, and then you need to slow down at that speed, so you need to calculate the instant speed, not the average speed.

It is necessary to know the distance and time to calculate the instant speed. After the mouse is clicked, the setInverval timer is used to record the position and time stamp of the mouse every 100ms. After the mouse is lifted, the calculation is terminated to obtain the current position and time, and the difference with the historical position and time stamp is made to obtain the speed within the last 100ms. The calculation is as follows:

/** * Start real-time speed calculation */
startCaclRealV = (a)= > {
    const me = this;
    const t = _REAL_VELOCITY_TIMESPAN;
    const timer = setInterval((a)= > {
        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); }}},/** * Calculate the real-time speed */
caclRealV = (a)= > {
    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),
    }
}

Copy the code

Scrolling animation

When the mouse is up, it starts to slow down at real time speed. Here you can use the slow function to calculate the position, set the scrollLeft, HERE I use the slow motion, use the requestAnimationFrame to execute the animation loop, use the Transform: Translate3d (0,${indicateTop}px,0) set offset to start PGU acceleration

Code reference:

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

/** * animation execution function * @param v speed pixel/second * @param a deceleration pixel/second squared * @param onMove callback function, return the moving distance * @param onEnd callback function, terminate animation */
export const animate = (v: number, a: number, onMove: (dist) = > boolean, onEnd: (a)= > void) : {destroy: (a)= > 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);
}

/** * Use requestAnimationFrame to execute animation loop * @param duration Animation interval, RequestAnimationFrame does not require setting @param onMove animation execution function @param onEnd animation termination function */
export const loopByFrame = (duration = 16, onMove = () = > true, onEnd = (a)= > void 0) : {destroy: (a)= > void} = > {let animateFrame;
    function step(func, end = () = >void 0) {
        if(! func) { end();return;
        }

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

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

    step(onMove, onEnd);

    return {
        destroy,
    }
}

/** * Use setInterval to execute function loop * @param duration time interval * @param cb callback * @param onEnd termination function */
export const loopByInterval = (duration = 16, cb = () = > true, onEnd = (a)= > void 0) : {destroy: (a)= > void} = > {const timer = setInterval((a)= > {
        if(! cb()) { clearInterval(timer); onEnd(); } }, duration);return{ destroy() { clearInterval(timer); onEnd(); }}},/** * calculate the distance in time * @param v speed * @param a deceleration * @param time */
export const move = (v: number, a: number, time: number) = > {
    // Get the speed of the next moment, terminate if the speed is 0
    const nextV = caclNextVelocity(v, a, time);
    if (nextV <= 0) {
        return 0;
    }
    // Calculate the distance to the next moment
    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'; }}/** * decelerate linear motion formula, calculate distance * @param v speed * @param t time unit of second * @param a acceleration */
export const caclDist = (v: number, t: number, a: number) = > {
    return v * t - (a * t * t) / 2;
}

/** * Calculated speed * @param v0 initial speed * @param a acceleration * @param t time */
export const caclNextVelocity = (v0: number, a: number, t: number) = > {
    return v0 - a * t;
}

/** * calculation speed * @param dist Distance in pixels * @param time time in seconds */
export const caclVelocity = (dist: number, time: number) = > {
    if (time <= 0) {
        return 0;
    }
    return dist / time;
}

Copy the code

Scrollbar synchronization

This mode can be used in cases where multiple area scrollbars need to be synchronized on a page, such as in our system, table header, table body and toolbar need to be synchronized

Scroll bar synchronization originally we used the scrollLeft attribute of the system’s own Scroll bar for synchronization, but it would be very slow. Now we use Scroll component and use CSS3’s Transform to synchronize, and the effect is much better.

Sample code:

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>            
        )
    }
}
Copy the code

Results show

The last

This paper tries to realize the scrollbar to solve the problem of inconsistent behavior of each browser on THE PC side, compatible with native API, can do seamless switching, the overall difficulty is medium, the main implementation of slow animation need to pay attention to some, is there a better way to some problems?

  1. Instant speedIs there a better way to calculate?
  2. The animation in this paper adopts linear motion with uniform deceleration. Is it possible to provide abundant animation to meet the needs in the future?
  3. Currently, mobile terminal is not supported. Can mobile terminal be supported in the future?