《手写可拖拽吸附的悬浮球组件》有点长,但收获满满
这是我参与更文挑战的第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
结尾
如果你觉得该文章对你有帮助,欢迎点个赞👍和关注。