模拟antd-mobile的slide滑块效果附三种实现方式(高手勿喷)

425 阅读5分钟

模拟antd-mobile的slide滑块效果附三种实现方式(高手勿喷)

第一种ts方式(tsx)

import './index.css'
export default class index extends Component {
  //定义数据
  state = {
    //记录当前点击的小球
    active: 0,
    //小球相关的数据
    ballData: [
      {
        //一号小球距离盒子边的位置
        toBoxLeft: 0,
        //一号小球的位置(绝对定位)
        left: 0,
      },
      {
        toBoxLeft: 0,
        left: 0,
      },
    ],
  }
  render() {
    //结构出state中的数据
    const { active, ballData } = this.state
    //求出两个球中的left值最小值,作为slide滑块的left值
    let slideLeft = Math.min.apply(
      Math,
      ballData.map((tim) => tim.left)
    )+10
    //求出两个球之间的绝对值作为滑块的长度
    let slideWidth = Math.abs(ballData[0].left - ballData[1].left)
    return (
      <div className='app'>
        <div className='top'>
          {/* 设置滑块的left值和长度 */}
          <div className='slide' style={{ left: slideLeft, width: slideWidth }}></div>
          {/* 遍历生成小球 */}
          {ballData.map((tim, i) => {
            return (
              <div
                // 根据点击的小球设置其层级,并且设置小球的left值
                style={{ zIndex: active === i ? 500 : 5, left: ballData[i].left }}
                className='ball'
                //点击小球的事件
                onTouchStart={(e) => {
                  //将事件源类型断言成div类型
                  let ev = e.target as HTMLDivElement
                  //计算距离小球边的距离
                  let x = e.changedTouches[0].pageX - ev.offsetLeft
                  //深拷贝一份小球的数据
                  let newBallData = [...ballData]
                  //改变对应的点击位置相对于小球边的距离
                  newBallData[i].toBoxLeft = x
                  //将当前点击的索引和更改完后的小球数据设给状态
                  this.setState({
                    active: i,
                    ballData: [...newBallData],
                  })
                }}
                //移动小球的回调
                onTouchMove={(e) => {
                  //深拷贝一份小球的数据
                  let newBallData = [...ballData]
                  //动态计算出当前操作小球的left(绝对定位)值
                  let x = e.changedTouches[0].pageX - newBallData[i].toBoxLeft
                  newBallData[i].left = x < 0 ? 0 : x > 300 ? 300 : x
                  //将计算完毕的值设置给小球数据
                  this.setState({
                    ballData: [...newBallData],
                  })
                }}
              >
                {/* 设置数值 */}
                {((tim.left / 300) * 100) | 0}
              </div>
            )
          })}
        </div>
      </div>
    )
  }
}

第一种ts方式(css)

body {
  width: 100%;
  height: 100%;
}
* {
  margin: 0;
  padding: 0;
}
.app {
  position: absolute;
  width: 100%;
  height: 100%;
}
.top {
  position: relative;
  margin-left: 20px;
  margin-top: 50px;
  width: 80%;
  height: 10px;
  border-radius: 5px;
  background-color: #eee;
}
.slide {
  position: absolute;
  left: 0;
  top: 0;
  height: 10px;
  width: 0;
  background-color: blue;
}
.ball {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  background-color: pink;
  border-radius: 10px;
  text-align: center;
  line-height: 20px;
}

第二种jsx方式(有刻度)jsx部分

import './App.css'
export default class App extends Component {
  state = {
    //用于遍历小球的依赖数组
    ballArr: [1, 2],
    //保存当前选中的小球的下标
    active: 0,
    //保存li的数组
    arr: [],
    //保存li距离父级的左侧位置
    liArr: [],
    //保存两只小球所处的下标
    indexArr: { ball1: 0, ball2: 0 },
    //li最右侧的值
    width: 339,
    //鼠标距小球左侧的距离
    x: 0,
    //小球移动时距左侧的位置
    left: 0,
    //小球1的具体位置(绝对定位值)
    boxLeft1: 0,
    //小球2的具体位置(绝对定位值)
    boxLeft2: 0,
    //用于遍历li的依赖数组
    initLiArr: [1, 2, 3, 4, 5],
  }
  componentDidMount() {
    //获取所有的li计算出li距其父级的left值
    let arr = [...this.refs.top.children].slice(0, 5)
    let liArr = arr.map((e) => {
      return e.offsetLeft
    })
    //把获取的li和li距其父级的left值存入状态中
    this.setState({ arr, liArr: [...liArr, this.state.width] })
  }
  render() {
    //结构出state中的数据方便使用
    const { x, left, boxLeft1, boxLeft2, initLiArr, liArr, arr, ballArr, active, indexArr } =
      this.state
    return (
      <div className='app'>
        {/* ref标记父盒子div,用于获取其中的子元素 */}
        <div ref={'top'} className='top'>
          {/* 遍历渲染所有li */}
          {initLiArr.map((e) => (
            <li></li>
          ))}
          {/* 遍历渲染小球 */}
          {ballArr.map((e, i) => {
            return (
              <div
                style={{
                  // 点击小球后提高它的层级,使他可以拖动
                  zIndex: active === i ? 500 : 5,
                  //设置小球的位置
                  left: (i === 0 ? boxLeft1 : boxLeft2) - 10,
                }}
                className='ball'
                //触摸小球事件
                onTouchStart={(e) => {
                  this.setState({
                    //动态计算出鼠标距离小球左边的边距
                    x: e.changedTouches[0].pageX - e.target.offsetLeft,
                    //设置当前索引值区分点击的小球
                    active: i,
                  })
                }}
                //小球移动事件
                onTouchMove={(e) => {
                  this.setState({
                    //移动时记录小球距app盒子的边距
                    left: e.changedTouches[0].pageX - x,
                  })
                  //计算每个盒子与刚刚记录的left值的差的绝对值数组
                  let arr2 = liArr.map((e) => Math.abs(e - left))
                  //查出最小绝对值的下标
                  let index = arr2.findIndex((item) => item === Math.min.apply(Math, arr2))
                  let newArr = []
                  this.setState(
                    (e) => {
                      return {
                        //通过刚才的下标动态设置两只小球的位置
                        [active === 0 ? 'boxLeft1' : 'boxLeft2']: liArr[index],
                        //通过刚才的下标动态设置两只小球所处的相对下标
                        indexArr: { ...indexArr, [active === 0 ? 'ball1' : 'ball2']: index },
                      }
                    },
                    () => {
                      //将两只小球的下标进行排序
                      newArr = [this.state.indexArr.ball1, this.state.indexArr.ball2].sort(
                        (a, b) => {
                          return a - b
                        }
                      )
                      //遍历li的元素数组,把两只小球下标之间的li设置颜色
                      arr.forEach((e, i) => {
                        if (i >= newArr[0] && i < newArr[1]) e.style.backgroundColor = '#409eff'
                        else e.style.backgroundColor = 'pink'
                      })
                    }
                  )
                }}
              ></div>
            )
          })}
        </div>
      </div>
    )
  }
}

第二种jsx(有刻度的部分)css部分

body {
  width: 100%;
  height: 100%;
}
* {
  margin: 0;
  padding: 0;
}
.app {
  position: absolute;
  width: 100%;
  height: 100%;
}
.top {
  margin: 0 auto;
  position: relative;
  margin-top: 20px;
  width: 90%;
  height: 10px;
  border: 1px solid #000;
  display: flex;
  align-items: center;
  justify-content: space-around;
}
li {
  list-style: none;
  width: 18%;
  height: 10px;
  border-radius: 5px;
  background-color: pink;
}
.ball {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  background-color: blue;
  border-radius: 50%;
  z-index: 5;
}

第三种带小球碰撞效果(tsx部分)

import React, { Component } from 'react'
import './index.css'
export default class index extends Component {
  //设置状态
  state = {
    //保存当前选中的小球下标
    active: 0,
    //保存小球相关的数据,left为小球的位置,to为触摸小球时鼠标距离小球的位置
    data: [
      { left: 0, to: 0 },
      { left: 0, to: 0 },
    ],
  }
  render() {
    //结构赋值
    const { active, data } = this.state
    //动态计算中间滑块的位置(取两只小球的最小值)
    let left =
      Math.min.apply(
        Math,
        data.map((e) => e.left)
      ) + 10
    //动态计算出滑块的长度(取两只小球位置差的绝对值)
    let width = Math.abs(data[0].left - data[1].left)
    return (
      <div className='app'>
        <div className='top'>
          {/* 将滑块的样式动态赋值 */}
          <div className='slide' style={{ width, left }}></div>
          {/* 遍历生成小球 */}
          {data.map((tim, i) => {
            return (
              <div
                className='ball'
                // 将小球的索引和位置动态设置进来
                style={{ left: tim.left, zIndex: active === i ? 500 : 5 }}
                //触摸小球触发事件
                onTouchStart={(e) => {
                  //拿到小球的dom
                  let ev = e.target as HTMLDivElement
                  //动态计算出触摸点距小球边的距离
                  tim.to = e.changedTouches[0].pageX - ev.offsetLeft
                  //将计算完的距离更新至状态里
                  this.setState({ active: i, data: [...data] })
                }}
                onTouchMove={(e) => {
                  //动态计算出当前触摸小球的位置
                  let x = e.changedTouches[0].pageX - tim.to
                  //约束当前触摸小球的left值
                  tim.left = x > 300 ? 300 : x < 0 ? 0 : x
                  //判断碰撞
                  if (i === 0 && tim.left + 20 > data[1].left) data[1].left = tim.left + 20
                  if (i === 1 && tim.left - 20 < data[0].left) data[0].left = tim.left - 20
                  //将当前小球的位置更新值状态里面
                  this.setState({ data: [...data] })
                }}
              >
                {/* 动态计算出每个小球的进度值 */}
                {((tim.left / 300) * 100) | 0}
              </div>
            )
          })}
        </div>
      </div>
    )
  }
}



css部分

body {
  width: 100%;
  height: 100%;
}
* {
  margin: 0;
  padding: 0;
}
.app {
  position: absolute;
  width: 100%;
  height: 100%;
}
.top {
  position: relative;
  margin-left: 20px;
  margin-top: 50px;
  width: 80%;
  height: 10px;
  border-radius: 5px;
  background-color: #eee;
}
.slide {
  position: absolute;
  left: 0;
  top: 0;
  height: 10px;
  width: 0;
  background-color: blue;
}
.ball {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  background-color: pink;
  border-radius: 10px;
  text-align: center;
  line-height: 20px;
}