云凤蝶如何打造媲美 sketch 的自由画布

云凤蝶如何打造媲美 sketch 的自由画布

在 Design Tools 中,组件间的对齐与吸附功能是否好用是决定其画布是否可以高效进行产品设计的关键因素。云凤蝶作为一款快速制作高品质中后台应用的 hpaPaaS 平台,同样拥有自由拖拽的可视化画布。那么在云凤蝶的自由画布中,对齐规则是怎样的?实现这些规则的策略是怎样的?规则和策略是否足够完备?最终效果如何?能否媲美 Sketch 等设计软件?这篇文章就来对这些问题进行一一解答。

几个术语

在开始前,先简单介绍几个术语及其在画布中出现时的样式。

构成组件的 6 条线

可以看到,一个组件在画布中可以由 6 条线 (vt / vm / vb | hl / hm / hr) 来表示,组件移动过程中的对齐其实就是组件的 6 条线到其它组件线的集合中寻找临近线,找到后考虑 吸附 + 对齐 的过程。

对齐线
间距线
间距块

以上是组件在移动过程中对齐时出现的几种状态,其中出现 3 个名词,3 种辅助样式:

  • 红色实线代表吸附线
  • 蓝色实线代表距离线
  • 粉色块代表间距块

通用规则

在介绍云凤蝶画布中的对齐规则之前,我们先来直观地看下当下几款不错的设计产品在对齐方面是如何做的。这里主要以 SketchFigma 这两款设计工具为例,其它如 FramerX墨刀等也可自行参考,效果大致相同,只是它们在实现上的优化程度略有高低。

Sketch

Sketch 对齐
Sketch 对齐

在 Sketch 中,以带边框组件的边框中线为基准,在移动过程中不断去找到距离最近的线去对齐。其中

  • 在首次接近某条线时,有一个吸附的过程。如在移动 1px 找到吸附线时,会让组件实际移动 6px,达到吸附效果
  • 当组件在吸附线上时,再次移动时,考虑是否能找到相邻的线,如果可以找到,则移动到下条吸附线上;如果未找到时,则鼠标移动某个距离时,触发移动,有种卡点的感觉,用以辅助对齐;
  • 另外还有比较细节的地方,比如当某一个方向移动吸附到相邻的线后(右图),如果相反方向移动,则非常轻松,而如果继续同一方向继续移动,则需要移动比较大的距离,以此实现了更好的吸附对齐效果。
  • ...

Figma

Figma 对齐
Figma 对齐

在 Figma 中,功能大致相同,与 Sketch 不同的是组件是以外边框为对齐线,且在组件移动过程中没有距离线的出现的。

提前剧透:这方面云凤蝶会有独特的策略。

对比

以上用最简单的组件移动示例分别展示了 Sketch 和 Figma 这两款设计产品在组件移动时的一个不断对齐的过程,核心主要涉及到

  • 对齐(与哪些组件边框线对齐)
  • 吸附(多少距离内时吸附)
  • 距离(与哪些组件边框线考虑展示距离)
  • 画线(画哪几条线、从哪里出线)

至于它们是如何实现,目前是不了解的,未查找到其完整的策略及算法。但是通过细致的使用,我们可以体验到这些产品中触发不同功能的时机、功能的优化以及顺畅度等等。另外,这些设计工具中还有有很多其它的辅助功能,如精心设计的快捷键、间距块的辅助、可视区域内的防干扰对齐等等,这里没有进行展示,下面在介绍云凤蝶自由画布中的辅助功能时会有介绍。

Sketch 和 Figma 在设计领域已经算是比较上乘的产品了,通过了解它们,我们可以看到当前设计产品中画布较好的的使用体验是怎样的,以此借鉴来优化我们的画布体验。

云凤蝶规则

好了,前置内容铺垫结束,到了云凤蝶画布这里。云凤蝶也有可自由拖拽的画布,还有更丰富的资产支持添加更多样的组件。不过,我们除了支持上述 Design Tools 中两条线的预期偏差对齐吸附,还遵循 Ant Design 设计规范中的 Gutter=(8n)px 的原则,所以云凤蝶画布中组件在对齐吸附时,还需要在几个特殊的无形线(8px / 16px / 24px)处做间距卡点吸附

关于布局、网格单位、栅格等规范,可以参考 Ant Design 官网中 布局 一节进行详细了解。

接下来让我们详细来看下云凤蝶中的规则以及当前已经完成的功能和效果。

对齐吸附

对齐吸附

当组件朝某一方向移动时,以上图中横向左移动为例,它的 hl / hm / hr 会不断的去查找与这 3 条线相邻最近的线。其中

  • 当没有找到相邻线时,组件跟随鼠标移动;
  • 当初次找到时,组件便移动一个较大距离吸附过去;
  • 当在吸附线上再次移动时,继续查找相邻线,看是否有下一条吸附线
    • 如果有,则移动到下一条吸附线上;
    • 如果没有,则在鼠标移动一定距离后,组件离开;

以上便是组件对齐吸附的时机及规则,这是可自由拖拽画布中最基础的一个吸附对齐能力。

间距吸附

间距吸附

上文中提到过,在云凤蝶中搭建页面时,组件的摆放要遵循 Ant Design 中 (8n)px 的原则,所以在云凤蝶的自由画布中也要实现组件间距满足要求时的吸附能力。

resize 吸附

resize 吸附

resize 吸附的使用场景是在调整组件宽高时,想要与目标组件的某条边线对齐。由上图也可以看到,对于组件的边框对齐,我们是做了内外的一个适配,站在用户搭建使用的角度来,给出对齐提示。

间距块吸附

间距块吸附

间距块的对齐吸附更多的是给到用户一种提示,即横/纵方向上几个组件间满足同等间距。

吸附剪枝

可视区域变化
可视区域内组件变化

吸附剪枝主要是解画布中组件太多时组件需要对齐的目标组件太多的问题,该规则依赖于“用户只关心当前画布中组件的对齐”这一假设,减少对齐时的干扰。上动图中主要展示跟随可视区域变化,其内组件数量变化时的剪枝。

另一种吸附剪枝是在组件快速移动时,也要避免一直找线,干扰正常移动,如下图中所示。

快速移动时的剪枝

快捷键

这里简单举两个与辅助线相关功能的快捷键 command / option | alt。其实看到后面的实现后就会发现,只要有了这套线的逻辑,想实现类似的功能是比较容易的。

command 键标识组件相对位置
option/alt 键标识任意两个组件相对位置
command 键移动组件时无吸附对齐

画线原则

对齐

一个组件在对齐时,通常情况下出现的线的条数最多是

  • 3 + 3 条 红色的对齐线
  • 2 条 蓝色的距离线(蓝色线永远从移动的组件中线出发)

对于 Label

  • 横向线,label 在上
  • 纵向线,label 在左

规则原因

这里简单解释下,为什么要在画布中实现吸附对齐能力。

首先,可以回归到吸附对齐的作用 -- 弥补用户在精确操作鼠标上的不足。考虑以下场景:当用户试图把一个组件精确摆放到另一个组件旁边时,目标区域只有 1px,当在移动组件时,如何准确摆放到想要的位置?如果没有吸附对齐的能力,要想实现精确摆放是非常难操作的,要么容易重合,要么容易分离。此时,如果有吸附对齐的能力,就有种让“目标区域”变大的效果,当拖拽组件与目标组件两者仅仅相差某个指定范围的像素时,即认为是进入了吸附对齐的范围内,直接把移动组件吸附到目标位置上。与此同时,当组件离开时也会有一个“吸附引力”,即也有一个同等的离开距离,以形成卡点效果。这样,整个对齐操作就比较有控制力了。

同时,这种能力可以让画布中组件的摆放更加高效。依赖于 Ant Design 的 Gutter 设计原则,当我们在画布中进行页面搭建时,可以比较轻松的摆放到更符合设计原则的位置上去,更快速地搭建页面,从而达到提效的目标。

实现策略

当我们总结出自由画布中组件对齐吸附的能力之后,就可以考虑如何实现它。涉及到对齐吸附,一定就涉及到”移动组件“和”目标组件“,下面就从组件移动的角度来大致介绍实现以上规则的一种思路。

存储

在上文中提到,一个组件可以看做是由 6 条线构成的,组件移动过程中的吸附就是这些线之间的对齐关系。所以,首先要做的就是对组件进行线的存储。下面是一种可行的存储数据结构

一条线有自身的 pos、type及其所归属的 Box

// 线的数据结构
interface Line {
  pos: number;
  type: LineType;
  box?: Box | null;
}

一个组件有 6 条线,且冗余存储其 node 及组件实例信息 instance

// 组件的数据结构
interface Box {
  id: string;
  vt: Line;
  vm: Line;
  vb: Line;
  hl: Line;
  hm: Line;
  hr: Line;
  node: HTMLElement;
  instance: ComponentInstance;
}

线的存储可以考虑使用二叉树,普通二叉树可以参考 typescript-collections/src/lib/BSTree.ts,而横纵线可以构造两颗二叉树

// 线的集合
interface Lines {
  vLines: BSTree;
  hLines: BSTree;
}
如果你有其它好的思路,可以一起探讨,比如桶

移动

组件的移动,可以考虑两种实现方式

其中,H5 DnD 不是移动 HTML 元素,而是将数据对象从一个位置移动到另一个位置。要移动 HTML 元素,必须使用 MouseEvents:

  • mousedown: 选中元素
  • mousemove: 移动元素
  • mouseup: 释放元素
至于为什么会有对 DnD 的误解,可以参考 《Drag & Drop vs. MouseEvents - A misunderstanding

为了实现更好地拖拽移动效果,我们采用 MouseEvents 的实现方式。决定了如何拖拽以后,接下来就来考虑,如何实现组件的移动。下图展示了鼠标选中组件时的移动位置关系,其中, A 为鼠标 mousedown 时的选中点,B 为鼠标移动后的 mouseup 的释放点,moveX 则表示鼠标的移动距离,也即组件即将横向移动的距离。

鼠标在组件上的移动

找线

线的查找与线的存储数据结构密切相关。在存储时,我们采用的是二叉树,所以找线的过程就变成了在二叉树中查找与某个位置具有某种大小关系的线,具体的找线算法这里不表,只能说根据业务规则尽量找到最优的算法。找线逻辑是整个策略中的关键一环,通过合理的策略找出离当前组件最近的线,只有这样才能保证后面的吸附是正确的。

找线策略在上文对齐吸附中有提到,这里不再赘述。需要说明的是,“离当前组件最近”是需要重点考虑的,因为在横/纵向查找的过程中,3 条线都有可能遇到最近的线,而且还有 Ant Design Gutter 8xpx 原则在这里,所以这里需要综合考虑。

吸附

当通过前置步骤找到了要吸附的线,我们就可以考虑是否将组件进行移动吸附了,这里主要还是要结合找到的”最近的线”。不过需要注意的是,我们不能在找到线时就移动过去,是因为组件移动距离与鼠标移动偏差不一致时可能会带来的抖动。

此外,对于组件在吸附状态下的移动,也是需要做二次找线的,因为当此时可能直接离开当前的引力区域,也可能是移动到下一条吸附线上。

吸附同样是非常关键的一环,处理不好将直接影响到画布的使用体验,处理了上述两方面,理论上就达到一个不错的效果。

分类

在文章开头,给出了几种不同辅助功能的线,在目前的对齐策略中,有以下几种

enum PairType {
  distance = 'distance', // 距离线
  alignment = 'alignment', // 对齐线
  spacing = 'spacing', // 间距线
  area = 'area', // 间距块
}

无论是对齐线、距离线、间距线还是间距块,都是通过二叉树中一对 Line 构成的 LinePair 画出的

export interface LinePair {
  source: Line; // 需要对齐的边
  target: Line; // 可以被对齐的边
  type: PairType; // 这对线的关系
  delta: number; // 这对线的偏差
  duplicate?: Line[]; // 目标对齐线可能包含多条位置相同的边框线或者中间线
}

根据以上数据结构,我们可以对找到的线进行一个分类,标识出一对线的关系及偏差。

画线

在以上步骤中拿到分好类的线,就可以进行画线操作了。通过线的分类及规范的数据结构,我们可以保证各类型线的画线逻辑的纯粹性。然后针对不同类型的线,在画布中用不同的层来实时展示即可。

至于如何实现更高效的画线以及用何种技术手段来实现,则可以仁者见仁了。

总结

本文主要总结了当前云凤蝶自由画布中支持的对齐、吸附、间距、resize、画线等辅助功能以及实现这些功能的整体规则和策略,这些都是自由画布中最基础的能力,也是推导出画布中吸附对齐涉及取值的理论基础。


梳理下来可以看到,要想在自由画布中实现对齐的能力并不难,麻烦的是如何将规则和策略理清楚、提炼和沉淀,从全局的视角和可扩展的角度来实现。最近云凤蝶自由画布中对齐等辅助能力已经根据这些原则进行了 2.0 的升级,从整体使用体验上,组件摆放这件事情已经变得比较容易。

未来

这里罗列几点当前云凤蝶画布中相较于 Design Tools 中还可以优化的地方:

  • 标尺的对齐与吸附(标尺可以看做是只有一条 h / v 方向 height / width 为画布大小的线)
  • 间距块的查找纳入对齐吸附的整体查找策略中(当前是与对齐线、辅助线的查找分开的)
  • 无极缩放,在更小的区域也能优雅地实现想要的操作
  • 揣测用户意图,更“智能”快速的摆放策略

看到上述几点,感觉也不是很难啊,为啥还没做?

是的,主要是因为...

等你来,一起做!

参考


未来已来,时不我待!

云凤蝶招聘前端、Java、PD、设计岗位,未来等你共创!

如果你感兴趣,欢迎联系 chenyu@antfin.com 或 shuai.shao@antfin.com

编辑于 2019-11-20

文章被以下专栏收录