移动端滑动(touch)选项并进行单选

  • 移动端滑动选项并进行单选,点击查看多选效果案例
  • 通过 touchstarttouchmovetouchendtouchcancel 事件实现
  • 通过父元素代理事件的方式实现子组件点击选中选项
  • 如果选项添加 disabled 属性将不会被选中
  • 移动端拖拽父元素 .box.options 元素时,是有拖拽效果的,去除拖拽效果有两种方式:
    ① 用 css pointer-events: none; 阻止拖拽,.box 元素使用的是这种方式
    ② 用 js event.preventDefault() 阻止元素的事件, .options 元素使用的是这种方式
  • 如果需要通过鼠标滚轮来切换选项,可以使用 wheel 来实现,本例没有实现
  • 强制使选项在所选则的框中,也可以使用css scroll-snap-align: start; scroll-snap-type: y mandatory; 实现,本例是使用 js 计算出位置。

在这里插入图片描述

  • css 代码
     * {
        margin: 0;
        padding: 0;
      }
    
      ul,
      li {
        list-style: none;
      }
    
      .box {
        height: 300px;
        width: 400px;
        margin: 50px auto;
        border: 1px solid #000;
        border-radius: 12px;
        position: relative;
        /* background-color: red; */
        touch-action: none; /* 元素不能滑动 */
      }
    
      .box .columns {
        overflow: hidden;
        /* overflow-y: auto; */
        height: 100%;
      }
    
      .box .columns .options {
        transition-timing-function: cubic-bezier(0.23, 1, 0.68, 1);
        transition-duration: 0ms;
        transition-property: all;
      }
    
      .box .columns .options li {
        height: 60px;
        line-height: 60px;
        text-align: center;
        font-weight: bold;
      }
    
      .box .columns .mask {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        border-radius: 12px;
        z-index: 1;
        pointer-events: none; /*不能对鼠标事件做出反应 */
        background-repeat: no-repeat;
        background-position: top, bottom;
        -webkit-transform: translateZ(0);
        transform: translateZ(0);
        background: linear-gradient(
          to bottom,
          rgba(255, 255, 255, 0.9) 0%,
          rgba(255, 255, 255, 0.1) 45%,
          rgba(255, 255, 255, 0.1) 55%,
          rgba(255, 255, 255, 0.9) 100%
        );
      }
    
      .box .columns .hairline {
        border-bottom: 1px solid #000;
        position: absolute;
        top: 50%;
        right: 16px;
        left: 16px;
        height: 60px; /*等于li的高度*/
        transform: translateY(-50%);
        z-index: 2;
        pointer-events: none;
      }
    
  • html 代码
    <div class="box">
      <div class="columns">
        <ul class="options">
          <li data-index="0">选项一</li>
          <li data-index="1">选项2</li>
          <li disabled data-index="2">选项3</li>
          <li data-index="3">选项4</li>
          <li data-index="4">选项5</li>
          <li data-index="5">选项6</li>
          <li disabled data-index="6">选项7</li>
          <li data-index="7">选项8</li>
          <li data-index="8">选项9</li>
        </ul>
        <div class="mask"></div>
        <div class="hairline"></div>
      </div>
    </div>
    
  • javascript 代码
    const boxEl = document.querySelector('.box')
      const columnsEl = boxEl.querySelector('.columns')
      const optionsEl = columnsEl.querySelector('.options')
      const liFirstEl = optionsEl.querySelector('li:first-child')
    
      const boxHeight = boxEl.clientHeight
      const liHeight = liFirstEl.offsetHeight
      const liAllEl = optionsEl.querySelectorAll('li')
      const liCount = liAllEl.length
    
      let startY = 0
      let endY = 0
      let currentY = 0 // 当前Y轴移动的位置
      let isTouch = false // 是否是滑动事件
      const initPositionY = boxHeight / 2 - liHeight / 2 // 进入页面时Y轴开始的位置
      let beginPositionY = initPositionY // 每次滚动后Y轴开始的位置
      const maxTranslateY = initPositionY + liHeight // Y轴最大移动距离(向下移动)
      const minTranslateY = boxHeight / 2 - liCount * liHeight // Y轴最小移动距离(向上移动)
    
      ;(function () {
        // 初始化页面信息
        optionsEl.setAttribute(
          'style',
          `transition-duration: 0ms; transition-property: all;`
        )
    
        // 寻找第一个未被禁用的选项
        let index = -1
        for (let i = 0; i < liCount; i++) {
          if (!liAllEl[i].hasAttribute('disabled')) {
            index = i
            break
          }
        }
    
        if (index > -1) {
          liAllEl[index].classList.add('selected')
          beginPositionY = initPositionY - index * liHeight
          setTransform(beginPositionY, optionsEl)
        } else {
          console.log('没有可用的选项')
          beginPositionY = boxHeight / 2 + liHeight / 2
          setTransform(beginPositionY, optionsEl)
        }
      })()
    
      // 设置元素在Y轴的移动距离
      function setTransform(value, el) {
        el.style.transform = `translate3d(0px, ${value}px, 0px)`
      }
    
      // 获取元素在Y轴的移动距离
      function getTranslateY(el) {
        const translateStr = el.style.transform
        const valueStr = translateStr.match(/\(([^)]*)\)/)
        const valueArr = valueStr[1].split(',')
        return Number(valueArr[1].replace('px', ''))
      }
    
      // 给元素设置动画
      function setTransitionDuration(value, el) {
        el.style.transitionDuration = `${value}ms`
        el.style.transitionProperty = value === 0 ? 'none' : 'all'
      }
    
      // 重置动画
      function setTransitionNone(el) {
        setTimeout(() => {
          setTransitionDuration(0, el)
        }, 300)
      }
    
      // 设置 class
      function setClassName(index, elList) {
        for (let i = 0; i < elList.length; i++) {
          if (i === index) {
            elList[i].classList.add('selected')
          } else {
            elList[i].classList.remove('selected')
          }
        }
      }
    
      // 阻止父元素的滑动
      columnsEl.addEventListener(
        'touchmove',
        function (e) {
          e.preventDefault()
        },
        false
      )
    
      // 父元素代理子元素点击事件
      columnsEl.addEventListener(
        'click',
        function (e) {
          console.log('click 事件触发')
    
          const target = e.target
          let index = 0
    
          // 方案一:遍历所有 li 得到 index
          // for (let i = 0; i < liCount; i++) {
          //   if (liAllEl[i] === target) {
          //     index = i
          //     break
          //   }
          // }
    
          // 方案二: 根据 data-index 获取当前 li 的index
          index = target.dataset.index
    
          if (liAllEl[index].hasAttribute('disabled')) {
            return false
          }
    
          beginPositionY = initPositionY - index * liHeight
          setTransitionDuration(200, optionsEl)
          setTransform(beginPositionY, optionsEl)
          setTransitionNone(optionsEl)
          setClassName(index, liAllEl)
        },
        false
      )
    
      // 开始滑动
      optionsEl.addEventListener(
        'touchstart',
        function (e) {
          console.log('touchstart 事件触发')
          startY = event.targetTouches[0].pageY
        },
        false
      )
    
      // 滑动中
      optionsEl.addEventListener(
        'touchmove',
        function (e) {
          endY = event.targetTouches[0].pageY
          let moveY = endY - startY
          if (moveY !== 0) {
            isTouch = true
          }
          console.log('touchmove 事件触发')
          currentY = beginPositionY + moveY
    
          // Y 轴位置大于最大位置
          if (currentY > maxTranslateY) {
            currentY = maxTranslateY
          } else if (currentY < minTranslateY) {
            currentY = minTranslateY
          }
          setTransform(currentY, optionsEl)
        },
        false
      )
    
      function touchend() {
        // 滑动事件会和 click 事件冲突
        if (!isTouch) {
          return false
        }
        console.log('touchend 事件触发')
        let index
        setTransitionDuration(200, optionsEl)
        const isDragDown = endY - startY > 0 // 是否向上拖动
    
        // 超过头部与底部位置
        if (currentY >= maxTranslateY) {
          currentY = currentY - liHeight
          index = 0
        } else if (currentY <= minTranslateY) {
          currentY = currentY + liHeight / 2
          index = liCount - 1
        } else {
          // 根据不同的方向,回正所要选中的选项
          // 即:强制定位子列表元素在 hairline 中
          if (isDragDown) {
            // 向下拖动
            index = Math.floor((initPositionY - currentY) / liHeight)
          } else {
            // 向上拖动
            index = Math.ceil((initPositionY - currentY) / liHeight)
          }
          index = index <= 0 ? 0 : index >= liCount - 1 ? liCount - 1 : index
          currentY = initPositionY - liHeight * index
        }
    
        // 判断选项是否为不可选
        if (liAllEl[index].hasAttribute('disabled')) {
          let optionalIndex = index
          // 向下拖动
          if (isDragDown) {
            for (let i = index; i >= 0; i--) {
              if (!liAllEl[i].hasAttribute('disabled')) {
                optionalIndex = i
                break
              }
            }
          } else {
            // 向上拖动
            for (let i = index; i < liCount; i++) {
              if (!liAllEl[i].hasAttribute('disabled')) {
                optionalIndex = i
                break
              }
            }
          }
    
          // 该选项的 上边/下边 有可用的选项
          if (index !== optionalIndex) {
            console.log('optionalIndex------' + optionalIndex)
            index =
              optionalIndex <= 0
                ? 0
                : optionalIndex >= liCount - 1
                ? liCount - 1
                : optionalIndex
            currentY = initPositionY - liHeight * index
          } else {
            //上边/下边 没有可用选项,还原位置
            index = index <= 0 ? 0 : index >= liCount - 1 ? liCount - 1 : index
            currentY = beginPositionY
          }
        }
    
        setTransform(currentY, optionsEl)
        beginPositionY = currentY
        setClassName(index, liAllEl)
        isTouch = false
        setTransitionNone(optionsEl)
      }
      // 滑动结束
      optionsEl.addEventListener('touchend', touchend, false)
    
      // 滑动取消
      optionsEl.addEventListener(
        'touchcancel',
        (evt) => {
          evt.preventDefault()
          console.log('touchcancel 事件触发')
          touchend()
        },
        false
      )
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值