React无缝滚动组件

使用场景

内容滚动,例如大屏中的列表,卡片等

滚动组件演示效果图

需求

  • 轮播无缝滚动
  • 方向控制
  • 播放/暂停

原理

定义帧动画以及控制方向的css类,通过js增加/移除控制方向的css类的方式控制动画。

注意:需要复制出一份子组件放在子组件后(下/右)边,目的是消除视口中动画结束后导致的大片空白

核心

  • css3 animation动画
  • css3 transform根据自身宽高位移
  • classList 操作 class 集合

组件代码

./components/index.less

// 暂停播放
.pause {
  animation-play-state: paused !important;
}
// 播放
.play {
  animation-play-state: running !important;
}

// 向上滚动
.top {
  animation: top 3s linear infinite;
  &:hover {
    .pause();
  }
}

@keyframes top {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-50%)
  }
}

// 向左滚动
.left {
  animation: left 3s linear infinite;
  &:hover {
    .pause();
  }
}

@keyframes left {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-50%)
  }
}

./components/index.tsx

import {
  ForwardRefRenderFunction,
  forwardRef,
  useImperativeHandle,
  useRef,
  CSSProperties,
  ReactNode,
} from 'react';
import './index.less';

export type Direction = 'left' | 'top';

export interface RollMethodTypes {
  play(): void;
  pause(): void;
}

interface ComponentPropTypes {
  children: ReactNode;
  direction?: Direction;
  animation?: CSSProperties['animation'];
}

// 父盒子需规定视口宽高,并且overflow: hidden !!!
const Roll: ForwardRefRenderFunction<RollMethodTypes, ComponentPropTypes> = (
  { children, direction = 'top', animation },
  ref,
) => {
  const roll = useRef<HTMLDivElement>(null);

  // 向父组件暴露 播放、暂停 方法
  useImperativeHandle(ref, () => ({
    play: () => {
      roll.current?.classList.replace('pause', 'play');
    },
    pause: () => {
      roll.current?.classList.replace('play', 'pause');
    },
  }));

  // 向左滚动
  if (direction === 'left' || animation?.toString().includes('left')) {
    return (
      <div
        ref={roll}
        className={`${direction} play`}
        // 水平排列,并且复制一份无缝衔接,水平宽度变成视口的两倍,以消除视口中动画结束后导致的大片空白
        style={{ width: '200%', display: 'flex', animation }}
      >
        <div style={{ flex: 1 }}>{children}</div>
        <div style={{ flex: 1 }}>{children}</div>
      </div>
    );
  }

  // 向上滚动
  return (
    <div ref={roll} className={`${direction} play`} style={{ animation }}>
      {/* 垂直排列,并且复制一份无缝衔接,让垂直高度变成视口的两倍,以消除视口中动画结束后导致的大片空白 */}
      {children}
      {children}
    </div>
  );
};

export default forwardRef(Roll);

如何使用(Demo)

index.less

.title {
  background: rgb(121, 242, 157);
}

.content {
  .item {
    height: 80px;
  }
  .item:nth-child(1) {
    background-color: red;
  }
  .item:nth-child(2) {
    background-color: yellow;
  }
  .item:nth-child(3) {
    background-color: blue;
  }
  .item:nth-child(4) {
    background-color:rgb(121, 242, 157);
  }
  .item:nth-child(5) {
    background-color: rgba(0, 0, 0, 0.3);
  }
  .item:nth-child(6) {
    background-color: #daa520
  }
}

index.tsx

import { useRef, useState } from "react";
import Roll from "./components/Roll";

import styles from './index.less';

import { Direction, RollMethodTypes } from './components/Roll'

const Index = () => {
  const roller = useRef<RollMethodTypes>(null);
  const [num, setNum] = useState(0);
  const [direction, setDirection] = useState<Direction>('left');
  
  return (
    <div>
      <div style={{ width: 500, height: 200, overflow: 'hidden' }}>
        <Roll
          ref={roller}
          direction={direction}
          // animation="left 1s linear infinite"
        >
          <div className={styles.content}>
            <div className={styles.item} style={{ color: '#fff', fontSize: 40  }}>{num}</div>
            <div className={styles.item}>2222222222</div>
            <div className={styles.item}>333333333333333333333333333333</div>
            <div className={styles.item}>444444444444444444</div>
            <div className={styles.item}>55555</div>
            <div className={styles.item}>6</div>
          </div>
        </Roll>
      </div>
      <button onClick={() => setDirection(direction === 'left' ? 'top' : 'left' )}>{direction == 'left' ? '向上滚动' : '向左滚动' }</button>
      <button onClick={() => setNum(num + 1)}>+1</button>
      <button onClick={() => roller.current?.play()}>开始滚动</button>
      <button onClick={() => roller.current?.pause()}>暂停滚动</button>
    </div>
  )
};

export default Index;
### 实现无缝滚动效果的React插件 为了在React项目中实现无缝滚动效果,可以考虑使用`react-seamless-scroll`组件。虽然直接名为`react-seamless-scroll`的库可能不是最流行的选项,但是存在一些功能相似且广受好评的选择。 #### 使用 `react-waypoint` 一种方法是利用`react-wayport`来检测元素何时进入视口并触发相应的事件处理程序[^1]: ```jsx import React from 'react'; import Waypoint from 'react-waypoint'; function InfiniteScrollList({ items, loadMore }) { return ( <div> {items.map(item => ( <div key={item.id}>{/* 渲染列表项 */}</div> ))} <Waypoint onEnter={loadMore} /> </div> ); } ``` 然而,这主要用于无限加载场景而非传统意义上的无缝滚动。 #### 更贴合需求的是基于Vue的解决方案移植到React中的思路 对于真正想要达到类似于传送带式的循环滚动体验,则可以从研究如何将面向Vue设计的`vue-seamless-scroll`转换成适用于React版本入手[^2]。尽管这不是一个原生支持React的包,但从其原理出发,在React环境中重现相同行为并非不可能完成的任务。 具体来说,可以通过自定义Hooks以及CSS动画组合的方式来模拟这种连续不断的数据流展示形式。下面是一个简化版的概念验证示例: ```jsx import React, { useState, useEffect } from "react"; import "./SeamlessScroll.css"; const SeamlessScroll = ({ data }) => { const [offsetY, setOffsetY] = useState(0); useEffect(() => { let timerId; if (data.length > 0) { timerId = setInterval(() => { setOffsetY((prev) => prev + 1); if (Math.abs(prev) >= document.querySelector(".scroll-item").clientHeight) { setOffsetY(-document.querySelector(".scroll-container").clientHeight); } }, 30); // 调整速度 } return () => clearInterval(timerId); }, [data]); return ( <div className="scroll-wrapper"> <div className="scroll-container" style={{ transform: `translateY(${offsetY}px)` }} > {data.map((item, index) => ( <div key={index} className="scroll-item"> {/* Render your item here */} {item.text} </div> ))} </div> </div> ); }; export default SeamlessScroll; ``` 配合如下样式文件`.css`: ```css .scroll-wrapper { overflow: hidden; } .scroll-container { display: flex; flex-direction: column; transition: all ease-in-out 0.5s; } .scroll-item { padding: 1em; border-bottom: solid 1px #ccc; } ``` 此代码片段展示了如何创建一个简单的垂直方向上的无缝滚动容器,并通过定时器更新偏移量以驱动内容向上移动的效果。当顶部的内容完全离开视野时,立即将整个容器重置回初始位置从而形成平滑过渡的感觉。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值