前言

做的项目中有需要用到排序,里面有一个内容是通过拖动来设定排序方案,所以找了一个项目来分析了一下,然后自己做了一个demo。项目参考:kevenfeng/DragSort: 小程序拖动排序 (github.com)
PS:参考的项目不写注释真的好难看懂,他的这个代码也有其他的功能,不是纯粹的拖动排序,看起来真的很费力

分析

下面是实例,因为用ps处理的图像,所以会有奇怪的抖动?: (
dragsort

需求分析

我们来分析一下这个demo的需求,首先按住滑块的响应区域注意,只能在那个区域中),滑块有一个“被拉上来的”效果,且滑块原来的位置没有内容,接下来我们可以在除了标题外的整个区域中拖动,来选择放置的位置,在我们“拉上来的滑块”覆盖滑块一半高度的时刻,发生交换事件,在我们松手之后,“拉上来的滑块”又“回到”空白区域

我们从逻辑层和视图层两个方面来分析这个demo

视图分析

首先,我们要先确定放置底部滑块的区域,因为这个区域需要能够滚动,我们选择使用scroll-view(可滚动视图区域)来放置所有没有被选中的滑块,接下来我们需要一个能够触发事件的区域(touchArea),即demo中的响应列这个区域需要能够响应我们的点击,并且监听我们移动的位置。

下图蓝色区域为响应区域
dragsort-1

其次,因为选中的滑块需要在视图中能够移动,我们需要使用movable-view(可移动的视图容器)搭配movable-area(限制movable-view的可移动区域)来使用滑块。

这里我们先注意一点,movable和scroll不能直接嵌套使用(同时出现),因为大家都具有滑动的功能,那么小程序又怎么判断你到底是在移动movable还是srcoll?(可能以后有办法,至少目前我不行)在理解了这一点之后,我们就要明白,我们不能直接把一个滑块从“底部”移动到“顶部”,即从scroll-view组件移动到movable的组件中,那么我们的解决办法就是在movable(可移动的视图容器)中去“复制”一个和选中的滑块一模一样的滑块,并通过移动这个滑块来完成排序。

既然movable和scroll不能同时出现,那么我们的解决办法就是控制movable的出现时间即在我们选中滑块的时候,出现movable区域,然后控制滑块来进行排序,接着在取消选中的时候,再让movable区域“消失”

样式分析

首先,能够触发事件的区域touch-Area一直在那个地方,所以需要设定为绝对定位,因为区域高度不定,由js来控制高度,其次,可移动的视图区域movable-area也需要使用绝对定位,因为可移动的区域在视图中永远是标题往下,高度由js来控制。在选中的时候,让下面scroll中被选中的滑块设为透明。在我们为选中的滑块的时候,需要把movable-areadisplay改为none,在触发事件的时候改回来

逻辑分析

首先为触发事件的区域绑定事件,catchtouchstart,catchtouchmove,catchtouchend。

在点击开始catchtouchstart的时候,获取第一次接触的地点的坐标,这里注意一点,我们自己在设置时往往用微信的rpx像素单位,但是此时获取的是px像素单位。然后通过我们得到的像素单位,来获取我们选择的对象(即通过像素获得目标…)得到数据之后将我们“拉上来的滑块”的中点移动到我们接触的点(因为是绝对定位,即获取的点的坐标减去区域外标题的高度再减去一半的滑块高度),这是我们开始点击时需要做的。

接下来是按住移动catchtouchmove的时候,我们可以从事件传来的参数中得到我们目前的视图中坐标,我们需要重新计算坐标,让“拉上来的滑块”的中心一直在我们接触的点,然后又要通过像素计算位置,来进行数组的交换(这里其实是个难点,可以看代码)

最后是松开事件的时候catchtouchend的时候,我们只需要把之前设置的点击触发的属性全部改回去即可

代码

<view class="title">
    <view class="title_row">
        <text class="left">响应</text>
        <text class="right">属性</text>
    </view>
</view>
<view class="content">
    <!-- 滑块的点击区域,用于绑定事件 -->
    <view class="touchArea" style="height:{{scrollPosition.scrollViewHeight}}rpx" 
        catchtouchstart="draggleTouch"catchtouchmove="draggleTouch" catchtouchend="draggleTouch">
    </view>
    <!-- 滑块的可移动区域 -->
    <movable-area class="movable_area {{movableViewPosition.className}}" style="height:{{scrollPosition.scrollViewHeight}}rpx">
        <movable-view class="movable_view" style="height:{{movableViewPosition.everyOptionCell}}rpx"
         direction="vertical" x="{{movableViewPosition.x}}" y="{{movableViewPosition.y}}" damping="1000">
            <view class="content_row shadow">
                <text class="content_row_left">按住</text>
                <text class="content_row_right">{{movableViewPosition.data.name}}</text>
            </view>
        </movable-view>
    </movable-area>
    <scroll-view class="scroll_content"  scroll-y="{{scrollPosition.scrollY}}" 
    style="height:{{scrollPosition.scrollViewHeight}}rpx" bindscroll="bindscroll">
        <block wx:for="{{optionsListData}}" wx:key="id">
            <view class="content_row {{item.isSelect?'black':''}}">
                <text class="content_row_left">按住</text>
                <text class="content_row_right">{{item.name}}</text>
            </view>
        </block>
    </scroll-view>
</view>
.title {
  height: 100rpx;
  border: 1rpx solid #000;
  display: flex;
  width: 100%;
  position: fixed;
  z-index: 200;
  background-color: #fff;
  .title_row {
    width: 100%;
    height: 100rpx;
    position: fixed;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .left {
    padding-left: 100rpx;
  }

  .right {
    padding-right: 100rpx;
  }
}
.content{
  padding-top: 100rpx;
  width: 100%;
  .touchArea{
    z-index: 200;
    position: absolute;
    top:100rpx;
    left: 95rpx;
    width: 80rpx;
  }
  .movable_area{
    width: 100%;
    z-index: 100;
    position: absolute;
    background-color: rgba(0,0,0,0);
    .movable_view{
      width: 100%;
      z-index: 150;
    }
    .content_row{
      background-color: #fff;
    }
  }
  .content_row{
    display: flex;
    height: 150rpx;
    width: 100%;
    align-items: center;
    justify-content:space-between;
    border: 1rpx solid #ccc;
    .content_row_left{
      display: flex;
      width: 80rpx;
      margin-left: 95rpx;
      font-size: 25rpx;
    }
    .content_row_right{
      margin-right: 100rpx;
    }
  }
}
.none{
  display: none;
}
.black{
  background-color: rgba(0,0,0,0);
  color: rgba(0,0,0,0);
}
.shadow{
  box-shadow: 0 0 12px rgba(0, 0, 0, .2)
}
Page({
  data: {
    optionsListData: [

    ],
    scrollPosition: {
      everyOptionCell: 75,
      top: 50,
      scrollTop: 0,
      scrollY: true,
      scrollViewHeight: 1100,
      scrollViewWidth: 375,
      windowViewHeight: 1100,
    },
    movableViewPosition: {
      y: 0,
      className: "none",
      data: {}
    },
    selectItemInfo: {
      selectPosition: 0
    }
  },

  onLoad: function () {
    let optionsListData = [
      { name: '计算机211', id: 1, isSelect: false },
      { name: '计算机212', id: 2, isSelect: false },
      { name: '计算机213', id: 3, isSelect: false },
      { name: '计算机214', id: 4, isSelect: false },
      { name: '计算机215', id: 5, isSelect: false },
      { name: '计算机216', id: 6, isSelect: false },
      { name: '计算机217', id: 7, isSelect: false },
      { name: '计算机218', id: 8, isSelect: false },
      { name: '计算机219', id: 9, isSelect: false },
      { name: '计算机220', id: 10, isSelect: false }
    ]
    this.setData({
      optionsListData: optionsListData
    })
  },
  bindscroll: function (event) {
    let scrollTop = event.detail.scrollTop;
    this.setData({
      'scrollPosition.scrollTop': scrollTop
    })
  },
  //监听事件,进行分别处理
  draggleTouch: function (event) {
    let touchType = event.type;
    switch (touchType) {
      case "touchstart":
        this.scrollTouchStart(event);
        break;
      case "touchmove":
        this.scrollTouchMove(event);
        break;
      case "touchend":
        this.scrollTouchEnd(event);
        break;
    }
  },
  //获取下标
  getOptionInfo: function (code) {
    for (let i = 0, j = this.data.optionsListData.length; i < j; i++) {
      let optionData = this.data.optionsListData[i];
      if (optionData.id == code) {
        optionData.selectIndex = i;
        return i;
      }
    }
    return {};
  },
  //通过y获取对象
  getPositionDomByXY: function (potions) {
    let y = potions.y - this.data.scrollPosition.top + this.data.scrollPosition.scrollTop;//减去滑块之外的距离,获得从第一个滑块到当前滑块的距离
    let optionsListData = this.data.optionsListData;
    let everyOptionCell = this.data.scrollPosition.everyOptionCell;
    //遍历数组,得到我们的数据
    for (let i = 0, j = optionsListData.length; i < j; i++) {
      if (y >= i * everyOptionCell && y < (i + 1) * everyOptionCell) {
        return optionsListData[i];
      }
    }

    return optionsListData[0];
  },
  scrollTouchStart: function (event) {
    let optionsListData = this.data.optionsListData
    let firstTouchPosition = {
      //获取开始地点
      y: event.changedTouches[0].pageY,
    }
    let data = this.getPositionDomByXY(firstTouchPosition);//返回对象
    //movable-area滑块位置处理

    let movableY = firstTouchPosition.y - this.data.scrollPosition.top - this.data.scrollPosition.everyOptionCell / 2;//获取的位置的y减去滑块外的区域再减去一半的行滑块高度,使得滑块中间使我们选中的区域
    this.setData({
      movableViewPosition: {
        y: movableY,
        className: "",
        data: data
      }
    })

    data.selectPosition = event.changedTouches[0].clientY;
    let index = this.getOptionInfo(data.id)
    optionsListData[index].isSelect = true
    this.setData({
      'scrollPosition.scrollY': false,
      selectItemInfo: data,
      optionsListData
    })
  },
  scrollTouchMove: function (event) {
    let moveDistance = event.changedTouches[0].clientY;
    let selectItemInfo = this.data.selectItemInfo
    let selectPosition = selectItemInfo.selectPosition;
    let movableY = event.changedTouches[0].pageY - this.data.scrollPosition.top - this.data.scrollPosition.everyOptionCell / 2;
    let selectIndex = selectItemInfo.selectIndex;
    let optionsListData = this.data.optionsListData
    const everyOptionCell = this.data.scrollPosition.everyOptionCell
    const data = this.data.movableViewPosition.data
    this.setData({
      movableViewPosition: {
        y: movableY,
        className: "",
        data: data
      }
    })
    if (moveDistance - selectPosition + everyOptionCell / 2 > 0 && selectIndex < optionsListData.length - 1) {
      if (optionsListData[selectIndex] == selectItemInfo) {
        optionsListData.splice(selectIndex, 1);
        optionsListData.splice(++selectIndex, 0, selectItemInfo);
        selectPosition += everyOptionCell;
      }
    }
    if (moveDistance - selectPosition + everyOptionCell / 2 < 0 && selectIndex > 0) {
      if (optionsListData[selectIndex] == selectItemInfo) {
        optionsListData.splice(selectIndex, 1);
        optionsListData.splice(--selectIndex, 0, selectItemInfo);
        selectPosition -= everyOptionCell;
      }
    }

    this.setData({
      'selectItemInfo.selectPosition': selectPosition,
      'selectItemInfo.selectIndex': selectIndex,
      optionsListData: optionsListData,
    });
  },
  scrollTouchEnd: function (event) {
    console.log(event)
    let data = this.data.movableViewPosition.data
    let index = this.getOptionInfo(data.id)
    let optionsListData = this.data.optionsListData
    optionsListData[index].isSelect = false
    this.setData({
      movableViewPosition: {
        y: 0,
        className: "none",
        data: {}
      },
      'scrollPosition.scrollY': true,
      'movableViewPosition.className': "none",
      optionsListData: this.data.optionsListData
    })
    console.log(optionsListData)
  }
})

Q.E.D.