阅读 1214

🔥🔥🔥 手写TS组件之《可拖拽吸附的悬浮球组件》

《手写可拖拽吸附的悬浮球组件》有点长,但收获满满

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

前言

目前,前端生态圈中各种各样的UI framework 触目皆是,但有没有发现很多的UI framework 都没有悬浮球这个组件 ,其实这个也不难实现,所以我决定要亲手写一个出来!

实现思路

  1. 获取屏幕的widthheight,即得到悬浮球移动的范围
  2. 利用CSS中的positionabsolute属性,同时搭配lefttop属性来实现元素位置

拖拽事件的过程

选中元素 > 拖动元素 > 拖动结束

开始我们的主题

实现一个类,让用户传入需要拖动的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);
    });
  }
复制代码
  • 定义一个设定元素位置方法,传入xy来设定lefttop
  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
复制代码

结尾

如果你觉得该文章对你有帮助,欢迎点个赞👍和关注。

文章分类
前端
文章标签