NutUI组件库的Drag组件,使用时组件内部报错。于是自己写了个,这个在模拟器和电脑端小程序环境下拖拽正常,非常丝滑,但在真机环境下就有一个很奇怪的现象:
当拖拽的起始方向是从上往下时,拖拽很卡顿,拖拽元素不跟手,而起始方向是从下往上,则很丝滑。百思不得其解。不知道是小程序中需要特殊处理还是怎么回事,因为从代码逻辑分析,完全找不到造成这个问题的原因。代码如下:
import { useRef, PropsWithChildren, useEffect, CSSProperties } from 'react';
import { View } from '@tarojs/components';
import Taro from '@tarojs/taro';
import useScreenInfo from '@/hooks/useScreenInfo'; // 这里的代码请往后翻
import { throttle } from '@/hooks/utils'; // 这里的代码请往后翻
interface Props extends PropsWithChildren {
children: React.ReactNode;
childrenSelector?: string;
dragY?: boolean;
dragX?: boolean;
style?: CSSProperties;
className?: string;
};
let isMove = false;
let diffX = 0;
let diffY = 0;
const Draggable = ({ children, childrenSelector, dragY = true, dragX = false, style = {}, className = ''}: Props) => {
const draggableRef = useRef(null);
const elementSize = useRef({ width: 0, height: 0, left: 0, top: 0 });
const { current: draggableId } = useRef('id_' + new Date().getTime());
const { screenWidth, screenHeight, navBarHeight } = useScreenInfo();
// 仅在组件初始化时获取元素尺寸
useEffect(() => {
Taro.createSelectorQuery()
.select(childrenSelector ?? `#${draggableId}`)
.boundingClientRect()
.exec((res) => {
if (res[0]) {
elementSize.current = {
width: res[0].width,
height: res[0].height,
left: res[0].left,
top: res[0].top,
};
if (childrenSelector) {
// @ts-ignore
draggableRef.current.style.width = `${res[0].width}px`;
// @ts-ignore
draggableRef.current.style.height = `${res[0].height}px`;
};
// @ts-ignore
draggableRef.current.style.opacity = 1;
}
});
}, []);
const handleTouchStart = (e) => {
if (!dragX && !dragY) return;
Taro.createSelectorQuery()
.select(childrenSelector ?? `#${draggableId}`)
.boundingClientRect()
.exec((res) => {
if (res[0]) {
elementSize.current = {
width: res[0].width,
height: res[0].height,
left: res[0].left,
top: res[0].top,
};
isMove = true;
diffX = e.touches[0].clientX - res[0].left;
diffY = e.touches[0].clientY - res[0].top;
}
});
};
const handleTouchMove = (e) => {
if (!isMove) return
let { width, height, top, left } = elementSize.current;
const touchX = e.touches[0].clientX;
const touchY = e.touches[0].clientY;
// 补偿拖拽时元素的位置,使鼠标能处于按下时相对元素的位置不变
let x = dragX ? touchX - diffX : left;
let y = dragY ? touchY - diffY : top;
if (dragX) {
// 屏幕左边缘
if ((touchX - diffX) <= 0) x = 0;
// 屏幕右边缘
if ((touchX + (width - diffX)) >= screenWidth) x = screenWidth - width;
};
if (dragY) {
// 屏幕底部
if ((touchY + (height - diffY)) >= screenHeight) y = screenHeight - height;
// 屏幕顶部
if ((touchY - diffY) <= navBarHeight) y = navBarHeight;
};
updateDraggablePosition(x, y)
};
const updateDraggablePosition = (x, y) => {
// @ts-ignore
draggableRef.current.style.left = `${x}px`;
// @ts-ignore
draggableRef.current.style.top = `${y}px`;
// @ts-ignore
draggableRef.current.style.bottom = `unset`;
// @ts-ignore
draggableRef.current.style.right = `unset`;
};
return (
<View
id={draggableId}
className={`dd-draggable ${className}`}
ref={draggableRef}
onTouchMove={throttle(handleTouchMove, 10)}
onTouchStart={handleTouchStart}
onTouchEnd={() => {
isMove = false;
diffX = 0;
diffY = 0;
}}
style={style}
>
{children}
</View>
);
};
export default Draggable;
// useScreenInfo代码:
import { useEffect, useState } from 'react';
import Taro from '@tarojs/taro';
const useScreenInfo = (callback?: (data: {screenWidth: number, screenHeight: number, navBarHeight: number}) => void) => {
const [screenInfo, setScreenInfo] = useState({
screenWidth: 0,
screenHeight: 0,
navBarHeight: 0,
});
useEffect(() => {
const { windowWidth, windowHeight, statusBarHeight = 0 } = Taro.getWindowInfo();
const menuButtonRect = Taro.getMenuButtonBoundingClientRect();
let navBarHeight =
menuButtonRect.height + (menuButtonRect.top - statusBarHeight) * 2 + statusBarHeight;
const platform = Taro.getDeviceInfo()?.platform;
if (['windows', 'mac'].includes(platform)) navBarHeight = menuButtonRect.height * 1.3;
const data = {
screenWidth: windowWidth,
screenHeight: windowHeight,
navBarHeight,
};
setScreenInfo(data);
if (typeof callback === 'function') callback(data);
}, []);
return screenInfo;
};
export default useScreenInfo;
// throttle
type ThrottleOptions = {
leading?: boolean; // 是否在开始时立即调用
trailing?: boolean; // 是否在延迟结束后调用一次
};
export function throttle<T extends (...args: any[]) => any>(
func: T,
wait: number,
options: ThrottleOptions = { leading: true, trailing: true }
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
let lastArgs: Parameters<T> | null = null;
let lastInvokeTime = 0;
const { leading = true, trailing = true } = options;
const invokeFunc = () => {
if (lastArgs) {
func(...lastArgs); // 这里确保 lastArgs 不为 null
lastInvokeTime = Date.now();
lastArgs = null;
}
};
const throttled = (...args: Parameters<T>) => {
const now = Date.now();
if (!lastInvokeTime && !leading) {
lastInvokeTime = now;
}
const remaining = wait - (now - lastInvokeTime);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
lastArgs = args;
invokeFunc();
} else if (trailing && !timeout) {
lastArgs = args;
timeout = setTimeout(() => {
timeout = null;
if (trailing) {
invokeFunc();
}
}, remaining);
}
};
throttled.cancel = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
lastArgs = null;
lastInvokeTime = 0;
};
return throttled;
}