Sketch 插件导出切片

Sketch 作为流行的 UI 设计软件,除了设计之外,还承担了设计与开发之间沟通的桥梁作用。通过 Sketch 导出的在线标注能够节省很多沟通的成本。除了标注之外还有个比较重要的功能就是切图的导出。Sketch 中如果要导出一张切图,需要将其标记为切片(Slice)。在 Sketch 中切片的标记是多种多样的,针对不同的切片标记插件需要处理的逻辑也有细微的差别。下面我们就来看看不同的切片操作在插件中应该如何导出吧。

注: Ctrl + Shift + K 可以在 Sketch 中调出插件脚本运行的 Playground,可以方便的调试代码。

图层及编组切片

这种是最普通的方式了,当我们想要将某个图层导出成图片的时候,就会为该图层设置导出选项。导出选项中我们可以设置多种导出尺寸和格式,在左侧图层面板中设置了导出选项的图层会增加类似刻刀的图标标记。

Sketch API 提供了 sketch.export() 方法帮助我们在切片中导出切片图层。设置了导出选项的图层,图层属性会带有 exportFormats 属性,我们可以根据它判断是否是需要导出切图的图层。

const sketch = require("sketch/dom");
const artboard = sketch.getDocuments()[0].pages[0].layers[0];

const exportLayers = artboard.layers.filter(layer => layer.exportFormats.length);
exportLayers.forEach(layer => sketch.export(layer, {
  scales: 1, 
  formats: 'svg', 
  output: `~/Desktop/Sketch-Export-Demo` 
}));

编组带切片图层

除了为图层设置导出项之外,我们还可以专门添加切片图层来导出图片。切片图层会将所有与该切片图层同级的图层叠加后产生的图片进行导出。同时它不依赖素材图层,在尺寸设置上更加自由。

理论上这种情况使用 sketch.export() 方法也是没有问题的。不过这种情况下会像示例图一样,切片导出会把父级的白色背景色也导出出来,然而大部分情况下我们需要的其实只是透明图层。

这时候要导出不带背景色的图片的话,需要将切片图层和同级元素都放在一个编组里,这时候切片图层导出会多出一个 Export group contents only 的选项,中文译为仅导出编组内内容。当它被选中后,由于父级背景色不属于编组就会被排除了。

所以我们需要使用代码实现编组勾选配置导出三件事情。我们使用 sketch.Group 实例化了一个与画板等大的编组,并将同级的图层复制了一份放到了新创建的编组里。这里需要注意的是图层的坐标是相对的,在编组内的坐标会基于编组图层本身进行偏移。所以我们需要基于新的编组图层位置重新计算复制图层的位置,嗯,小学减法操练起来~

至于勾选配置这件事,我似乎没有找到 JavaScript API 能干这个事情,只能通过 sketchObject 属性获取到 OC 对象调用 Native 的方法设置了。至于为什么 setLayerOptions() 的参数是 2,我要说是因为要设置的是第 2 个选项你信么(掩面…

const sketch = require('sketch/dom');
const artboard = sketch.getDocuments()[0].pages[0].layers[0];

const duplicateLayers = artboard.layers.map(layer => {
  const copy = layer.duplicate();
  copy.frame = new sketch.Rectangle(
    layer.frame.x - artboard.frame.x, 
    layer.frame.y - artboard.frame.y, 
    layer.frame.width, 
    layer.frame.height
  );
  return copy;
});
const group = new sketch.Group({
  name: '切片编组',
  parent: artboard,
  frame: artboard.frame,
  layers: duplicateLayers
});

const slice = group.layers.find(layer => layer.type === sketch.Types.Slice);
slice.sketchObject.exportOptions().setLayerOptions(2);

sketch.export(slice, {scales: 2, formats: 'png', output: '~/Desktop/Sketch-Export-Demo'});
group.remove();

控件内切片图层

上面说的都是画板本身的图层设置成切片的配置。除了上文说到的元素之外,在 Sketch 中还存在着控件(Symbol)元素。它可以类比为代码中的基类,每一个控件可以实例化出一个控件实例,实现控件一处修改,处处生效的特性,让 UI 设计更加的工程化。控件还有类似代码中变量的覆盖层概念,支持将控件中的某个元素配置化,每个实例配置不同的覆盖层满足不同控件实例求同存异的需求。

正常情况下插件只能拿到画板下的图层,也就是只能拿到最终的控件实例。如果在控件中包含切片的话(如上图),普通方法是无法获取的。这时候就需要使用上图的“解绑”这个功能,对应到代码的话就是 layer.detach() 方法。点击解绑之后,控件实例就会转换成普通的编组图层,里面会包含一份控件所有元素的复制。这样我们就能按照之前的流程进行处理了。

const sketch = require('sketch/dom');
const artboard = sketch.getDocuments()[0].pages[0].layers[0];

artboard.layers.forEach(layer => {
  if(layer.type !== sketch.Types.SymbolInstance) {
    return;
  }

  //为了不影响原图层,使用 duplicate() 方法复制一份图层出来再使用 detach() 进行解绑
  const symbolGroup = layer.duplicate().detach({recursively: false});
  
  const exportLayers = symbolGroup.layers.filter(layer => layer.exportFormats.length);
  exportLayers.forEach(layer => sketch.export(layer, {
    scales: 1, 
    formats: 'svg', 
    output: '~/Desktop/Sketch-Export-Demo'
  }));
  
  //最后操作完成后将复制图层删除
  symbolGroup.remove();
});

控件画板为切片

在之后的使用中,我们发现也会存在直接给控件画板设置成切片导出的操作。这种情况下我们直接对控件实例进行解绑会发现解绑后的编组并没有标记成切片,甚至还会出现其他的一些情况。从下图可以看到不仅解绑后的编组丢失了切片标记,控件尺寸也发生了变化。原始是 32×32 的控件,经过解绑之后尺寸变成了 26×26,周边填充的留白消失了。这是因为看到的留白本质是画板尺寸撑起来的,解绑相当于对控件内的所有图层的拷贝,然而并不包括画板。所以画板的切片属性,以及因为画板尺寸带来的留白等特性都丢失了。

解决的办法也很简单,我们可以通过 sketch.getSymbolMasterWithID() 方法获取到控件,判断控件本身有切片标记的话特殊处理一下。刚才我们说了,控件解绑后肯定是没办法获取到尺寸了,所以我们需要换个思路。由于整个控件是切片,所以我们其实是不需要去观察控件内部是否存在切片导出图层,也就不需要像上面那么复杂去做解绑的操作。通过额外增加一个切片图层,补充上控件丢失的切片标记信息。剩下来的事情其实就和前文“编组带切片图层”一节是一样的了。

const sketch = require('sketch/dom');
const document = sketch.getDocuments()[0];
const artboard = document.pages[0].layers[0];

artboard.layers.forEach(layer => {
  if(layer.type !== sketch.Types.SymbolInstance) {
    return;
  }

  const master = document.getSymbolMasterWithID(layer.symbolId);
  if(!master?.exportFormats.length) {
    return;
  }

  const instance = layer.duplicate();
  instance.frame = new sketch.Rectangle(0, 0, layer.frame.width, layer.frame.height);
  const slice = new sketch.Slice({
    name: layer.name + '_Slice', 
    frame: new sketch.Rectangle(0, 0, layer.frame.width, layer.frame.height),
    exportFormats: [
      {size: '1x', fileFormat: 'svg'}
    ]
  });
  slice.sketchObject.exportOptions().setLayerOptions(2);
  const group = new Group({
    name: layer.name + '_Group',
    parent: layer.parent,
    frame: layer.frame,
    layers: [
      slice,
      instance
    ]
  });

  sketch.export(slice, {scales: '1', formats: 'svg'});
  group.remove();
});

这里我们分别创建了当前图层的复制层和与控件等大的切片图层,最后在当前图层的位置实例化了一个编组巧妙的将前两者包裹住再导出切片。可能会有同学疑问,既然已经不需要解绑来获取控件内部的切片,那为什么不在判断该控件实例需要切片导出的时候直接设置该控件实例的导出项呢?

感兴趣的同学可以试试,你会发现在这种情况下控件实例的导出项也会和我们最开始说的一样丢失画板的留白的。可以想象到 Sketch 内部本身的导出逻辑可能和我们解绑操作差不多。通过等大的切片图层,我们能很好的将控件画板带来的留白保存下来。而冗余的再套了一层编组,则是为了解决前文说的切片导出会附带同层级的背景问题。

后记

上述列出来的切片情况基本包含了大部分设计师的切片导出习惯,控件切片在进行解绑后可以回归到图层、编组、切片的逻辑中。按照上述逻辑递归所有图层可以完成所有切片图层的导出。为了更好的帮助大家理解,以上代码都是真实代码,可以直接在 Sketch 的代码编辑器中运行。

通过以上的例子可以看到,Sketch API 操作真的就是 JavaScript 语法,一点 OC 的东西都没有,对前端工程师非常友好。不过 sketch.export() 方法目前封装的不是非常的完美,在导出组件库中的控件时会存在导出图片空白的情况。这时候只能使用 OC 的方法进行导出了,希望能在之后的版本中修复该问题。

function nativeExport(layer, {format, scale, filename}) {
  const output = MSExportRequest.exportRequestsFromExportableLayer(layer.sketchObject).firstObject();
  output.format = format;
  output.scale = scale;
  return context.document.saveExportRequest_toFile(output, options.filePath);
}

参考资料:

  1. 《Set “Export group contents only”》
  2. 《手把手教你写一个批量切图sketch插件》