《手写可拖拽吸附的悬浮球组件》有点长,但收获满满
这是我参与更文挑战的第1天,活动详情查看: 更文挑战
前言
目前,前端生态圈中各种各样的UI framework 触目皆是,但有没有发现很多的UI framework 都没有悬浮球这个组件 ,其实这个也不难实现,所以我决定要亲手写一个出来!
实现思路
- 获取屏幕的
width
和height
,即得到悬浮球移动的范围 - 利用CSS中的
position
的absolute
属性,同时搭配left
和top
属性来实现元素位置
拖拽事件的过程
选中元素 > 拖动元素 > 拖动结束
开始我们的主题
实现一个类,让用户传入需要拖动的DOM元素
先定义一个类并导出,这个类命名为Drag
,并先定义一些必要的属性
export default class Drag{
// 元素
element: HTMLElement;
// 屏幕尺寸
screenWidth: number;
screenHeight: number;
// 元素大小
elementWidth: number;
elementHeight: number;
isPhone: boolean;
// 当前元素坐标
elementX: number;
elementY: number;
// 元素offset
elementOffsetX: number;
elementOffsetY: number;
// 是否处于拖动状态
moving: boolean;
// 吸附
autoAdsorbent: boolean;
// 隐藏
hideOffset: number;
}
复制代码
在Drag
类中,创建一个构造函数,声明需要传入的参数,元素是必不可少的,所以我们第一个参数就是DOM元素了
constructor(element: HTMLElement) {
// 我需要传入一个DOM元素,它是被用户拖动的元素
}
复制代码
- 初始化一些参数
- 获取屏幕的宽高
- 获取元素的宽高
- 判断设备,如果是电脑端设备则抛出一个
error
- 将元素
position
属性的值设定为absolute
constructor(element: HTMLElement) {
this.element = element;
this.screenWidth = window.innerWidth || window.outerWidth || 0;
this.screenHeight = window.innerHeight || window.outerHeight || 0;
this.elementWidth = this.element.offsetWidth || 0;
this.elementHeight = this.element.offsetHeight || 0;
this.isPhone = /(iPhone|iPad|iPod|iOS|Android)/i.test(navigator.userAgent);
this.element.style.position = 'absolute';
this.elementX = 0;
this.elementY = 0;
this.elementOffsetX = 0;
this.elementOffsetY = 0;
this.moving = false;
if (!this.isPhone) {
console.error('警告!!当前插件版本只兼容移动端');
}
}
复制代码
- 定义一个
watchTouch
方法,用来给拖拽元素添加事件- 这里还有个点需要注意,
touchEvent
是不能直接获取到元素的offset值的,所以我们利用了touchObject.pageX / touchObject.pageY - DOMRect.left / DOMRect.top
来获得元素offset
值
- 这里还有个点需要注意,
private watchTouch(): void {
this.element.addEventListener('touchstart', (event: TouchEvent) => {
const rect = (event.target as HTMLElement).getBoundingClientRect();
// 页面被卷去的高度
// 不兼容IE
const docScrollTop = document.documentElement.scrollTop;
this.elementOffsetX = event.targetTouches[0].pageX - rect.left;
this.elementOffsetY = event.targetTouches[0].pageY - rect.top - docScrollTop;
this.moving = true;
this.element.addEventListener('touchmove', this.move.bind(this), { passive: false });
});
window.addEventListener('touchend', () => {
this.moving = false;
document.removeEventListener('touchmove', this.move);
});
}
复制代码
- 定义一个设定元素位置方法,传入
x
和y
来设定left
和top
值
private setElementPosition(x: number, y: number): void {
// 溢出处理
// 溢出范围
// 但页面超出屏幕范围,计算当前屏幕范围
const leftScope = this.moving ? 0 : 0 - this.hideOffset;
// 当前屏幕right最大值
const rs = this.screenWidth - this.elementWidth;
const rightScope = this.moving ? rs : rs + this.hideOffset;
const bottomScope = this.screenHeight - this.elementHeight;
if (x <= leftScope && y <= 0) {
[x, y] = [leftScope, 0];
} else if (x >= rightScope && y <= 0) {
[x, y] = [rightScope, 0];
} else if (x <= leftScope && y >= bottomScope) {
[x, y] = [leftScope, bottomScope];
} else if (x >= rightScope && y >= bottomScope) {
[x, y] = [rightScope, bottomScope];
} else if (x > rightScope) {
x = rightScope;
} else if (y > bottomScope) {
y = bottomScope;
} else if (x <= leftScope) {
x = leftScope;
} else if (y <= 0) {
y = 0;
}
this.elementX = x;
this.elementY = y;
this.element.style.top = `${y}px`;
this.element.style.left = `${x}px`;
}
复制代码
- 定义一个
move
方法,它将调用上面设定的setElementPosition
方法
private move(event: TouchEvent): void {
event.preventDefault();
if (!this.moving) return;
this.elementY = (event.touches[0].pageX - this.elementOffsetX);
this.elementX = (event.touches[0].pageY - this.elementOffsetY);
const ex = (event.touches[0].pageX - this.elementOffsetX);
const ey = (event.touches[0].pageY - this.elementOffsetY);
this.setElementPosition(ex, ey);
}
复制代码
到了这里我们的组件已经可以实现简单的拖拽了!
但这还达不到我们前面说到的吸附功能
我们继续给Drag
类添加个吸附功能
- 吸附思路
- 当
touchend
事件触发时,我们需要判断当前元素与屏幕之间,悬靠在哪一边更近一些const screenCenterY = Math.round(this.screenWidth / 2);
this.elementX < screenCenterY
- 定义一个动画函数,也就是元素从A点到B点的过渡效果(如果没有这一步,很生硬)
- 定义吸附功能开关
- 当
private animate(targetLeft: number, spd: number): void {
const timer = setInterval(() => {
let step = (targetLeft - this.elementX) / 10;
// 对步长进行二次加工(大于0向上取整,小于0向下取整)
step = step > 0 ? Math.ceil(step) : Math.floor(step);
// 动画原理: 目标位置 = 当前位置 + 步长
const x = this.elementX + step;
this.setElementPosition(x, this.elementY);
// 检测缓动动画有没有停止
if (Math.abs(targetLeft - this.elementX) <= Math.abs(step)) {
// 处理小数赋值
const xt = targetLeft;
this.setElementPosition(xt, this.elementY);
clearInterval(timer);
}
}, spd);
}
复制代码
private adsorbent():void {
// 判断吸附方向
// 屏幕中心点
const screenCenterY = Math.round(this.screenWidth / 2);
// left 最大值
const rightScope = this.screenWidth - this.elementWidth;
// 根据中心点来判断吸附方向
if (this.elementX < screenCenterY) {
this.animate(0 - (this.hideOffset), 10);
} else {
this.animate(rightScope + (this.hideOffset), 10);
}
}
复制代码
定义一个接口interface
,作为Drag
的第二个参数:
interface Options {
autoAdsorbent?: boolean;
}
复制代码
将前面的constructor
方法参数修改为:
constructor(element: HTMLElement, dConfig: Options = {})
复制代码
在Drag
类中添加一个autoAdsorbent
属性,用于判断用户是否开启了吸附功能
export default class Drag{
// 吸附
autoAdsorbent: boolean;
//...
}
复制代码
在watchTouch
方法中,touchend
事件加入
window.addEventListener('touchend', () => {
// ...
if (this.autoAdsorbent) this.adsorbent();
});
复制代码
这里还会有一个小问题,如果用户没有传入dConfig
呢?
我们可以在construction
方法中补充一句,意思是如果dConfig
参数中的autoAdsorbent
不存在,则将它设置为false
constructor(element: HTMLElement, dConfig: Options = {}) {
dConfig = {autoAdsorbent: dConfig.autoAdsorbent || false}
}
复制代码
使用我们的Drag类
第一步,引入我们写好的Drag
import Drag from 'Drag';
复制代码
<div class="root">
<div class="BDrag"></div>
</div>
复制代码
.drag{
width: 50px;
height: 50px;
background-color: rgb(238, 238, 238);
border-radius: 50%;
border: 5px solid rgb(170, 170, 170);
}
复制代码
BetterGraggbleBall
提供了一个类,实例化的第一个参数是一个原生DOM元素
const BDragDom = document.getElementById('BDrag');
const BDrag = new BDrag(BDragDom);
复制代码
插件 GIT地址:github.com/QC2168/bett…
你也可以使用npm直接安装它
npm install better-draggable-ball --save
复制代码
结尾
如果你觉得该文章对你有帮助,欢迎点个赞?和关注。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END