需求是写一个小程序,用来实现一个app大部分功能。里面有个功能是列表的左划删除,可能app那边有现成的组件/库可以用,小程序官方没有提供这样的组件,于是求助于google。
搜到了这个 ,看起来挺好的,但是要求高度固定,我列表里面有图片,我无法确定图片的高度固定,跟相关人员沟通起来又是一堆废话,算是备选。
然后找到了这个
nutui的swipe组件,源码 vue写的,我项目用的是react,不过没关系,vue3的Composition api与react hooks是师出同门。看懂它然后翻译一下(我又找到了一个适合自己的巨人)。
源码解读 布局 html、css、js永远是一家人,不要看不起html、css,优秀的布局可以省好多代码。
布局分为3部分,content、left、right,content为列表的主要内容,left/right分别表示向右/左滑所展示的额外内容,然后left、right分别绝对定位,并且配合translate移出content的可见范围。
逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <view :class="classes" :style="touchStyle" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd" > <view class="nut-swipe__left" ref="leftRef" :id="'leftRef-' + refRandomId"> <slot name="left"></slot> </view> <view class="nut-swipe__content"> <slot name="default"></slot> </view> <view class="nut-swipe__right" ref="rightRef" :id="'rightRef-' + refRandomId"> <slot name="right"></slot> </view> </view> </template>
1 2 3 4 5 const touchStyle = computed (() => { return { transform : `translate3d(${state.offset} px, 0, 0)` }; });
一目了然。有三个slot,分别对应上述布局中的content、left、right。监听了最外层view的touch事件,目的是根据touch时的各种值来控制offset的值,从而生成touchStyle,使最外层的view移动。
那么如何得到touch事件的各种值呢,他们封装了个useTouch
代码还是挺简的
1 2 3 4 5 6 7 const startX = ref (0 ); const startY = ref (0 );const deltaX = ref (0 );const deltaY = ref (0 ); const offsetX = ref (0 );const offsetY = ref (0 ); const direction = ref<Direction >('' );
有三个函数,reset/start/move,start与move分别在touchStart与touchMove时调用,用来记录滑动的距离,调用start时会调用reset。
值得一提的是,这里计算滑动方向的方法
1 2 3 4 5 6 7 8 9 function getDirection (x: number , y: number ) { if (x > y && x > MIN_DISTANCE ) { return 'horizontal' ; } if (y > x && y > MIN_DISTANCE ) { return 'vertical' ; } return '' ; }
是根据每次滑动offsetX与offsetY的大小以及滑动距离是否大于10决定的。
再来看swipe组件处理滑动的逻辑
1 2 3 4 5 6 7 8 9 const initWidth = async ( ) => { leftRefWidth.value = await getRefWidth (leftRef); rightRefWidth.value = await getRefWidth (rightRef); }; onMounted (() => { setTimeout (() => { initWidth (); }, 100 ); });
首先拿到left slot与right slot的宽度,如果没有,就是0。然后会监听touchstart、touchmove、touchend事件。
touchstart
1 2 3 4 onTouchStart (event: Event ) {if (props.disabled ) return ; touch.start (event); }
平淡无奇,核心是调用了touch.start方法。
onTouchMove
1 2 3 4 5 6 7 8 9 10 11 12 13 14 async onTouchMove (event: Event ) { if (props.disabled ) return ; touch.move (event); if (touch.isHorizontal ()) { state.moving = true ; setoffset (touch.deltaX .value ); if (props.touchMovePreventDefault ) { event.preventDefault (); } if (props.touchMoveStopPropagation ) { event.stopPropagation (); } } }
核心是调用touch.move以及setoffset方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const setoffset = (deltaX: number ) => { position = deltaX > 0 ? 'right' : 'left' ; let offset = deltaX; switch (position) { case 'left' : if (opened && oldPosition === position) { offset = -rightRefWidth.value ; } else { offset = Math .abs (deltaX) > rightRefWidth.value ? -rightRefWidth.value : deltaX; } break ; case 'right' : if (opened && oldPosition === position) { offset = leftRefWidth.value ; } else { offset = Math .abs (deltaX) > leftRefWidth.value ? leftRefWidth.value : deltaX; } break ; } state.offset = offset; };
这几行if else写的很精妙。swipe组件同时支持左/右滑,先来看左划的逻辑。
opened表示是否已经打开,如果是true并且本次滑动方向与上一次一致,那么offset永远等于-rightRefWidth。
假设是第一次滑,并且right slot不为空,走的是这句代码。
1 offset = Math .abs (deltaX) > rightRefWidth.value ? -rightRefWidth.value : deltaX;
精妙的三元表达式,无形中处理了很多边界情况,先看最正常的:第一次滑,并且right slot不为空。
这时Math.abs(deltaX) < rightRefWidth,所以offset为deltaX。
那什么时候Math.abs(deltaX) >= rightRefWidth呢,要么right slot完全被划出,要么right slot 为空。假设right slot为空,这时再左滑是滑不动的。
右滑同理。
onTouchEnd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 onTouchEnd ( ) {if (state.moving ) { state.moving = false ; oldPosition = position; switch (position) { case 'left' : if (Math .abs (state.offset ) <= rightRefWidth.value / 2 ) { close (); } else { state.offset = -rightRefWidth.value ; open (); } break ; case 'right' : if (Math .abs (state.offset ) <= leftRefWidth.value / 2 ) { close (); } else { state.offset = leftRefWidth.value ; open (); } break ; } } }
这个方法隐藏着一个小小的交互体验。假设onTouchEnd触发时,左滑的距离小于rightRefWidth的一半,会自动close,反之open。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const open = (p: SwipePosition = '' ) => { opened = true ; if (p) { state.offset = p === 'left' ? -rightRefWidth.value : leftRefWidth.value ; } }; const close = ( ) => { state.offset = 0 ; opened = false ; };
open与close所做的事是分别让offset达到最大值或者最小值。