likes
comments
collection
share

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

作者站长头像
站长
· 阅读数 30

前言

因为目前有时间了,所以在整理一下自己这几年写过的一些东西的相关文档,准备把一些东西改一下发出来,有的内容可能并不复杂,甚至有点浅显,但是也是对自己这几年的一些复盘和总结了

如果有需要,转载前请向我确认

需求

曾经我需要去实现一个可以等比拉伸的功能。 而经过考虑,认为这个功能比较通用,所以打算抽离为一个hook,以便复用 且该功能计算较为麻烦,所以梳理成了一篇文档,方便后续的排查和修改

需求拆解&预研

需求拆解

这个需求的基础要求大概就如下图所示

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

我们很容易可以从这个图中得知,这个需求的基础要求的实现有以下几点:

  • 录制区域需要可以自由拖拽
  • 录制区域可以四角拖拽拉伸
  • 录制区域左上角需要显示当前区域的分辨率

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

  • 录制区域需要限制拖拽的边界为屏幕内
  • 分辨率显示在边界情况下会回弹

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

  • 录制区域的拖拽拉伸需要限制最小值

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

  • 录制区域内需要支持传入ReactNode,显示倒计时情况
  • 录制在开始后不允许拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

  • 录制区域的初始化需要根据不同屏幕的不同分辨率适配

小结

所以我们这个需求目前来看的整体的需求细节有以下几点:

  • 录制区域本身的实现,包括阴影部分如何实现,拉伸效果如何处理
  • 录制区域需要可以自由拖拽
  • 录制区域可以四角拖拽拉伸
  • 录制区域左上角需要显示当前区域的分辨率
  • 录制区域需要限制拖拽的边界为屏幕内
  • 分辨率显示在边界情况下会回弹
  • 录制区域的拖拽拉伸需要限制最小值
  • 录制区域内需要支持传入ReactNode,显示倒计时情况
  • 录制在开始后不允许拖拽拉伸
  • 录制区域的初始化需要根据不同屏幕的不同分辨率适配
  • 录制区域的拖拽拉伸需要等比拉伸,目前暂定为16:9

注:注意最后一个需求细节 → 录制区域的拖拽拉伸需要等比拉伸,目前暂定为16:9,这个就是我们整个需求的大坑开始!

方案对比

录制区域阴影方案对比

我们很明显看到这个需求中的录制区域需要我们实现在录制区域内为正常亮度,背景为有透明度的黑色阴影,并且需要支持在拖拽拉伸过程中阴影的变化会跟随录制区域的变化而变化 在这里我想到了两种实现的方案: 方案1:设置全屏背景色为带透明度黑色,录制区域为高亮透明div,覆盖全屏的背景色 方案2:设置录制区域div的阴影为超过屏幕宽高的大小,且颜色为带透明度的黑色 经过慎重考虑:

  • 副作用
  • 后期的维护和修改成本
  • 实现难度
  • 显示时的视觉效果

我最终决定了才用方案2的实现

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

录制区域拖拽拉伸方案对比

说到拖拽,我们脑子里第一时间想到的肯定是 HTML5 里的 Drag 和 Drop,只要设置draggable="true” ,再设置 ondragover 和 ondragstart ,那么我们就可以很愉快地进行拖拽的处理了

而说到拉伸,我们自然也会想到 css 中的 resize 属性,我们很容易去实现一个简单的拉伸效果

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

方案1: draggableresize

所以根据我们脑子里知识点的第一印象,我们能想到的实现方案就是 html 的 draggable 与 css 的resize 属性 但是我们经过实践发现该方案并不能实现我们的需求,下面是这个方案的优缺点 优点:

  • 实现方便,有现成的api
  • 实现逻辑容易维护
  • resize 可以通过设置min-width、min-height、max-width和max-height可以限制拉伸尺寸,不需要自己计算
缺点:
  • draggable 的拖拽动画难以处理
  • draggable 的拖拽的视觉效果与视觉要求不符
  • resize 的拉伸局限性较大,只能一个角度的拖拽拉伸
  • resize 的拉伸没有办法实现等比的拉伸效果
  • resize 允许拉伸的角度的图标样式无法去除

所以最终我们没有使用这个方案实现

方案2: react-draggable 或 react-dnd

在发现方案1不可用的情况下,那么我们自然而然想去寻求是否有现成的库可以实现我们的需求 但是经过调研,现成的组件库也并不能符合我们的需求

  • 新版的 hook 风格的 react-dnd 的 拖拽动画以及样式处理没办法处理到符合我们的需求,甚至其他的需求都有相关的需求点没办法处理而选择弃用该库
  • 两个库都只能实现拖拽的处理效果,拉伸相关仍需自己实现

方案3: 鼠标事件的实现

因为我们已经排除了上面两个方案,那么我能想到的就只有通过鼠标事件来获取到鼠标的落点,对div的位置和大小进行手动的计算了,这样的话,如何实现这个需求就完全取决于我们的代码怎么写了 但是这时候我们又再面临了一个抉择 pointer event 还是 mouse event ? Poiter API 整合了鼠标、触摸和触控笔的输入,使得我们无需对各种类型的事件区分对待,而我们并不能排除是否有用户会在触摸屏上去使用我们的产品,并且Pointer API 的功能支持是比 mouse 强大的,所以经过综合考虑,我决定使用Poiter API 来实现这个需求

draggable 与 resizereact-draggable 或 react-dnd鼠标事件实现
优点1.实现方便,有现成的api 2.实现逻辑容易维护 3.resize 可以通过设置min-width、min-height、max-width和max-height可以限制拉伸尺寸,不需要自己计算实现简单、维护方便1.可控性高,所有的处理都可以自己定义和实现2.方便后续拓展以及优化3.对动画的处理可自定义,支持度高
缺点1.draggable 的拖拽动画难以处理2.draggable 的拖拽的视觉效果与视觉要求不符3.resize 的拉伸局限性较大,只能一个角度的拖拽拉伸4.resize 的拉伸没有办法实现等比的拉伸效果5.resize 允许拉伸的角度的图标样式无法去除1.react-dnd 的 拖拽动画以及样式处理没办法处理到符合我们的需求2.两个库都只能实现拖拽的处理效果,拉伸相关仍需自己实现,并且代码修改麻烦1.实现成本高,计算量大2.具体代码实现较为复杂,理解需要一���的图形抽象能力
选择综合考虑选择了方案3

功能设计&实现

1. 整体功能框架设计 - 通过策略模式来匹配对应事件的触发

方法拆分

因为我们需要做的是四个角的拖拽拉伸和整体的录制区域的拖拽,所以我们可以简单地拆分一下方法

我们很清楚,一个角的拉伸实际上是两个方向的改变,就像下图中的例子(左上角拉伸,并且我们在实现这个需求的时候期望的是一个比较通用和拓展性较高的方法,所以我们也需要预留四个方向的拉伸相关的方法

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

所以我们可以很明确知道我们至少是需要有9个方法来触发我们对应的事件:

drag、left、right、top、bottom、leftTop、leftBottom、rightTop、rightBottom

既然有这么多的方法,那么我们总不能在使用到的时候再去单独调用某个方法吧,这样的话会变得很混乱,并且也不好管理,所以我们这里可以定义一个策略 action 来管理相关的方法

// action 对象下的键是拖拽元素的data-key
const action = {
  drag: dragFun,
  top: topDragFun,
  right: rightDragFun,
  bottom: bottomDragFun,
  left: leftDragFun,
  rightTop: rightTopDragFun,
  rightBottom: rightBottomDragFun,
  leftBottom: leftBottomDragFun,
  leftTop: leftTopDragFun,
};

如何触发对应的方法

上面我们的方法的策略已经定义好了,那么我们要怎么去决定在拖拽过程中去触发哪个方法呢?

那么我们就要去借助鼠标事件来实现了,例如我现在需要触发的是拖拽(drag)的事件,那么我需要做的事情是

在需要触发拖拽事件的div中设置 data-key="drag”,然后在鼠标按下时记录当前的 key是哪个方法

然后在 pointermove 中再去判断触发的是哪个对应的方法,具体的实现我们在后面再去细说

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

2. 返回值设计

基础数值

返回值需要如何设计?

首先我们需要知道的是,我们这个拖拽拉伸区域的位置数据是需要提供给其他的各方面功能需求的小伙伴来使用

例如工具位置计算、分辨率位置计算以及后续需求可能会用到计算相对位置的需求等

也就是说我们至少需要提供这个区域的 x、y、height、width 四个基础信息

在这个基础上,我们发现有一个 api 能获取到的信息可以满足我们大部分的基础数值要求:getBoundingClientRect

说明

getBoundingClientRect 返回值是一个 DOMRect 对象,是包含整个元素的最小矩形(包括 padding和 border-width)

该对象使用 left、top、right、bottom、x、y、width 和 height这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 width和 height以外的属性是相对于视图窗口的左上角来计算的。

而这些参数的意义我们可以通过下图非常直观地看到:

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

兼容不同客户端缩放比的数值

因为我们是属于windows客户端,所以我们可以预料到的事情是,我们这个需求和常规的web开发,会面临很多不同的设备,不同的设备会有不同的屏幕大小以及不同的分辨率,用户也有可能会在小屏幕的时候会设置屏幕显示的显示缩放比例,甚至用户也会设置不同的分辨率显示,所以我们在返回值里面也需要去计算好兼容的分辨率的值返回出去。

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

所以我们在这里需要新增的四个参数是 dpiWidthdpiHeightdpiXdpiY 那这四个参数我们需要如何计算呢?我们可以根据上面拿到的rect对象的值来乘于缩放的比来获取到当前的一个dpi参数

export const calculateDpi = ({ resRect }: { resRect: IDOMRect }) => {
  const screenDisplay = remote.screen.getPrimaryDisplay();
  const { scaleFactor } = screenDisplay;
  return {
    dpiWidth: resRect.width * scaleFactor,
    dpiHeight: resRect.height * scaleFactor,
    dpiX: resRect.x * scaleFactor,
    dpiY: resRect.y * scaleFactor,
  };
};

所以我们最后的返回值设计为

export interface IDOMRect {
  x: number;
  y: number;
  left: number;
  top: number;
  right: number;
  bottom: number;
  width: number;
  height: number;
  dpiWidth: number;
  dpiHeight: number;
  dpiX: number;
  dpiY: number;
}

3. API 设计

显而易见需要的api

在我们确定完返回值之后需要做的就是设计相关的api,那么我们显而易见可以知道必须需要的api有以下几个:

// 可拖拽拉伸区域的div的dom
ref: RefObject<HTMLDivElement>;
// 拖拽拉伸的比例
ratio?: Declare.IRatio;
// 一些基础的信息,我们目前显而易见知道必须要有的是可拖拽区域的最小宽高
defaultInfo?: Declare.IDefaultStyleInfo;
// 是否全屏,这个会涉及到可拖拽拉伸区域的初始化相关的问题
isFullScreen?: boolean;
 
export interface IRatio {
  // 要求缩放的比例,强制要求缩放只能按一定比例进行
  x: number;
  y: number;
}
 
IDefaultStyleInfo {
  minWidth?: number;
  minHeight?: number;
}

基于拓展性 / 边界问题考虑的api

基于拓展性考虑

因为我们的目标是写一个比较通用的拖拽拉伸的hooks,所以我们自然要考虑一些当前需求之外的可能性,以及后续可能会有出现的新需求(例如人像录制)和原需求更改,所以以下的api设计就是为了解决可能未来会存在的问题。

情况1: 现在的默认限制区域为全屏,但是我们很有可能会面临限制区域为自定义的情况,所以我们理所当然需要一个可以让用户自定义限制区域的 api


// 存在限制区域并非窗口的情况,可以传入限制区域的信息,将拖拽和拉伸的区域限制在限制区域内
bound?: IDOMRect;

情况2: 可能会存在用户并不想直接操作在 该 hook 里直接操作dom 的情况,例如考虑到一些例如 窗口移动和变更由外部进行的(electron的单独窗口) 之类的场景

noChangeDomRect?: boolean;

情况3: 各种拖拽过程中的行为事件,例如 拖拽开始、拖拽中、拖拽结束、初始化结束等事件,我们这个hooks的作用是拖拽拉伸,这是一个行为,那么我们肯定需要对其行为过程中触发的必要事件抛出,方便使用的人进行处理。

并且拖拽过程中我们最好可以暴露出当前触发的 action 类型,个人感觉后续需求中必然会有用到的地方

dragStop?: (rect: IDOMRect) => void;
dragStart?: (rect: IDOMRect, dragType: EDragType) => void;
dragging?: (rect: IDOMRect) => void;
afterInit?: (rect: IDOMRect) => void;
 
export enum EDragType {
  drag = 'drag',
  top = 'top',
  right = 'right',
  bottom = 'bottom',
  left = 'left',
  rightTop = 'rightTop',
  rightBottom = 'rightBottom',
  leftBottom = 'leftBottom',
  leftTop = 'leftTop',
}

基于边界问题考虑

因为考虑到之前有了解到另一个小伙伴的需求中可能会出现精度的问题,所以我为了避免

1.视频的裁切出现因为展示区域裁切和视频裁切的精度问题 2.win7系统下,录制忽略某些元素的api无效,导致录制进去拖拽拉伸的边框

这两个问题,将拖拽区域的边框裁切进去的边界问题,所以我选择在这种情况下将录制区域的border放到拖拽拉伸区域的父元素上去,这样就可以让录制区域只有录制区域本身,而border在录制区域之外,不会影响到录制区域本身,此时应该需要提供一个忽略border的功能,所以我们就需要有这样的一个 api

ignoreBorder?: {
    borderSize: number; // border 宽度
}; // 是否需要忽略border(操作父元素偏离了我的设计初衷,所以保留可以不用操作父元素的处理,选择以存在 ignoreBorder 时再操作父元素)

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

完整的api

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

4.关键功能实现

整个功能实现的关键流程图如下

主流程图

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

action 策略中的事件实现及其流程图、示例图

在 action 响应策略的事件中有5个基础事件和4个组合事件,其中基础事件包括了拖拽移动事件、基础的顶部、底部、左边、右边四个触发点的事件,而组合事件则是 左上角、左下角、右上角、右下角四个触发点的拖拽事件

按照我们上面所述,四个组合事件本质上就是顶部、底部、左边、右边四个触发点的事件的组合,但是这是基于不需要等比的拖拽拉伸下的场景。

例如:

我们的顶部、底部、左边、右边四个触发点的事件分别是 topDrag 、 rightDrag 、bottomDrag、leftDrag 四个事件,那么左上角的拖拽拉伸则是同时触发 topDrag、leftDrag 两个事件

但是在我们需要等比拖拽拉伸的这个需求的前提下,我们的组合事件的判断和处理就多了非常非常多的逻辑和边界判断处理,所以也就不能再简单得组合事件以实现两个方向的拖拽拉伸需求了,具体的细节我们会在下面的实现中一一说明。

拖拽移动事件

拖拽移动的事件的实现非常简单,还记得我们在主流程图中有一步是记录两次移动的diff吗? 我们只需要把当前全局保存下的x,y的坐标 加上 保存的diff坐标 计算出新的x,y坐标即可

const dragFun = () => {
  x += diff.x;
  y += diff.y;
};

顶部拖拽拉伸事件

触发顶部拖拽拉伸的事件后的元素高度为 react.bottom - 鼠标落点 y 坐标,而当前 y 坐标则为鼠标落点 y 坐标,具体如下图所示 基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

边界考虑及处理

向上拖拽的过程中我们其实不只是可以拉伸、也可以缩小,也就是往回拖拽,鼠标落点落在原本的拖拽区域内,所以我们这里就必须去做一个限制:当拖拽变更后的高度大于 设置的最小高度,才允许进行变更 这样是为了避免缩小的区域能比限制的最小区域还小 所以最后的代码为

const topDragFun = ({ e }: { e: any }) => {
    // 如果不存在比例
    // 顶部变更距离 = 盒子模型的bottom坐标 - 鼠标y落点
    const topDistance = rect.bottom - e.clientY;
    if (topDistance >= minimumHeight) {
      refHeight = topDistance;
      y = e.clientY;
    }
};

底部拖拽拉伸事件

触发底部拖拽拉伸的事件后的元素高度为 e.clientY - rect.top,而当前 y 坐标则为因为是触发的是底部的拖拽拉伸,上面的y点不需要变更

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

边界考虑及处理

我们这里跟上面一样必须去做一个限制:当拖拽变更后的高度大于 设置的最小高度,才允许进行变更 这样是为了避免缩小的区域能比限制的最小区域还小 所以最后的代码为

const bottomDragFun = ({ e }: { e: any }) => {
  const downDistance = e.clientY - rect.top;
  if (downDistance >= minimumHeight) {
    refHeight = downDistance;
  }
};

左边拖拽拉伸事件

触发左边拖拽拉伸的事件后的元素宽度为 e.clientY - rect.top,而当前 x 坐标则为鼠标的落点x坐标,示例如下图所示 基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

边界考虑及处理

我们这里跟上面一样必须去做一个限制:当拖拽变更后的宽度大于 设置的最小宽度,才允许进行变更 这样是为了避免缩小的区域能比限制的最小区域还小 所以最后的代码为

const leftDragFun = ({ e }: { e: any }) => {
   const leftDistance = rect.right - e.clientX;
   if (leftDistance >= minimumWidth) {
     refWidth = leftDistance;
     x = e.clientX;
   }
 };

右边拖拽拉伸事件

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

边界考虑及处理

我们这里跟上面一样必须去做一个限制:当拖拽变更后的宽度大于 设置的最小宽度,才允许进行变更 这样是为了避免缩小的区域能比限制的最小区域还小 所以最后的代码为

const rightDragFun = ({ e }: { e: any }) => {
  const rightDistance = e.clientX - rect.left;
  if (rightDistance >= minimumWidth) {
    refWidth = rightDistance;
  }
};

基于边界条件考虑的特别注意点

因为以下剩下的计算方法的实现的的宽高很多需要乘于 hooks 外的传入的比例要求,有可能会存在结果存在小数的情况,而又因为精度问题,有可能会出现在拖拽拉伸过程中的细微抖动的情况。 所以这种情况下我们需要将每一次的计算都进行一个向下取证,以获取到整数来进行计算。

为什么是向下取整而不是向上取整? 向上取整有可能会导致最后计算结果超出限制区域

右上角区域拖拽拉伸(复杂计算)

流程图

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

说明图
基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

右下角区域拖拽拉伸 (复杂计算)

流程图

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

左下角区域拖拽拉伸 (复杂计算)

流程图

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

左上角区域拖拽拉伸 (最复杂,因为涉及到x , y两个点的变更)

流程图

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

区分拉伸方向策略实现

这里的判断方式主要是判断在当前这个鼠标落点下向x偏移的位置更多还是向y偏移的位置更多来判断当前的拉伸方向是横向还是纵向

流程图

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

获取真实计算的x、y坐标流程图及其示例图 - 基于拓展性的考虑(拖拽边点不在拖拽作用区域边缘的情况,下一个需求可以预见会存在相关问题)

因为我们的拖拽拉伸的功能是通过计算鼠标落点来实现的,但是我们实际上可能会存在一些业务场景,我们拖拽拉伸的作用点可能并不在需要拖拽来伸区域的边点上,所以会导致我们的计算出错,在开始拖拽前出现先往回跳再开始,具体入下图所示:

1.需求本身要求的拖拽作用点并非拖拽区域边点

2.用户点击时,边点可能作用域较大,会导致轻微跳动

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

所以基于这种情况,我们可以对坐标做重新计算,保证了即使我们的初始位置在需要拖拽拉伸的区域内部也可以正确计算。

解决方案: 我们可以做一个处理,就是判断当前的鼠标落点的位置与边点的距离,若鼠标落点在元素内,则计算取边点位置, 但是这样就会出现无法缩小的问题,所以我们这时候还需要记录一下初始落点,然后再和初始落点进行比对,如果鼠标落点在初始点和边点中,则取边点,否则如果小于边点则取鼠标落点,否则如果大于初始点则取鼠标落点减去初始点与边点的距离,如果都不符合则直接取初始点。

而这个计算过程的处理会因为拖拽的方向不同,计算方式也会有所不同,就像我们上面进行过的拆分一样,拖拽拉伸本质上是可以分为顶部、底部、左边、右边四个触发的拉伸,所以我们这里可以定义一个枚举

// 计算真实点的类型有四种,分别是Y方向向上、向下,X方向向左,向右
export enum EPracticalPointJudgeType {
  up = 'up',
  down = 'down',
  left = 'left',
  right = 'right',
}

于是乎,我们的计算坐标真实落点的处理可以以流程图来这么表示

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

具体的逻辑图解可以表示为 在这里有一点是需要注意的,因为我们是需要动态判断鼠标的真实落点,所以鼠标的事件是一个一直不断变化的过程

图解 - 顶部拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

图解 - 底部拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

图解 - 左边拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

图解 - 右边拖拽拉伸

基于 electron 实现录制屏幕的录制区域的等比拖拽拉伸

转载自:https://juejin.cn/post/7377567987120144435
评论
请登录