前言
嗨喽,各位小伙伴,今天是圣诞节,祝大家节日快乐。之前有给大家分享过vue实现拖拽视图,目前我正在开发的项目使用的是React Hooks + TS技术栈,前段时间呢基于React + TS 重构了拖拽视图组件并且做了优化,今天刚好有时间写文章分享给大家。我们知道,在开发移动端H5页面时,部分页面可能存在悬浮按,而悬浮按钮往往会出现遮挡正文视图的情况,此时产品经理就会要求你这个悬浮按钮得支持拖拽,不过不用担心,耀哥今天就带着大家去封装一个拖拽视图组件,以便不时之需。
![1a3b2458b81c7f70d3c6aeb66f298a24.gif](https://i-blog.csdnimg.cn/blog_migrate/764c3bdf9c38d47b116de197ec1eab2a.gif)
思考
要想实现拖拽视图组件,我们可将拖拽元素固定定位并且监听拖拽元素的 touchmove
事件实时计算更新 top
及 left
值即可。在封装拖拽视图组件之前,首先要思考以下几个问题:
「1. 调用方式」
调用者应该如何调用我们封装的拖拽视图组件?你要暴露哪些属性供调用者使用?为了让调用者能够自定义拖拽视图,这里我选择使用插槽自定义视图内容,其次暴露供调用者设置初始位置的属性 positon
及监听用户点击拖拽元素的 onTap
属性。所以调用方式如下:
{}}>
"box">拖拽式图内容
「2. 处理初始值」
通常悬浮按钮放置于屏幕右下角位置,所以我默认将拖拽视图放置在距离屏幕右侧15像素,底部80像素的位置,并且将拖拽视图放置的位置以屏幕右侧和底部为基准,即:
- 如果只设置top或者bottom值,则水平方向默认居右;
- 如果只设置left或者right值,则垂直方向默认居下;
- 如果同时设置top、right、bottom、left值,则right/bottom值有效;
- 同一方向,如果同时设置top、bottom值,则bottom值有效;
- 同一方向,如果同时设置left、right值,则right值有效;
「3. 处理边界」
拖拽元素理论上只支持在屏幕内拖拽,不可超出屏幕边界,所以我们只需计算出拖拽元素在屏幕中可拖拽的范围即可。由于拖拽元素使用固定定位并且我们通过改变 top
和 left
属性达到拖拽效果,因此:
- top 取值范围:0 ~ 屏幕高度 - 拖拽元素的高度
- left 取值范围:0 ~ 屏幕宽度 - 拖拽元素的宽度
「4. 核心技术」
- fixed
- getBoundingClientRect()
- touchmove 事件
实现
提示:代码中均附有详细注释,这里直接贴出代码,有不明白的朋友欢迎留言。
首先在components目录下新建拖拽视图文件:
.
├── DragView
├── index.tsx
└── index.less
index.tsx 文件代码:
import React, { FC, memo, useEffect, useRef, useState } from 'react';
import './index.less';
/** 组件属性接口 */
interface IProps {
/** 拖拽元素 */
children: JSX.Element | JSX.Element[];
/** 拖拽元素初始位置 */
position?: {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
/** 点击事件 */
onTap?: () => void;
}
const DragView: FC = props => {// 设置默认初始位置const { position = { right: 15, bottom: 80 } } = props;// refsconst lgWrapper = useRefnull>(null); /** 存储容器对象 */// statesconst [rect, setRect] = useState(() => ({ width: 0, height: 0 })); /** 屏幕尺寸 */const [bounding, setBounding] = useState(() => ({x: 0,y: 0,})); /** 拖拽边界值 */const [pos, setPos] = useState(() => ({ x: 0, y: 0 })); /** 拖拽元素坐标 */// methodsconst calc = () => {// 获取屏幕的尺寸信息const clientWidth = window.innerWidth;const clientHeight = window.innerHeight;if (lgWrapper.current) {// 获取容器元素的尺寸信息const _rect = lgWrapper.current.getBoundingClientRect();// 获取用户设置的位置信息const { top, right, bottom, left } = position;// 定义_pos记录临时坐标,默认在右下侧const _pos = {
x: clientWidth - _rect.width,
y: clientHeight - _rect.height,
};// 单独判断并设置各方向的值if (top !== undefined) {
_pos.y = top;
}if (right !== undefined) {
_pos.x = clientWidth - right - _rect.width;
}if (bottom !== undefined) {
_pos.y = clientHeight - bottom - _rect.height;
}if (left !== undefined) {
_pos.x = left;
}// 同一方向,如果同时设置top、bottom值,则bottom值有效;if (top !== undefined && bottom !== undefined) {
_pos.y = clientHeight - bottom - _rect.height;
}// 同一方向,如果同时设置left、right值,则right值有效;if (left !== undefined && right !== undefined) {
_pos.x = clientWidth - right - _rect.width;
}// 更新拖拽元素位置
setPos({ ..._pos });// 记录容器尺寸信息
setRect(_rect);// 获取拖拽元素在屏幕内可拖拽的边界值
setBounding({
x: clientWidth - _rect.width,
y: clientHeight - _rect.height,
});
}
};// effects
useEffect(() => {if (lgWrapper.current) {// 当组件一加载就计算初始位置信息
calc();
}
}, [lgWrapper]);
useEffect(() => {// 拖拽事件处理函数const onMove = (event: TouchEvent) => {// 获取触点const touch = event.touches[0];// 定位滑块的位置
let x = touch.clientX - rect.width / 2;
let y = touch.clientY - rect.height / 2;// 处理边界if (x 0) {
x = 0;
} else if (x > bounding.x) {
x = bounding.x;
}if (y 0) {
y = 0;
} else if (y > bounding.y) {
y = bounding.y;
}// 更新拖拽视图位置
setPos({ x, y });// 阻止默认行为
event.preventDefault();// 阻止事件冒泡
event.stopPropagation();
};// 监听拖拽事件if (lgWrapper.current) {
lgWrapper.current.addEventListener('touchmove', onMove, {
passive: false,
});
}// 移除拖拽事件return () => {if (lgWrapper.current)
lgWrapper.current.removeEventListener('touchmove', onMove);
};
}, [lgWrapper, bounding, rect]);// renderreturn (
className="lg-drag-view"
style={{
left: `${pos.x}px`,
top: `${pos.y}px`,
}}
onClick={() => {if (props.onTap) props.onTap();
}}
>
{props.children}
);
}; export default memo(DragView);
index.less 文件:
.lg-drag-view {
position: fixed;
z-index: 999;
}
大家可直接复制代码去验证组件是否可用。
尾言
好啦,各位小伙伴,拖拽视图是不是特别简单呢?大家赶快去尝试吧。今天的分享就到这里啦,喜欢这篇文章的朋友可以点赞,也可以直接关注我的公众号,您的关注与支持,是我唯一继续下去的动力!