高难度自定义实现小程序精美的slider选择器,泰裤辣

832

最近在个人开发的小程序中,想要改版设计一个好看点的滑动选择器,因为自带的滑动选择器实在是太太太丑了。自带的小程序slider长这样:

微信图片_20230426112617.jpg

经过各种APP探索,最终小红书的【身高体重选择器】映入我的眼帘,它长这样:

微信图片_20230426113122.jpg

依葫芦画瓢

有了葫芦好画瓢,接下来就看下怎么在小程序中实现同样的效果吧。

使用scroll-view组件

很显然,这里滚动需要用到scroll-view组件,监听滚动的距离实时计算出对应值。

布局实现

我们先仔细观察下结构

image.png

可以看到,该滑动器主要由两部分组成:浮标及指示尺,其中浮标是需要固定位置显示,所以在写布局的时候,我们不能将浮标标签写在scroll-view里面,需要与scroll-view同级通过定位方式显示。 另外,由于起始值跟最大值需要可以滚动到浮标位置,所以scroll-view里面的起始跟结尾需要有标签宽度进行占位。 指示尺的实现就比较简单,使用盒子+borderRight实现即可。

布局代码实现

说明:以下代码使用uniapp vue3 setup 写法实现

  <view class="relative">
    <!-- 浮标尺 -->
    <view class="pointer-wrap">
      <text class="triangle"></text>
      <text class="slide-num">{{ slideNum }}岁</text>
      <text class="pointer"></text>
    </view>

    <scroll-view
      :scroll-with-animation="true"
      :scroll-left="scrollLeft"
      scroll-x
      enhanced
      :show-scrollbar="false"
      :bounces="true"
      @scroll="onScroll"
      class="relative"
    >
      <view class="expand-line">
        <text class="gap-space"></text>
        <text
          class="line-box"
          :id="'box' + idx"
          :class="{ higher: idx % 5 === 0 }"
          v-for="(_, idx) in max - min + 1"
        >
          <text class="num" v-if="idx % 5 === 0">{{ min + idx }}</text>
        </text>
        <text class="gap-space last"></text>
      </view>
    </scroll-view>
    
  </view>

其中值得一提的是,隐藏scroll-view滚动条通过配置show-scrollbar:false即可。

css样式:


<style lang="scss">
.pointer-wrap {
  position: absolute;
  z-index: 9;
  left: 166px;
  top: 20px;

  .triangle {
    position: absolute;
    top: -10px;
    left: -4px;
    width: 0;
    height: 0;
    border-top: 5px solid #efaf13;
    border-bottom: 5px solid white;
    border-left: 5px solid white;
    border-right: 5px solid white;
  }

  .slide-num {
    position: absolute;
    width: 40px;
    text-align: center;
    font-size: 14px;
    color: #efaf13;
    top: -30px;
    left: -16px;
    font-weight: bold;
  }

  .pointer {
    display: inline-block;
    width: 1px;
    height: 20px;
    background-color: #efaf13;
  }
}

.expand-line {
  position: relative;
  white-space: nowrap;
  padding-top: 20px;
  padding-bottom: 20px;

  .gap-space {
    display: inline-block;
    width: 157px;
    height: 12px;

    &.last {
      width: 171px;
    }
  }

  .line-box {
    box-sizing: border-box;
    position: relative;
    display: inline-block;
    width: 10px;
    height: 12px;
    border-right: 1px solid #ccc;

    &.higher {
      height: 20px;
    }

    .num {
      position: absolute;
      bottom: -18px;
      right: -7px;
      font-size: 12px;
    }
  }
}
</style>

逻辑实现

以上标签布局及样式就简单带过,接下来重点解析js逻辑实现。

监听滚动动画停止

我们在滑动的时候,由于惯性在手指离开的时候,会继续滚动一段距离才停下,所以我们需要监听滚动停止事件。重点来了,小程序该组件当前只提供了手指离开的事件,并没有提供动画滚动结束的api,所以只能取巧实现,代码如下:

function onScroll(e) {
  if (scrollEndTimer) {
    clearTimeout(scrollEndTimer)
    scrollEndTimer = null
  }
  
  scrollEndTimer = setTimeout(() => {
    // 滑动结束
  }, 300)
  
}

通过判断onScroll事件回调,如果300毫秒内没有回调则判断为动画滚动结束(注意:300毫秒参数来源于真机调试测试出来的值,不排除个别机型有差异)

滚动到指示器中间位置,进行精确定位

在滚动的时候,我们的浮标可能落在两条线中间区域,这时候就要判断下,滑动的位置是否超过一半,如果超过一半则跳去下条线的位置,否则回到上条线的位置。

function onScroll(e) {
 const left = e.detail.scrollLeft
 const correctLeft = Math.floor(left / boxWidth) * boxWidth
 const leftMore = left % boxWidth < boxWidth / 2 ? 0 : boxWidth
 scrollLeft.value = correctLeft + leftMore
}

获取间隔盒子实际渲染宽度

上面的boxWidth就是线条之间的间隔(盒子宽度),这边是设为20rpx。注意,这里rpx单位也就意味着他会根据不同设备可能会出现不一样的实际渲染宽度(px单位),例如iPhone 14/Pro 上渲染是10px,iPhone 14 Pro Max则渲染出11px,所以我们在进行计算滑动位置时,需要获取到页面上实际渲染的宽度。实现如下:

const { ctx } = getCurrentInstance()

onMounted(() => {
  const query = uni.createSelectorQuery().in(ctx)
  query
    .select('#box0')
    .boundingClientRect((data) => {
      if (data && data.width) {
        boxWidth = data.width
      }
    })
    .exec()
})

注意,以上代码是通过封装组件方式实现,所以在获取元素大小位置信息时,需要传入this参数,而上面的ctx是uniapp vue3 setup写法。

处理边界值

在滑动到最末端时,由于弹性动画效果,会出现超过最大值或低于最小值,需要处理下实时显示的值

let setValueTimer = null
function onScroll(e) {
  if (!setValueTimer) {
    setValueTimer = setTimeout(() => {
      const left = e.detail.scrollLeft
      const val = props.min + Math.floor(left / boxWidth)
      slideNum.value = val > props.max ? props.max : val
      slideNum.value = val < props.min ? props.min : val
      setValueTimer = null
    }, 60)
  }
}

体验优化:添加震动反馈

为了更好的体验,我们可以在滑动的时候添加震动反馈,但是这里需要注意的是,iPhone 由于很早之前就使用tapic engine震动带来非常好的统一震动体验,所以在iOS端加上震动能增强体验。但是安卓端由于机型众多,使用的震动反馈强度也不一样,反而容易带来不好的体验,所以建议通过判断系统只在iOS端加上震动反馈。

let vibrateShortTimer = null
const systemInfo = uni.getSystemInfoSync()
const isIOS = systemInfo.platform === 'ios'
function onScroll(e) {

  if (isIOS && !vibrateShortTimer) {
    vibrateShortTimer = setTimeout(() => {
      uni.vibrateShort({
        type: 'light',
      })
      vibrateShortTimer = null
    }, 300)
  }
}

最后看下实现效果吧

微信图片_20230428103629.jpg

微信小程序搜索【识光】小程序可以查看体验,或者评论区扫码即可查看

不完美的地方

经过真机测试,在部分安卓手机上会出现浮标线未能对齐间隔线的细微间距,这个可能由于分辨率或其他原因影响暂时无法保证做到所有机型完美对齐。

获取组件源码

目前该组件已经封装成组件使用(uniapp vue3 setup),需要源码的欢迎私信我获取呀~(^_-)