使用React实现内容滑动组件效果

 更新时间:2023年05月17日 15:14:52   作者:SaebaRyo  
这篇文章主要介绍了使用React实现一个内容滑动组件效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

使用React实现一个内容滑动组件

最近在做项目时遇到一个需求,需要让一个列表能够通过点击按钮进行滚动,每次都是一屏的距离,不足则结束。并且,这个列表项是在react-grid-layout中的某一个模块内。所以包裹这个列表的容器会随时发生变化。在完成这个组件后,通过这篇文章总结一下。

UI/原型分析

那么从上面的功能描述以及项目中的UI,我们可以分析得到这样一个假想图:

image.png

  • 我们需要实现一个容器来作为我们的可视区域,并且这个容器是可以伸缩的。
  • 列表内容如果超出容器的可视区域,那么就会被隐藏。
  • 需要左右都有按钮,来支持用户左右滑动内容来查看,每次滑动距离为 容器的宽度,也就是可视区域
  • 当在伸缩容器的时候,如果是向右侧伸缩,并且可视区域已经被拉伸的超出了列表被隐藏的右侧内容时,右侧的隐藏内容需要有一个吸附效果,即跟着被伸缩的容器移动,直到左侧隐藏内容的偏移值为0。

话不多说,我们先上简单的最终效果图

有固定宽度,不可伸缩

slider-demo.gif

  • 无固定宽度,可伸缩(这里直接用币安的效果来展示,如果你有兴趣,可以自己下载个react-grid-layout或者其他库来试一下效果)

mxefs-bl7av.gif

功能实现

监听元素尺寸变化

工欲善其事必先利其器

在分析完后,我们发现有一个点。如果要支持react-grid-layout这类的伸缩功能,需要能够监听到元素的动态变化。

那么,我们可以先将这部分逻辑抽离,封装成一个hook:

hooks/useResizeObserver.ts

import { useLayoutEffect, useState } from "react";
// 接收保存被监听dom的ref
const useResizeObserver = (ref: React.RefObject<HTMLElement>) => {
  const [width, setWidth] = useState<number>(0);
  useLayoutEffect(() => {
     // 使用ResizeObserver来监听DOM的变化
    const resizeObserver = new ResizeObserver(() => {
      setWidth((ref.current as HTMLElement).clientWidth);
    });
    resizeObserver.observe(ref.current as HTMLElement);
    return () => {
      resizeObserver.disconnect();
    };
  }, [ref]);
  return width;
};
export default useResizeObserver;

其中核心的逻辑是使用ResizeObserver类来监听一个元素的尺寸变化。然后返回变化后的width

组件开发。

有了上面的分析,再实现代码就是一步步走即可。所以我们直接贴代码:

components/SliderContainer/index.tsx

import React, {
  useMemo,
  useState,
  useRef,
  useLayoutEffect,
  useEffect,
} from "react";
import type { ReactElement } from "react";
import "./index.css";
import ArrowLeft from "@/assets/arrow-left.svg";
import ArrowRight from "@/assets/arrow-right.svg";
import useResizeObserver from "@/hooks/useResizeObserver";
export interface SliderContainerProps {
  width: number;
  children: ReactElement; // 需要包括的内容
}
const LEFT = "left";
const RIGHT = "right";
export const SliderContainer: React.FC<SliderContainerProps> = ({
  width = "inherit",
  children,
}) => {
  const listRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const containerWidth = useResizeObserver(containerRef);
  const [listWidth, setListWidth] = useState(0);
  const [translateX, setTranslateX] = useState(0);
  // 缓存
  const cache = useRef(containerWidth);
  // 处理容器宽度变化时,内部元素的吸附效果
  useEffect(() => {
    if (
      containerWidth > cache.current && // 当容器可拖拽时,表示用户正在向右拖拽
      translateX < 0 && // 表示左侧有内容被隐藏
      listWidth - Math.abs(translateX) - containerWidth <= 0 //表示右侧已经没有被隐藏的内容了
    ) {
      const distance = containerWidth - cache.current;
      setTranslateX((cur) => cur + distance);
    }
    // 更新缓存
    cache.current = containerWidth;
  }, [containerWidth, translateX, listWidth]);
  useLayoutEffect(() => {
    setListWidth((listRef.current as HTMLDivElement).clientWidth);
  }, [children]);
  // 判断按钮是否可见
  const [leftArrowVisible, rightArrowVisible] = useMemo(() => {
    let leftArrowVisible,
      rightArrowVisible = false;
    // listWidth - Math.abs(translateX) - containerWidth 为右侧隐藏内容
    if (listWidth - Math.abs(translateX) - containerWidth > 0) {
      rightArrowVisible = true;
    }
    if (translateX < 0) {
      leftArrowVisible = true;
    }
    return [leftArrowVisible, rightArrowVisible];
  }, [listWidth, translateX, containerWidth]);
  const handleArrowClick = (direction: string) => {
    if (direction === LEFT) {
      // 左侧隐藏内容
      const leftSpaceWidth = Math.abs(translateX);
      if (leftSpaceWidth > containerWidth) {
        setTranslateX((cur) => cur + containerWidth);
      } else {
        setTranslateX((cur) => cur + leftSpaceWidth);
      }
    }
    if (direction === RIGHT) {
      // 右侧隐藏内容
      const rightSpaceWidth = listWidth - Math.abs(translateX) - containerWidth;
      if (rightSpaceWidth > containerWidth) {
        setTranslateX((cur) => cur - containerWidth);
      } else {
        setTranslateX((cur) => cur - rightSpaceWidth);
      }
    }
  };
  return (
    <div ref={containerRef} style={{ width: width }} className="container">
      {leftArrowVisible && (
        <>
          <button
            color="white"
            className="leftArrow btn"
            onClick={() => handleArrowClick(LEFT)}
          >
            <img src={ArrowLeft} alt="" />
          </button>
          <div className="linerGrid leftGradient"></div>
        </>
      )}
      <div
        ref={listRef}
        className="list"
        style={{
          transform: `translateX(${translateX}px)`,
          transition: "all 0.3s linear",
        }}
      >
        {children}
      </div>
      {rightArrowVisible && (
        <>
          <div className="linerGrid rightGradient"></div>
          <button
            color="white"
            className="rightArrow btn"
            onClick={() => handleArrowClick(RIGHT)}
          >
            <img src={ArrowRight} alt="" />
          </button>
        </>
      )}
    </div>
  );
};

组件目前设置了两个属性widthchildren

  • width: 如果不传,默认inherit,表示该容器是可伸缩的,那么组件内部会自己计算;如果传了固定宽度,则按照该固定宽度来设置
  • children: 需要滚动的内容

其中,主要是有5个点需要着重理解

  • useLayoutEffect中,需要通过传入的children来判断是否需要更新list的长度,防止计算不准确
  • 判断按钮何时显示
  • 按钮点击时需要处理的UI逻辑
  • 如果容器是可伸缩的,需要通过useRef的缓存来判断用户向哪个方向伸缩容器
  • 使用transformtransition让动画更流畅自然

对应的css样式为:

components/SliderContainer/index.css

.container {
  display: flex;
  overflow: hidden;
  position: relative;
  height: 100%;
}
.list {
  display: flex;
  align-items: center;
}
.btn {
  width: 40px;
  height: 100%;
  position: absolute;
  z-index: 1;
  display: flex;
  justify-content: center;
  align-items: center;
}
.leftArrow {
  left: 0;
}
.rightArrow {
  right: 0;
  z-index: 0;
}
.linerGrid {
  position: absolute;
  width: 20px;
  height: 100%;
}
.leftGradient {
  left: 40px;
  z-index: 1;
  background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0.2));
}
.rightGradient {
  right: 40px;
  background: linear-gradient(269.21deg, #ffffff, rgba(255, 255, 255, 0.2));
}

使用方式

我们可以通过下面的方式来使用

const list = [
  { key: "1", name: "列表项1" },
  { key: "2", name: "列表项2" },
  { key: "3", name: "列表项3" },
  { key: "4", name: "列表项4" },
  { key: "5", name: "列表项5" },
  { key: "6", name: "列表项6" },
  { key: "7", name: "列表项7" },
  { key: "8", name: "列表项8" },
  { key: "9", name: "列表项9" },
  { key: "10", name: "列表项10" },
];
function App() {
  return (
    <div className="App">
      <SliderContainer width={300}>
        <>
          {list.map((item) => (
            <div style={{ width: 100 }} key={item.key}>
              {item.name}
            </div>
          ))}
        </>
      </SliderContainer>
    </div>
  );
}

到此这篇关于使用React实现内容滑动组件效果的文章就介绍到这了,更多相关React滑动组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • react组件中过渡动画的问题解决

    react组件中过渡动画的问题解决

    这篇文章主要为大家介绍了react组件中过渡动画的问题解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • react:swr接口缓存案例代码

    react:swr接口缓存案例代码

    useSWR 是一个 React Hooks,是 HTTP 缓存库 SWR 的核心方法之一,SWR 是一个轻量级的 React Hooks 库,通过自动缓存数据来实现 React 的数据获取,本文给大家介绍react:swr接口缓存案例详解,感兴趣的朋友一起看看吧
    2023-11-11
  • React 正确使用useCallback useMemo的方式

    React 正确使用useCallback useMemo的方式

    这篇文章主要介绍了React 正确使用useCallback useMemo的方式,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下
    2022-08-08
  • 详解在React.js中使用PureComponent的重要性和使用方式

    详解在React.js中使用PureComponent的重要性和使用方式

    这篇文章主要介绍了详解在React.js中使用PureComponent的重要性和使用方式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • React Streaming SSR原理示例深入解析

    React Streaming SSR原理示例深入解析

    这篇文章主要为大家介绍了React Streaming SSR原理示例深入解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 关于React中setState同步或异步问题的理解

    关于React中setState同步或异步问题的理解

    相信很多小伙伴们都一直在疑惑,setState 到底是同步还是异步。本文就详细的介绍一下React中setState同步或异步问题,感兴趣的可以了解一下
    2021-11-11
  • React+TypeScript项目中使用CodeMirror的步骤

    React+TypeScript项目中使用CodeMirror的步骤

    CodeMirror被广泛应用于许多Web应用程序和开发工具,之前做需求用到过codeMirror这个工具,觉得还不错,功能很强大,所以记录一下改工具的基础用法,对React+TypeScript项目中使用CodeMirror的步骤感兴趣的朋友跟随小编一起看看吧
    2023-07-07
  • ReactNative支付密码输入框实现详解

    ReactNative支付密码输入框实现详解

    这篇文章主要为大家介绍了ReactNative支付密码输入框实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • React中的JSX  { }的使用详解

    React中的JSX  { }的使用详解

    这篇文章主要介绍了React中的JSX{ }的使用,React使用JSX来替代常规的JavaScript,JSX可以理解为的JavaScript语法扩展,它里面的标签申明要符合XML规范要求,对React JSX使用感兴趣的朋友一起看看吧
    2022-08-08
  • 如何将你的AngularJS1.x应用迁移至React的方法

    如何将你的AngularJS1.x应用迁移至React的方法

    本篇文章主要介绍了如何将你的AngularJS1.x应用迁移至React的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02

最新评论