程序员玛丽的星海方舟

关于我如何艰难地在公司业务中实现侧滑置顶删除功能(二)

上一章内容:关于我如何艰难地在公司业务中实现侧滑置顶删除功能(一)

原代码不再贴出。

新的工程需求

上次算是做出来一个像样的侧滑置顶删除功能了,在微信小程序调试中以及安卓机上调试都没有问题,但在iOS测试机上运行时却出现了问题:

不知道是因为这个APP在iOS端上的一个整体功能还是微信小程序原生的触发,在右滑取消菜单的时候会触发一个返回到上级页面的操作……着实是让人不解。理论上,iOS原生的返回手势只有在手指接近屏幕左侧且右滑的时候才会触发。事实上,这个APP的另一个模块也有类似的侧滑删除功能,且右滑该条消息取消菜单时不会触发返回。于是mentor带我去请教了负责这块的iOS开发大哥,他表示:可以尝试在右滑取消时接管手势,但是微信小程序如何实现他也不知道。

初步想法:阻止手势穿透

微信小程序的事件有着冒泡机制,而手势则是整个窗口的一些事件(具体事件为tap, touchstart, touchmove, touchend, touchcancel等)。在wxml代码中进行事件捕捉时,需要设定组件的属性catchtap/bindtap, catchtouchstart/bindtouchstart等,这些都是用来捕捉微信小程序原生事件的属性。catch和bind的区别则是,catch会阻止事件冒泡,而bind不会。也就是说,只要设置catchtap,tap事件就会被局限在这个组件上,在它之后可能会收到事件的父组件、子组件都无法收到这个事件了。所以,如果对消息条组件设置catchtap,会不会就能够阻止页面返回呢?

尝试:将bind换成catch

index.wxml
1
2
3
<view wx:for="{{dataList}}" data-index="{{index}}" class="item {{item.isTouchMove ? 'touch-move-active' : ''}}" catchtouchstart="touchStart" catchtouchmove="touchMove">
<!-- 内容 -->
</view>

可以是可以阻止滚动,但结果带来了其他的麻烦,因为catch也阻止了上下滑动触发页面滚动的手势,所以如果有很多内容,手势会被限制在某条组件内,页面就动不了了。

尝试:使bind和catch的事件根据isTouchMove进行变化

index.wxml
1
2
3
4
<view wx:for="{{dataList}}" data-index="{{index}}" class="item {{item.isTouchMove ? 'touch-move-active' : ''}}" catchtouchstart="{{item.isTouchMove ? 'touchstart' : ''}}" catchtouchmove="{{item.isTouchMove ? 'touchmove' : ''}}" 
bindtouchstart="{{item.isTouchMove ? '' : 'touchstart'}}" bindtouchmove="{{item.isTouchMove ? '' : 'touchmove'}}">
<!-- 内容 -->
</view>

但是没有用,微信小程序并不能同时使用bind和catch两种属性,只会取其一。之后想想,这样的思路也不对,传空的事件名并不会使得事件解绑,而是引发了一个空的函数。

尝试:建立两个一模一样但分别是bind和catch的组件,然后根据isTouchMove的值选择显示哪一个

index.wxml
1
2
3
4
5
6
7
8
9
10
<block wx:for="{{dataList}}" data-index="{{index}}">
<!-- 一个是没有被打开菜单的(支持上下滑动和返回) -->
<view wx:if="{{!item.isTouchMove}}" class="item {{item.isTouchMove ? 'touch-move-active' : ''}}" bindtouchstart="{{item.isTouchMove ? '' : 'touchstart'}}" bindtouchmove="{{item.isTouchMove ? '' : 'touchmove'}}">
<!-- 内容 -->
</view>
<!-- 被打开菜单的(阻止上下滑动和返回) -->
<view wx:else class="item {{item.isTouchMove ? 'touch-move-active' : ''}}" catchtouchstart="{{item.isTouchMove ? 'touchstart' : ''}}" catchtouchmove="{{item.isTouchMove ? 'touchmove' : ''}}">
<!-- 内容 -->
</view>
</block>

效果是可行,但是会牺牲滑出和收起菜单的动画效果,因为通过css实现的动画效果只能在同一个组件中进行。

最终方案:建立两个一模一样但分别是bind和catch的组件,在每次动画结束后悄悄替换

绞尽脑汁之后,突然灵机一动想到了这样一个比较邪教的方法。

思路:设置一个hidden属性(用来标记被划开菜单的条目,即需要设成catch的条目的index),在每次动画开始时进行setTimeOut设定延时,等待动画完毕后渲染hidden,根据hidden的值悄悄将所有条目替换成catch或bind的组件。

注意:css中,动画设定是0.4s,延时应当适当小于这个值。

index.wxml
1
2
3
4
5
6
7
8
9
10
<block wx:for="{{dataList}}" data-index="{{index}}">
<!-- 一个是没有被打开菜单的(支持上下滑动和返回) -->
<view wx:if="{{index!==hidden}}" class="item {{item.isTouchMove ? 'touch-move-active' : ''}}" bindtouchstart="{{item.isTouchMove ? '' : 'touchstart'}}" bindtouchmove="{{item.isTouchMove ? '' : 'touchmove'}}">
<!-- 内容 -->
</view>
<!-- 被打开菜单的(阻止上下滑动和返回) -->
<view wx:else class="item {{item.isTouchMove ? 'touch-move-active' : ''}}" catchtouchstart="{{item.isTouchMove ? 'touchstart' : ''}}" catchtouchmove="{{item.isTouchMove ? 'touchmove' : ''}}">
<!-- 内容 -->
</view>
</block>
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/* Page */
data: {
dataList: [/*存放的数据,每条中应有一个默认为false的属性isTouchMove*/],
startX: 0,
startY: 0,
hidden: null,//标记被隐藏的块index
},


touchStart(e) {
let dataList = this.data.dataList;
dataList.forEach(item => {
if (item.isTouchMove) {
item.isTouchMove = !item.isTouchMove;
}
});
this.setData({
dataList: dataList,
});

this.data.hidden = null; //先把hidden置null, 但不渲染

this.data.startX = e.touches[0].clientX;
this.data.startY = e.touches[0].clientY;
},
touchMove(e) {
let moveX = e.changedTouches[0].clientX,
moveY = e.changedTouches[0].clientY,
curIndex = e.currentTarget.dataset.index,
dataList = this.data.dataList,
angle = this.angle(
{ X: this.data.startX,
Y: this.data.startY
}, { X: moveX,
Y: moveY
}
);
const that = this; //用于在forEach函数里访问data
dataList.forEach((item, index) => {
if (curIndex === index && angle < 30 && moveX < this.data.startX) {
item.isTouchMove = true;
that.data.hidden = index;//如果有块被滑动,将hidden设为此块,否则就会保持在null
} else {
item.isTouchMove = false;
}
});

this.setData({
dataList: dataList,
});

setTimeOut({
that.setData({
hidden: that.data.hidden //0.3s后渲染hidden来替换组件,此时动画基本放完了
})
}, 300);
},

经检验,如果动画设在0.3s,对于快速滑动也能比较好地适应。既保证了动画的流畅性,又保证了接管手势,不触发意外返回。

总结

至此,这个功能总算是顺利完成。虽然性能问题可能有待商榷(因为要进行大量的if else),但是目前来说是没有想出更好的解决方法。我感觉这个功能对我来说还是比较有锻炼性的,如果另辟蹊径也能达到目的的话,不妨大胆尝试。如有更好的想法,欢迎在评论区留言。