E44e67bf777111cb355a598cb5bd504f
浅谈 JSBox 界面编辑器的实现

JSBox 界面编辑器

JSBox 是我的一个个人项目,如果你还不知道是什么:https://sspai.com/post/42361

从第一个版本开始,JSBox 就支持通过 JavaScript 来写界面,类似这样:

$ui.render({
  views: [
    {
      type: "button",
      props: {
        title: "Hey!"
      },
      layout: (make, view) => {
        make.center.equalTo(view.super);
        make.size.equalTo($size(100, 36));
      },
      events: {
        tapped: () => {
          console.log("Hey!");
        }
      }
    }
  ]
});

这几行代码会在屏幕中间创建一个按钮,点击之后控制台会输出 Hey!,很简单。

最近几个月,我花了一点时间来给 JSBox 的界面系统造了个可视化的编辑器,不是什么新鲜玩意,简单说就是可以通过拖拽来构造界面:

你也可以理解成一个移动端简化版本的 Interface Builder,这边文章就是来介绍这个编辑器背后的一些基本原理。

数据存储

这样一个编辑器的本质是什么?是一个 JSON 编辑器罢了(当然,很多的编辑器用 XML 做的数据存储),我们要做的事情只不过是提供一个所见即所得的 JSON 编辑器,这个编辑器可以编辑一些视图的属性,比如说:

  • 长宽和位置
  • 绑定的事件
  • 颜色、字体
  • 特有的一些属性,比如 button 会有 title

这个编辑器能解析包含上述内容的一个 JSON 文件,用可视化的方式进行编辑,编辑完之后能够将上述内容序列化后存起来。

现在有一个问题,JSON 只支持下面几种数据:

  • string.
  • Numeric types.
  • object.
  • array.
  • boolean.
  • null.

假设我们要把一个 UIColor 存在里面,要做什么?序列化呗,通过 JSON 支持的方式表达出来。序列化的思路有两种:

  • 易于人读,例如 "RGBA(255, 255, 255, 1)"
  • 易于机器读,例如:
{
  "$type": "$color",
  "$props": {
    "red": 238,
    "green": 241,
    "blue": 241,
    "alpha": 1
  }
}

当然,这样一个文件我是不希望用户去读写的,他应该用于依赖于界面编辑器去操作他(回想下,除了解决冲突你应该不会去手动改一个 storyboard 文件),甚至不需要了解这背后的存储是什么样的,于是易于机器读是我考虑的。在我的实践里面,就是把所有的对象都封装成了包含 $type$props 的一个 JSON,通过简单的 JSON 解析和写入,完成了所有类型的读写。

视图树

这是整个编辑器中最基本的概念,因为本质上一个 View 就是一棵树:

{
  views: [
    {
      views: [

      ]
    }
  ]
}

他可以嵌套多层下去,所以在这么一个编辑器中会出现很多树的节点访问操作,需要熟练掌握递归和非递归实现。有时候需要访问比较深的一个节点,有时候需要获取一个节点所有的子孩子。

编辑器 UI 部分

这样一个编辑器框架其实是整个项目中最简单的部分。最核心的部分,你需要实现一个拖动手势用于:

  • 移动一个 View 的位置
  • 调整一个 View 的大小

如果不考虑 Auto Layout 的话,非常简单(但 JSBox 支持 Auto Layout,我在后面会解释这个问题),唯一比较麻烦的是实现辅助线效果:

这个效果的实现思路是这样的:

  • 拖动开始之后收集一下目前屏幕上所有的矩形
  • 拖动过程中,检测拖动的方向
  • 根据方向枚举收集到的矩形,如果发现靠近某一条边,就主动靠过去

这是一个最基本的模型,在实际工程中有很多优化的点,比如说根据距离来计算权值,进而决定靠近哪个视图比较好。另外一个问题是,在支持 Auto Layout 的场景下,还需要提醒用户当前的 Frame 和 Auto Layout 出来的结果是否一致,否则要提供警告。

属性编辑器

这个编辑器可以这样编辑每个 View 的属性:

其实实现这些东西并没有多少技术难度,但是却极为繁琐,基本可以认为是体力活,但我们有必要通过简单的协议来实现所有的视图类型,比如说现在 JSBox 支持的 20 几种视图:

假设你已经实现了这些子编辑器:

  • 颜色选择器
  • 字体选择器
  • 日期选择器
  • ...

这些都是体力活,能够编辑单一的属性。现在我们要归类所有的视图支持的属性,有哪些类型,比如:

  • 整数
  • 浮点数
  • 字符串
  • 颜色
  • 字体
  • 日期
  • 各种枚举值
  • ...

把这些收集起来是为了造一个映射表,比如说对于 button 他可能有这样一个表:

top Created with Sketch.