Ever wrote a short version of the swipe wheel component, did not consider many details and general configuration parameters, mainly is to record the implementation mentality, there would be no source code, in the years ago to pick up tidy up some kind of, and encapsulated into components, besides the react itself without any third-party dependencies, gzip compressed size is only 3.7 KB, Source code address, sample address

Thinking back

The core idea is to make use of the visual sense and switch back without the user feeling it. There is an idea that is a little different from before. The action of switch back is reset and reset during the switch, and the previous absolute layout is abandoned. The movement is mainly achieved by changing the outer container transform. The steps of seamless rotation are as follows

  1. The current position is shown in figure 3, and the red arrow is the viewable area of the mobile phone.
  2. Before moving to the right, move position 1 behind position 3 by transform
  3. Then move the outer container to complete the first seamless
  4. Before moving to the right again, reset position 1 to original position (fast moving)
  5. And move the outer container to position 1 to reach the viewable area (fast move)
  6. Move to the right to complete the second seamless. Left in the same way

After understanding the design idea, I began to design the component API and method, and the document is as follows

API

parameter instructions type The default value
autoplay Optional, automatic rotation interval, unit ms number 3000
duration Optional, animation duration in ms number 500
initialSwipe Optional, default location number 0
loop Optional: Whether to play in a loop boolean true
vertical Optional, whether to slide longitudinally boolean false
touchable Optional, whether can gesture swipe boolean true
showIndicators Optional, whether to display dot boolean true
style Optional, container style, height needs to be set for portrait object
onSlideChange Optional, switch index callback function(current)

methods

The name of the describe
slideTo(to, swiping) When swiping = true, no animation is used
next() Switch to the next index
prev() Switch to the previous index

Get ready and have fun implementing code against documentation!!

Step 1: Layout the page

Swipe verifies whether children are SwipeItem components by splitting the components into Swipe and SwipeItem components. Swipe is the main container and SwipeItem is the sub-item. Swipe uses Flex layout to verify whether children are SwipeItem components. The flexDirection itself can be displayed horizontally and vertically, so that the layout can be switched horizontally and vertically through the vertical attribute in the later stage. The core of the layout is to dynamically calculate the width of SwipeItem and the width of the moving container (SwipeItem width * SwipeItem number).

// Swipe.tsx
import React from 'react';
import SwipeItem from './SwipeItem';
import './style.less';

const Swipe:React.FC<SwipeProps> = (props) = > {
    const {
        initialSwipe = 0.// Default index
        vertical = false.// Whether longitudinal
        duration = 500.// Toggle the animation time
        autoplay = 3000.// Automatic playback interval
        touchable = true.// Whether gesture sliding is supported
        loop = true.// Whether seamless rotation
        showIndicators = true.// Whether to display dots
        onSlideChange
    } = props;
    
    // Count swipeItems
    const count = useMemo(() = > React.Children.count(props.children), [props.children]);
    // Get the width and height of the container
    const { size, root } = useRect<HTMLDivElement>([count]);
    // Get the height/width of SwipeItem
    const itemSize = useMemo(() = > vertical ? size.height : size.width, [size, vertical]);
    // Get whether SwipeItem should be set to height or width
    const itemKey = useMemo(() = > vertical ? 'height' : 'width', [vertical]);
    // Set the SwipeItem style
    const itemStyle = useMemo(() = > ({ [itemKey]: itemSize }), [itemKey, itemSize]);
    // Set the movement container style
    const wrappStyle = useMemo(() = > ({ [itemKey]: itemSize * count }), [count, itemSize, itemKey]);
    
    return (
    	<div ref={root} style={props.style} className="lumu-swipe">
            <div style={wrappStyle} className={`lumu-swipe__containerThe ${vertical ? 'lumu-swipe__vertical' :"'} `} >{ React.Children.map(props.children, (child, index) => { if (! React.isValidElement(child)) return null if (child.type ! == SwipeItem) return null; CloneElement (child, {style: itemStyle, vertical: vertical})}); cloneElement({style: itemStyle, vertical: vertical})}); }</div>
        </div>)}Copy the code
// SwipeItem.tsx
import React from 'react';
import './style.less';
const SwipeItem:React.FC<SwipeItemProps> = (props) = > {
    const { children, style, vertical } = props;

    return (
        <div className="lumu-swipe__item"} style={style}>
            {children}
        </div>)};Copy the code
// style.less
@name: lumu;

.@{name}-swipe {
    overflow: hidden;
    &__container {
        display: flex;
        align-items: center;
        height: 100%;
    }
    &__vertical {
        flex-direction: column;
    }
    &__item {
        width: 100%;
        height: 100%;
        flex-shrink: 0; }}Copy the code

Step 2, move the container (core)

At this point you can basically see a static rotation map layout, then start the core content. The core content is encapsulated in a useSwipe hook method, which is used to expose autoplay, gesture sliding, and more

// Swipe.tsx. Ditto to omit// Core method
    const { 
        swipeRef, // Move the ref of the container
        setRefs, // Set the subcomponent ref
        current, // Current index
        slideTo, // Move the position
        next, // Fast moving method encapsulated by slideTo
        prev, // Fast moving method encapsulated by slideTo
        loopMove, // A circular movement method encapsulated by slideTo
    } = useSwipe({ count, vertical, duration, size: itemSize, loop }); 
    
    return (
    	<div ref={root} style={props.style} className="lumu-swipe">
            <div ref={swipeRef} style={wrappStyle} className={`lumu-swipe__containerThe ${vertical ? 'lumu-swipe__vertical' :"'} `} >{ React.Children.map(props.children, (child, index) => { if (! React.isValidElement(child)) return null if (child.type ! == SwipeItem) return null; Return React. CloneElement (child, {style: itemStyle, vertical: Vertical, // Mount child component instances through setRefs for later movement ref: setRefs(index)})}); }</div>
        </div>
    )
Copy the code
// useSwipe.ts
import { useRef, useState, useMemo, useEffect } from 'react';
import { SwipeItemRef } from '.. /SwipeItem';
import useRefs from './useRefs';

type SwipeParams = {
    count: number;
    vertical: boolean;
    duration: number;
    size: number;
    loop: boolean;
}

type SlideToParams = Partial<{
    step: number;
    swiping: boolean;
    offset: number; } >.const useSwipe = (options: SwipeParams) = > {
    const { count, vertical, duration, size, loop } = options;
    // Current index
    const [current, setCurrent] = useState(0);
    // Calculate the index, which is also thrown to the external index.
    const realCurrent = useMemo(() = > (current + count) % count || 0, [current, count]);
    // Move the container
    const swipeRef = useRef<HTMLDivElement>(null);
    // This method is mainly to mount the child component instance, the child component moves the position later
    const [refs, setRefs] = useRefs<SwipeItemRef>();
    // The minimum index value
    const minCurrent = useMemo(() = > loop ? -1 : 0, [loop]);
    // Maximum index value
    const maxCurrent = useMemo(() = > loop ? count : count - 1, [loop, count]);
    // Current direction of movement
    const loopDirection = useRef<1|-1> (1);
	
    // Listen on the index to change the current movement direction
    useEffect(() = > {
        if (realCurrent === 0) {
            loopDirection.current = 1;
        }
        if (realCurrent === count - 1) {
            loopDirection.current = -1;
        }
    }, [realCurrent]);

    // Set the position of the move container and whether there is a move animation
    const setStyle = (dom: HTMLDivElement | null, options: { swiping: boolean, offset: number }) = > {
        if(! dom)return;
        const { swiping, offset } = options;
        dom.style.transition = `all ${swiping ? 0 : duration}ms`;
        dom.style.transform = `translate${vertical ? 'Y' : 'X'}(${offset}px)`;
    }
    // Reset the container
    const resetCurrent = () = > {
        setStyle(swipeRef.current, {
            swiping: true.offset: -realCurrent * size
        })
    }
    // Resets the child component position
    const resetChild = (step: number, offset: number) = > {
        let direction = ' ';
        if (step < 0 || offset > 0) {
            direction = 'left';
        }
        if (step > 0 || offset < 0) {
            direction = 'right';
        }
        if ([-1, count - 1].includes(current)) {
            refs[0].setOffset(direction === 'right' ? count * size : 0);
            refs[refs.length - 1].setOffset(0);
        }
        if ([count, 0].includes(current)) {
            refs[0].setOffset(0);
            refs[refs.length - 1].setOffset(direction === 'right' ? 0 : -count * size)
        }
    }
	
    // Move container, step number of moves, swiping whether to close animation, offset, mainly used for gesture movement
    const slideTo = ({ step = 0, swiping = false, offset = 0 }: SlideToParams) = > {
        if (count <= 1) return;
        // If it is seamless rotation, you need to reset the position of the child components before moving
        loop && resetChild(step, offset);
        // Computes the index to be arrived
        const fetureCurrent = Math.min(Math.max(realCurrent + step, minCurrent), maxCurrent);
        // Calculate the offset of the move
        const fetureOffset = -fetureCurrent * size + offset;
        if (swiping) {
            setStyle(swipeRef.current, {
                swiping, offset: fetureOffset
            });
        } else {
            requestAnimationFrame(() = > {
                requestAnimationFrame(() = > {
                    setStyle(swipeRef.current, {
                        swiping, offset: fetureOffset
                    });
                })
            })
        }
        setCurrent(fetureCurrent);
    }

    const next = () = > {
        resetCurrent();
        slideTo({ step: 1 });
    }

    const prev = () = > {
        resetCurrent();
        slideTo({ step: -1 });
    }

    const loopSwipe = () = > {
        if (loop) {
            next();
            return;
        }
        if (loopDirection.current === 1) {
            next();
        } else{ prev(); }}return {
        swipeRef,
        setRefs,
        current: realCurrent,
        slideTo,
        next,
        prev,
        loopSwipe
    }
}

export default useSwipe;
Copy the code
// SwipeItem.tsx
import React, { useImperativeHandle, useMemo, useRef, useState } from 'react';
import { SwipeProps } from './Swipe';

interface SwipeItemRef {
    setOffset: React.Dispatch<React.SetStateAction<number>>
}

interface SwipeItemProps {
    readonlyvertical? : SwipeProps['vertical'];
    readonlystyle? : React.CSSProperties; children: React.ReactNode; }const SwipeItem = React.forwardRef<SwipeItemRef, SwipeItemProps>((props, ref) = > {
    const { children, style, vertical } = props;
    const [offset, setOffset] = useState(0);
    const swipeItemRef = useRef<HTMLDivElement>(null);
    
    useImperativeHandle(ref, () = > {
        return {
            setOffset
        }
    });

    const itemStyle = useMemo(() = > {
        return {
            transform: offset ? `translate${props.vertical ? 'Y' : 'X'}(${offset}px)` : ' '. style } }, [offset, style, vertical]);return (
        <div ref={swipeItemRef} className={"lumu-swipe__item"} style={itemStyle}>
            {children}
        </div>)});Copy the code

Step 3: Gesture processing

For gestures, a useTouch method is encapsulated, which records gesture time and gesture difference

// useTouch.ts
import { useRef } from 'react';

const useTouch = () = > {
    const startX = useRef<number> (0); // Start X coordinate
    const startY = useRef<number> (0); // Start Y coordinate
    const deltaX = useRef<number> (0); // The x-coordinate distance moved
    const deltaY = useRef<number> (0); // The Y distance moved
    const time = useRef<number> (0); // Time record

    const reset = () = > {
        startX.current = 0;
        startY.current = 0;
        deltaX.current = 0;
        deltaY.current = 0;
        time.current = 0;
    }

    const start = (event: React.TouchEvent | TouchEvent) = > {
        reset();
        time.current = new Date().getTime();
        startX.current = event.touches[0].clientX;
        startY.current = event.touches[0].clientY;
    }

    const move = (event: React.TouchEvent | TouchEvent) = > {
        if(! time.current)return;
        deltaX.current = event.touches[0].clientX - startX.current;
        deltaY.current = event.touches[0].clientY - startY.current;
    }

    const end = () = > {
        const tempDeltaX = deltaX.current;
        const tempDeltaY = deltaY.current;
        const timediff = new Date().getTime() - time.current;
        reset();
        return {
            deltaX: tempDeltaX,
            deltaY: tempDeltaY,
            time: timediff
        }
    }

    const getDelta = () = > {
        return {
            deltaX: deltaX.current,
            deltaY: deltaY.current
        }
    }

    return {
        move, start, end, getDelta
    }
}
Copy the code
// SwipeItem.ts. Duplicate code omissionconst touch = useTouch();

const onTouchStart = (event: React.TouchEvent | TouchEvent) = > {
    if(! touchable)return; 
    touch.start(event);
}

const onTouchMove = (event: React.TouchEvent | TouchEvent) = > {
    if(! touchable)return; 
    touch.move(event);
    const { deltaX, deltaY } = touch.getDelta()
    slideTo({ swiping: true.offset: vertical ? deltaY : deltaX });
}

const onTouchEnd = () = > {
    if(! touchable)return; 
    const { deltaX, time, deltaY } = touch.end();
    const delta = vertical ? deltaY : deltaX;
    const step = (itemSize / 2 < Math.abs(delta) || Math.abs(delta / time) > 0.25)? (delta >0 ? -1 : 1) : 0;
    slideTo({ swiping: false, step });
}
Copy the code

The fourth step, detail branch function processing

Details function is mainly through the above core content for extension, here is no longer posted code, the complete source code can be viewed here address, mainly has the following points:

  1. Automatic round seeding can be realized by calling loopMove method
  2. The onSlideChange method is implemented by listening for the current index call
  3. Page visiblity processing, by listening to the page visiblity to start and stop the automatic rotation
  4. In longitudinal rotation, prohibit touchmove bubbling
  5. Throw the Next, prev, slideTo methods with useImperativeHandle
  6. The showIndicators attribute is also implemented using slideTo and Current to implement dot components that are shown and hidden by attributes

The sample address

Review past

  • Unpack the React component step by step – Lazyload lazy loading

The last

Think it works? Like to collect, by the way point like it, your support is my biggest encouragement! Think it’s useless? Comment section to share your ideas, open to your guidance.