2

江湖规矩~无图无真相,先上图!

16edb990cbfd5ac9.gif
这是一个展览列表的滑动轮播组件,点击左右按钮就可以进行左右滑动,每次可以滑动指定的数量,以及可以指定展示多少行的内容。

本人前端小菜鸟,大佬们轻喷呜呜呜

下面来分享一下代码

因为本人采用的是React + Typescript来做项目的,大家如果有看不懂也没关系的!换成普通的html跟js也差不多的。

使用
<Slider cityList={cityList} row={2} step={2} />
组件代码
import React, { Component, ComponentType } from 'react'
import { Icon } from 'antd'

import './index.scss'

interface IProps {
  cityList: Array<number>,
  row: number,
  step: number
}

interface IStates {
  cityContainerWidth: number,
  cityWrapWidth: number,
  cityWrapTranslateX: number
}

class Slider extends Component<IProps, IStates> {
  constructor(props: IProps) {
    super(props)
    this.state = {
      cityContainerWidth: 0,
      cityWrapWidth: 0,
      cityWrapTranslateX: 0
    }
  }

  componentDidMount(): void {
    const { cityList, row } = this.props
    const cityWrapWidth: number = cityList.length > 12 ? Math.ceil(cityList.length / row) * 220 : 1320
    const cityWrapDom: HTMLElement | null = document.getElementById('city__wrap') as HTMLElement
    const cityContainerDom: HTMLElement | null = document.getElementById('city__container') as HTMLElement
    const cityContainerWidth: number = cityContainerDom.offsetWidth
    cityWrapDom && (cityWrapDom.style.width = `${cityWrapWidth}px`)

    this.setState({
      cityContainerWidth,
      cityWrapWidth
    })
  }

  handleArrowClick(direction: string): void {
    const { step } = this.props
    const { cityContainerWidth, cityWrapWidth, cityWrapTranslateX } = this.state
    const cityWrapDom: HTMLElement | null = document.getElementById('city__wrap') as HTMLElement
    /* 步长 */
    const translateStep: number = 220 * step
    const translateDistance: number = translateStep * (direction === 'left' ? 1 : -1)

    let newTranslateX: number = cityWrapTranslateX
    /* 相对移动距离 */
    const relativeTranslateX: number = cityContainerWidth - cityWrapTranslateX
    const isLeftEnd: boolean = relativeTranslateX <= cityContainerWidth
    const isLeftOverflow: boolean = relativeTranslateX - translateDistance <= cityContainerWidth
    const isRightEnd: boolean = relativeTranslateX + 10 >= cityWrapWidth // 这个10是代表右边距的10个像素,加上10隐藏
    const isRightOverflow: boolean = relativeTranslateX - translateDistance >= cityWrapWidth

    /* 点击左箭头 */
    if (translateDistance > 0) {
      /* 是否到达左边尽头 */
      if (isLeftEnd) return

      if (isLeftOverflow) {
        /* 超出范围,则滑动刚好到达左边末尾的距离 */
        newTranslateX = 0
      } else {
        /* 未超出范围,滑动距离直接与步长相加 */
        newTranslateX += translateDistance
      }

    } else if (translateDistance < 0) {
      /* 是否到达右边尽头 */
      if (isRightEnd) return

      if (isRightOverflow) {
        /* 超出范围,则滑动刚好到达右边末尾的距离 */
        newTranslateX += relativeTranslateX + 10 - cityWrapWidth
      } else {
        /* 未超出范围,滑动距离直接与步长相加 */
        newTranslateX += translateDistance
      }
    }

    const transformString: string = `translateX(${newTranslateX}px)`
    cityWrapDom && (cityWrapDom.style.transform = transformString)
    this.setState({
      cityWrapTranslateX: newTranslateX
    })
  }

  render() {
    const { cityList } = this.props
    return (
      <div className="city" >
        <div className="city__title">我是一个轮播图</div>
        <div className="city__container" id="city__container">
          <div className="city__arrow" onClick={() => this.handleArrowClick('left')}>
            <Icon className="icon" type="left" />
          </div>
          <div className="city__arrow city__arrow--right" onClick={() => this.handleArrowClick('right')}>
            <Icon className="icon" type="right" />
          </div>
          <div className="city__wrap" id="city__wrap">
            {cityList.map(item => (
              <div className="city__item" key={item}>{item}</div>
            ))}
          </div>
        </div>
      </div>
    )
  }
}

export default Slider as ComponentType<IProps>
样式文件(采用了sass来进行样式编写)
.city {
  &__container {
    position: relative;
    overflow: hidden;
    width: 1200px;
    padding-top: 10px;

    &::-webkit-scrollbar {
      width: 15px;
      height: 15px;
    }

    &::-webkit-scrollbar-track {
      border-radius: 20px;
      background: #e7e7e7;
    }

    &::-webkit-scrollbar-thumb {
      background: #66a6ff;
      background-image: linear-gradient(120deg, #89a4fe 0%, #66a6ff 100%);
      border-radius: 20px;
    }
  }

  &__title {
    margin-top: 30px;
    color: #333;
    font-size: 24px;
    font-weight: bold;
  }

  &__arrow {
    position: absolute;
    display: flex;
    justify-content: center;
    align-items: center;
    top: 50%;
    width: 50px;
    height: 100px;
    background: rgba(0, 0, 0, 0.7);
    transform: translateY(-50%);
    transition: all .3s ease;
    z-index: 2;
    opacity: .5;
    cursor: pointer;

    &--right {
      right: 0;
    }

    .icon {
      color: #fff;
      font-size: 30px;
    }

    &:hover{
      opacity: 1;
    }
  }

  &__wrap {
    transition: all .3s ease-in-out;
  }

  &__item {
    float: left;
    width: 210px;
    height: 90px;
    margin: 0 10px 10px 0;
    color: #fff;
    font-size: 40px;
    font-weight: bold;
    line-height: 90px;
    text-align: center;
    // background: url(https://static.zhipin.com/zhipin-geek/v98/web/geek/images/city_101010100.png) no-repeat;
    // background-size: cover;
    background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  }
}
最后是这里出现的itemList

这是一个将要用于展示的数据列表。这里我们可以用函数生成一个展示用的列表,大家如果有需要的话可以将其换成访问接口获取的列表...

  getCityList(): Promise<Array<number>> {
    return new Promise(async (resolve, reject) => {
      const length: number = 15
      const cityList: Array<number> = Array.from({ length }, (_: unknown, index: number): number => index + 1)
      resolve(cityList)
    })
  }

我们在这里生成了一个长度为15的自然数数组[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

代码分析

虽然代码中都有注释,但是还是简要的分享一下吧~

步骤当然是:
获取需要展示的数据列表 -> 给滑动轮播组件传入列表 -> 接受后根据列表的长度、需要展示的行数来对容器设置动态宽度

在接下来就是重头戏啦!!!

关于处理容器如何滑动的地方我可是下了不少功夫,我们一点一点来看吧

  1. 监听点击箭头按钮的函数

handleArrowClick(direction),这个函数的意思是,点击左箭头的时候,direction就是left,反之这是right

  <div className="city__arrow" onClick={() => this.handleArrowClick('left')}>
    <Icon className="icon" type="left" />
  </div>
  <div className="city__arrow city__arrow--right" onClick={() => this.handleArrowClick('right')}>
    <Icon className="icon" type="right" />
  </div>

分别是左右箭头,点击左箭头就给函数传入一个字符串代表方向

  1. 定义步长
/* 步长 */
const translateStep: number = 220 * step
const translateDistance: number = translateStep * (direction === 'left' ? 1 : -1)

这个很简单,就是根据父组件传入的step步长(每次点击滑动多少个元素),以及点击按钮时传入的方向。如果是左,则移动的距离是-步长,在这里我们用的是transform: translateX的属性来进行滑动,正轴是向右,所以如果要想展示的内容向右移动,则需要将移动的距离设置为负数,这样相对移动起来,才能达到完美想要的效果,现在不懂没关系,先继续往后看一看,

  1. 获得移动距离,以及判断是否超出等情况

划重点!!!划重点!!!划重点!!!

首先我们第一步先设置state,里面定义一个变量,来表示已经滑动的距离,初始为0

/* 已经滑动的距离 */
this.state = {
  cityWrapTranslateX: 0
}

然后我们获取一下滑动组件的外容器跟内容器的宽度,如果不懂的话可以百度一下轮播图的实现原理,简单说明一下的话就是说

啊啊啊.jpg

外容器就是可见的部分,而内容器就是一个部分会隐藏的,然后根据外容器的大小,就可以吧内容器里面的内容展示出来,就是图中重叠的部分啦,想象一下~内容器在左右移动的时候,我们见到的外容器中的东西是不是也在跟着移动了嘞~

好,不懂的可以去百度找一下更为详细的轮播图原理!俺先继续往下分析

/* 相对移动距离 */
const relativeTranslateX: number = cityContainerWidth - cityWrapTranslateX

然后我们通过获取到来的外容器的大小cityContainerWidth以及已经滑动的距离cityWrapTranslateX通过两者相减就可以得到相对移动距离啦,这是什么意思呢,我们先继续下去讲一下各种边界条件大家就懂啦!

const isLeftEnd: boolean = relativeTranslateX <= cityContainerWidth
const isLeftOverflow: boolean = relativeTranslateX - translateDistance <= cityContainerWidth
const isRightEnd: boolean = relativeTranslateX + 10 >= cityWrapWidth // 这个10是代表右边距的10个像素,加上10隐藏
const isRightOverflow: boolean = relativeTranslateX - translateDistance >= cityWrapWidth

这四个变量分别是:是否已经到达左边尽头(不能再点击了!)、点击按钮左边是否会溢出(就是滑动过头了!)、是否到达右边尽头、点击按钮右边是否会溢出

我们就主要分析后两个就好,后两个就大同小异啦(因为喜欢右边)
我们先假设外容器的宽度为1200,内容器的宽度为2000

  • isRightEnd
/* 相对移动距离 */
const relativeTranslateX: number = cityContainerWidth - cityWrapTranslateX
const isRightEnd: boolean = relativeTranslateX + 10 >= cityWrapWidth // 这个10是代表右边距的10个像素,加上10隐藏

我们先忽略这个10,因为每个元素有外边距10px,所以我们加上,我们大可先忽略不计

16edbc386b0b6cbe.jpg
为什么是这样呢,假设现在我们的移动距离为0,那么相对距离就是外容器的宽度了就是1200,那么内容器为2000,小于内容器

接下来假设我们向右移动800px,那么cityWrapTranslateX就是-800(总之记住内容向左就是负数!)这时候我们相对移动距离就是1200-(-800)就是2000,跟内容器一样了


看懂了吗!这样就相当于外容器向右移动了800px,因为前面也说了,外容器是控制我们的可视部分,所以外容器向右,等于内容也向右,就对了!

接下来在看这个const isRightEnd: boolean = relativeTranslateX + 10 >= cityWrapWidth是不是就明白了,如果大于等于,就代表到尽头了,再次点击就直接return不执行就好

  • isRightOverflow

这个意思就是点击的时候会不会超出容器的范围,这样是为了避免产生滑动过度产生多余空白的问题,例子如下:

16edbc999f8697d1.jpg

因为滑动过度而导致右边产生大量空白

所以我们这里判断一下相对移动距离是不是超出内容器的宽度,也就是说超出展示内容的宽度

const relativeTranslateX: number = cityContainerWidth - cityWrapTranslateX
const isRightOverflow: boolean = relativeTranslateX - translateDistance >= cityWrapWidth

我们看一下图

16edbce828250308.jpg

就是现在滑动的地方,已经只剩下300px就到尽头了,但是我们设置的每次滑动600px,那这时候,我们相对移动距离就是1200 - (-500)就是1700,接着relativeTranslateX - translateDistance就是1700 - (-600)就是2300, 比2000大,就代表将要溢出,溢出了300px那么多,

我们这时候就定义一个变量newTranslateX来表示新的滑动距离。默认是上一次滑动的距离

let newTranslateX: number = cityWrapTranslateX

如果是将要溢出的话,我们就设置为上一次的滑动距离加上相对滑动距离再减去内容器的宽度,这样说可能看不懂,那我们就来算一算

newTranslateX = 500 (已经滑动了500)
relativeTranslateX = 1200
cityWrapWidth = 2000

newTranslateX = 500 + 1200 - 2000 = -300
这样是不是就代表向右移动300个像素了呢!,这样就刚刚好到达末尾啦!

newTranslateX += relativeTranslateX - cityWrapWidth

最后我们再根据各种情况设置最新滑动距离就可以了,设置完滑动的距离,就可以改变样式啦

/* 点击左箭头 */
if (translateDistance > 0) {
  /* 是否到达左边尽头 */
  if (isLeftEnd) return

  if (isLeftOverflow) {
    /* 超出范围,则滑动刚好到达左边末尾的距离 */
    newTranslateX = 0
  } else {
    /* 未超出范围,滑动距离直接与步长相加 */
    newTranslateX += translateDistance
  }

} else if (translateDistance < 0) {
  /* 是否到达右边尽头 */
  if (isRightEnd) return

  if (isRightOverflow) {
    /* 超出范围,则滑动刚好到达右边末尾的距离 */
    newTranslateX += relativeTranslateX + 10 - cityWrapWidth
  } else {
    /* 未超出范围,滑动距离直接与步长相加 */
    newTranslateX += translateDistance
  }
}

const transformString: string = `translateX(${newTranslateX}px)`
cityWrapDom && (cityWrapDom.style.transform = transformString)
this.setState({
  cityWrapTranslateX: newTranslateX
})
  1. 我们会根据传进来的行数来进行数据划分
const { cityList, row } = this.props
const cityWrapWidth: number = cityList.length > 12 ? Math.ceil(cityList.length / row) * 220 : 1320
const cityWrapDom: HTMLElement | null = document.getElementById('city__wrap') as HTMLElement
cityWrapDom && (cityWrapDom.style.width = `${cityWrapWidth}px`)

这里就是简单的判断多少个元素,我们默认显示12个,两行,一行6个这样子,不满12个就分开展示

16edbd9344495627.jpg

设置为三行的时候:
16edbd9f2051b681.jpg

感觉全篇太多文字,很啰嗦,但是也算是一个小总结吧,小菜鸡写出来还是有点兴奋的,大家可以拿出纸和笔在纸上画一画示意图,很快就可以得出关系,然后慢慢推倒出来啦!


zhcxk1998
38 声望2 粉丝