美文网首页
通过Sketch生成整张Android代码

通过Sketch生成整张Android代码

作者: 硅谷干货 | 来源:发表于2023-03-28 20:21 被阅读0次

前言

sketch

sketch是一款用来制作矢量绘图的软件,矢量绘图也是进行网页,图标以及界面设计的最好方式。但除了矢量编辑的功能之外,sketch同样添加了一些基本的位图工具,比如模糊和色彩校正。是Mac系统才有的软件,可以理解为精简版的PS ,比PS更适合UI设计。该软件的特点是容易理解,上手简单,对于有设计经验的设计师来说,入门门槛很低。

sketch是用Objective-C构建的,是一套原生Objective-C开发的软件。Objective-C类通过Bridge (CocoaScript/mocha) 提供Javascript API调用。

sketch插件

sketch插件是按照特定方式管理的一个文件夹,是一个或多个scripts的集合,每个script定义一个或多个commands。sketch插件是以 .sketchplugin 扩展名的文件夹,包含文件和子文件夹。

sketch插件主要使用Javascript 语言编写,支持ES6语法,但运行环境既不是浏览器也不是Nodejs,而是Hybrid SketchAPI for macOS Native运行环境。

用JavaScript 编写一个sketch插件。利用ES6,访问macOS框架并使用sketch API,无需学习Objective-C或Swift。所有macOS 框架和内部sketch API都由CocoaScript提供给 JavaScript。

开发环境

  • VSCode
  • XCode
  • Proxyman
  • Skpm
  • Sketch DevTools

开发语言

  • JavaScritp
  • TypeScript
  • CocoaScript
  • Objective-C
  • React
  • NodeJS

插件术语

  • Plugin(插件): 一组 Scripts、Commands和其他资源组合在一起作为一个独立单元
  • Plugin Bundle(插件包): 磁盘上的文件夹,其中包含组成 Plugin的文件
  • Action(行为): 用户所做的事情(选择菜单或更改文档)触发Command
  • Command(命令): 一个插件可以定义多个命令; 通常每一个都与不同的菜单或键盘快捷键相关联,并导致执行不同的Handler程序
  • Handler(操作): 执行一些代码来实现Command的函数
  • Script(脚本): 一个 JavaScript文件, 包含一个或多个用来实现一个或多个Commands的Handlers

插件位置

sketch中插件的位置如下图所示:

[图片上传失败...(image-72e1c0-1680092605582)]

安装的sketch插件位于下面目录:

/Users/用户名/Library/Application Support/com.bohemiancoding.sketch3/Plugins

运行脚本

熟悉 sketch 的各元素 API 和属性使我们进行 sketch 开发插件开发的前置条件,市场上提供的 sketch 开发调试工具极少,这里推荐一款最先进的调试 sketch 调试工具sketchtool,最近两年没有在更新了,如果您使用的苹果 M 芯片,安装时可能直接提示不可用,如果想使用我们只能等等官网更新了。

var sketch = require("sketch");

var document = sketch.getSelectedDocument();
const layers = document.pages[0].layers;
layers.forEach((item) => {
  if (item.type === "Artboard") {
    console.log(item.name, item.type);
  }
});
console.log(layers);

我们可以通过sketch内置脚本编辑器编写一个简单的脚本,脚本编辑器提供对 JavaScript API 和内部 API 的完整访问。

[图片上传失败...(image-86d3c3-1680092605582)]

如果需要打印更多参数可以參考 官网 sketch api,拷贝到编辑区然后点击运行即可。

脚手架配置

我们的项目使用 skpm 搭建的,是基于 webpack 脚手架,样式加载器等配置需要找到我们项目根目录下的 webpack.skpm.config.js 文件。

module.exports = function (config, isPluginCommand) {
    if (!isPluginCommand) return;
    let debug = !!process.env.DEBUG;
    if (!debug) clearMapFilesForProduction('sketch-tinnove.sketchplugin/Contents');
    config.mode = debug ? 'development' : 'production';
    config.entry = {
        mark: './src/index.ts',
    };
    config.module = {
        rules: [{
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/
        }]
    };
    config.resolve = {
        extensions: ['.tsx', '.ts', '.js']
    }
}

脚本命令行和菜单配置

做过 Android 原生开发都知道,App 应用权限和 4 大组件等注册需要找到模块/scr/main 目录下面的一个清单文件 AndroidManifest.xml,其实插件开发也有点类似,在我们项目根目录下面有一个 menifest.json 文件。

文件 commands 节点下面配置命令行如下:

"handlers": {
  "run": "onRun",
  "actions": {
    "StartUp": "onStartUp",
    "SelectionChanged": "onSelectionChanged"
  }
}


{
  "name": "选择设计稿",
  "identifier": "commandSelectionChanged",
  "shortcut": "",
  "handler": "commandSelectionChanged",
  "script": "mark_bundle.js",
  "handlers": {
    "run": "onRun",
    "actions": {
      "SelectionChanged": "onSelectionChanged"
    }
  }
}

"handlers": {
  "run": "onRun",
  "setUp": "commandDocuments",
  "actions": {
    "SelectionChanged.finish": "commandChanged"
  }
}

然后在用到的地方引入如下,import { onSelectionChanged } from "./meaxure/panels/layersPanel";,就可以在函数调用处触发。

文件 menu 节点下面配置菜单如下:

"menu": {
  "isRoot": false,
  "items": [
    "commandDocuments",
    "commandComponents",
    "-",
    "commandExport"
  ],
  "title": "UI组件库"
}

设计稿源文件上传

先说下我们设计方案,大致分如下三个步骤:

    1. Sketch 源文件上传(服务端处理源文件上传 + 转 ZIP 文件 + 解压文件)
    1. 预览图逐个图片文件上传(由服务端暂存本地后上传文件夹)
    1. 切图 逐个图片文件上传(由服务端暂存本地后上传文件夹)

解压 sketch 源文件,我们不难发现,其中有我们渲染前端需要的元数据,比如:document.json、images、meta.json、pages、previews、user.json 等文件。保存源文件到云端资产库中存留,也方便供其他设计人员协作设计。

sketch 插件开发大概有如下三种方式:

  • 纯使用 CocoaScript 脚本进行开发
  • 通过 Javascript + CocoaScript 的混合开发模式
  • 通过 AppKit + Objective-C 进行开发

[图片上传失败...(image-d1f6ca-1680092605582)]

官方推荐方案对于前端会更友好,原生开发方案插件性能会更好,两种都不是很简单,有一定的学习成本,前端同学开发的话建议使用官方推荐方案。即混合开发模式进行sketch插件开发,具体流程参考下图:

[图片上传失败...(image-6cde74-1680092605582)]

Mocha桥接

  • UI层:可以通过webview内嵌实现,可以使用各种前端开发框架,比如React或者Vue等。UI层将用户的操作反馈传递给逻辑层,使其调用sketch API更新Layers。
  • 逻辑层:负责调用sketch API,显然不在WebView中,因此需要通过CocoaScript Bridge进行通信,逻辑层将从服务器获取到的数据传递给UI层展示。

[图片上传失败...(image-35be7-1680092605582)]

获取文件二进制数据

苹果原生示例代码如下:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (IBAction)clickTest:(id)sender {

    NSString *path = @"/Users/kotei/Documents/WorkNote/week05/123.txt";
    NSData *data = [NSData  dataWithContentsOfURL:[NSURL fileURLWithPath: path]];
    NSData *data2 = [[NSData data] initWithContentsOfFile:@"/Users/kotei/Documents/WorkNote/week05/模型库.sketch"];
    NSLog(@"data - %@ - %@", data, data2);


    // 测试方法,可以改动
    NSString *filename = [path lastPathComponent];
    [self testUploadWithFilename:filename designId:@"1623516470227025922"];
}


#pragma -- 非代理session请求方法
- (void)testUploadWithFilename:(NSString *)filename designId:(NSString *)designId{
    NSString *baseURL = @"http://ui-compenont-api.ac.koteicloud.com";
    NSString *pathStr  = @"/Users/kotei/Documents/WorkNote/week05/%E6%A8%A1%E5%9E%8B%E5%BA%93.sketch";
    NSString *unencodeStr = [pathStr stringByRemovingPercentEncoding];
    NSURL *unencodeUrl = [NSURL fileURLWithPath:unencodeStr];
    NSData *fileData = [NSData dataWithContentsOfURL:unencodeUrl];
    NSString *requestStr = [NSString stringWithFormat:@"%@/asset/design/details/uploadSketch4?fileName=%@&designId=%@",baseURL, filename ,designId];
    NSString *formatStr = [requestStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"requestxxx: %@ - %@",formatStr, requestStr);

    NSURL *requestUrl = [NSURL URLWithString:formatStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl];
    [request setHTTPMethod:@"POST"];
    [request setTimeoutInterval:120];
    [request setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Bearer 252fff80-ed0b-4534-9671-7bd9088afd1a" forHTTPHeaderField:@"Authorization"];

    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionUploadTask *sessionTask = [session uploadTaskWithRequest:request fromData:fileData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error == nil) {
            NSLog(@"response: %@", response);
            NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
            NSLog(@"jsonDict:%@", jsonDict);
        };
    }];
    [sessionTask resume];
}

#pragma -- 代理session请求方法
- (void)testUpload2WithFilename:(NSString *)filename designId:(NSString *)designId{
    NSString *baseURL = @"http://ui-compenont-api.ac.koteicloud.com";
    NSString *pathStr  = @"/Users/kotei/Documents/WorkNote/week05/%E6%A8%A1%E5%9E%8B%E5%BA%93.sketch";
    NSString *unencodeStr = [pathStr stringByRemovingPercentEncoding];
    NSURL *unencodeUrl = [NSURL fileURLWithPath:unencodeStr];
    NSData *fileData = [NSData dataWithContentsOfURL:unencodeUrl];
    NSString *requestStr = [NSString stringWithFormat:@"%@/asset/design/details/uploadSketch4?fileName=%@&designId=%@",baseURL, filename ,designId];
    NSString *formatStr = [requestStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"requestxxx: %@ - %@",formatStr, requestStr);

    NSURL *requestUrl = [NSURL URLWithString:formatStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl];
    [request setHTTPMethod:@"POST"];
    [request setTimeoutInterval:120];
    [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Bearer 252fff80-ed0b-4534-9671-7bd9088afd1a" forHTTPHeaderField:@"Authorization"];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    NSURLSessionUploadTask *sessionTask = [session uploadTaskWithRequest:request fromData:fileData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error == nil) {
            NSLog(@"response: %@", response);
            NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
            NSLog(@"jsonDict:%@", jsonDict);
        };
    }];
    [sessionTask resume];
}

#pragma -- 非代理connection请求方法
- (void)testUpload3WithFilename:(NSString *)filename designId:(NSString *)designId{
    NSString *baseURL = @"http://ui-compenont-api.ac.koteicloud.com";
    NSString *pathStr  = @"/Users/kotei/Documents/WorkNote/week05/%E6%A8%A1%E5%9E%8B%E5%BA%93.sketch";
    NSString *unencodeStr = [pathStr stringByRemovingPercentEncoding];
    NSURL *unencodeUrl = [NSURL fileURLWithPath:unencodeStr];
    NSData *fileData = [NSData dataWithContentsOfURL:unencodeUrl];
    NSString *requestStr = [NSString stringWithFormat:@"%@/asset/design/details/uploadSketch4?fileName=%@&designId=%@",baseURL, filename ,designId];
    NSString *formatStr = [requestStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"requestxxx: %@ - %@",formatStr, requestStr);

    NSURL *requestUrl = [NSURL URLWithString:formatStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl];
    [request setHTTPMethod:@"POST"];
    [request setTimeoutInterval:120];
    [request setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Bearer 252fff80-ed0b-4534-9671-7bd9088afd1a" forHTTPHeaderField:@"Authorization"];

    [request setHTTPBody:fileData];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
            NSLog(@"%@" , [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        }];
}

- (IBAction)testSketch2:(id)sender {
    NSString *filename = @"模型库3.sketch";
    NSString *pathStr = @"/Users/kotei/Documents/WorkNote/week05/%E6%A8%A1%E5%9E%8B%E5%BA%93.sketch";
    NSString *unencodeUrl = [pathStr stringByRemovingPercentEncoding];
    NSData *fileData = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:unencodeUrl]];

    NSURL *url = [NSURL URLWithString:@"http://10.10.12.175:8080/design/details/uploadSketch2"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    [request setValue:@"multipart/form-data" forHTTPHeaderField:@"Content-Type"];
    [request setValue:@"Bearer 57015836-7216-4dbb-a08f-d8e15802199c" forHTTPHeaderField:@"Authorization"];
    NSString *componentStr = [NSString stringWithFormat:@"fileName=%@", filename];
    request.HTTPBody = [componentStr dataUsingEncoding:NSUTF8StringEncoding];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        if(connectionError == nil){
            NSLog(@"response: %@", response);
            NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
            NSLog(@"jsonDict:%@", jsonDict);
        }
    }];
}

- (void)getUTF8StrWithFilename:(NSString *)filename{
    NSData *data = [NSData data];
    NSString *encodeStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"encodeStr:%@", encodeStr);
}

#pragma -- 请求代理过滤https
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
    if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]){
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        if(completionHandler)
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
    }
}

@end

二进制转 base64

如果直接传给 webview 端会读取错误,所以我们在插件端获取二进制然后原生方法转成 base64 字符串。

// 获取本地Buffer传给JS
function getBase64JSBufferStr() {
  const document = sketch.Document.getSelectedDocument();
  const pathStr = NSString.stringWithString(document.path);
  const unencodeUrl = pathStr.stringByRemovingPercentEncoding();
  const fileUrl = NSURL.fileURLWithPath(unencodeUrl);
  const fileData = NSData.dataWithContentsOfURL(fileUrl);
  const base64Data = fileData.base64EncodedDataWithOptions(0);
  const base64OCStr = NSString.alloc().initWithData_encoding(base64Data, 4);
  const base64JSStr = Buffer.from(base64OCStr).toString();
  const fileName = Buffer.from(unencodeUrl.lastPathComponent()).toString();
  return {
    fileName,
    base64Str: base64JSStr,
  };
}

// 获取本地OC二进制文件数据
function getOCFileData() {
  const document = sketch.Document.getSelectedDocument();
  const pathStr = NSString.stringWithString(document.path);
  const unencodeUrl = pathStr.stringByRemovingPercentEncoding();
  const fileUrl = NSURL.fileURLWithPath(unencodeUrl);
  const fileData = NSData.dataWithContentsOfURL(fileUrl);
  return fileData;
}

// 获取Buffer
function getOCFileDataWithBuffer() {
  const fileData = getOCFileData();
  const bytesBuf = Buffer.from(fileData) as Buffer;
  return bytesBuf;
}

webview 端获取 file

// base64字符串转换为 File 对象
function base64ToFile(base64Str, fileName) {
  const rawData = window.atob(base64Str);
  const outputArr = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArr[i] = rawData.charCodeAt(i);
  }
  return new File([outputArr], fileName, {
    type: "text/plain;charset=US-ASCII",
  });
}

然后就是正常的 ajax,表单方式上传到对应的服务端,就可以完成一次完整的源文件上传。

预览图上传

解压sketch源文件,我们发现文件夹中并没有预览图文件,这就意味着我们需要在web端或者在插件端来实现解析或者上传,但是我们经过反复实验在web端解析还原的效果并不好,最终选择在插件端解析上传的方案来实现。

显示效果图如下:

[图片上传失败...(image-e8a27b-1680092605582)]

生成预览图的关键代码如下:

// 画板编码为base64字符串
function createImageBase64WithLayer(
  layer: Layer,
  format: string = "png",
  scale: number = 1
) {
  return exportImageToBuffer(layer, {
    format: format,
    scale: scale,
    prefix: "",
    suffix: "",
  }).toString("base64");
}

切图上传

跟其他设计协作平台一样,切图一般是设计人员上传提供给开发人员使用的,为了达到最佳的视觉效果,这里我们采用插件端上传的方案。

显示效果图如下:

[图片上传失败...(image-bdb93-1680092605582)]

生成预览图的关键代码如下:

// 准备解析的数据
function prepareExportData(): [ExportData, { [key: string]: Artboard }] {
  let allArtboards: { [key: string]: Artboard } = {};
  let data = <ExportData>{
    selection: [],
    pages: [],
    exportOption: true,
    reverse: false,
  };

  let artboardSet = new Set<string>();
  if (context.selection.length > 0) {
    for (let layer of context.selection.layers) {
      if (layer.type == sketch.Types.Artboard) {
        artboardSet.add(layer.id);
        continue;
      }
      let artboard = layer.getParentArtboard();
      if (artboard) artboardSet.add(artboard.id);
    }
    data.selection = Array.from(artboardSet);
  }

  for (let page of context.document.pages) {
    let pageData = <PageInfo>{};
    let artboards = page.layers.filter(
      (p) => p.type == sketch.Types.Artboard || artboardSet.has(p.id)
    ) as Artboard[];
    pageData.name = page.name;
    pageData.objectID = page.id;
    pageData.artboards = [];
    let layerOrder = 0;
    for (let artboard of artboards) {
      layerOrder++;
      let artboardData = <ArtboardInfo>{};
      artboardData.name = artboard.name;
      artboardData.objectID = artboard.id;
      allArtboards[artboardData.objectID] = artboard;
      artboardData.layerOrder = layerOrder;
      artboardData.x1 = artboard.frame.x as number;
      artboardData.y1 = artboard.frame.y;
      artboardData.x2 = artboardData.x1 + artboard.frame.width;
      artboardData.y2 = artboardData.y1 + artboard.frame.height;
      artboardData.row = undefined;
      artboardData.column = undefined;
      pageData.artboards.push(artboardData);
    }
    data.pages.push(pageData);
  }
  return [data, allArtboards];
}

// 获取画板一维数组
function createSingleArtboardInfoList(
  data: ExportData,
  allArtboards: { [key: string]: Artboard }
) {
  const exportArtboards: {
    artboard: Artboard;
    children: (Layer | LayerPlaceholder)[];
  }[] = [];
  let layersCount = 0;
  for (let page of data.pages) {
    for (let info of page.artboards) {
      const artboard = allArtboards[info.objectID];
      const [children, count] = getChildrenForExport(artboard);
      layersCount += count;
      exportArtboards.push({ artboard: artboard, children: children });
    }
  }
  return exportArtboards;
}

// 生成画板信息列表
function convertWithArtboardInfoList(
  data: ExportData,
  allArtboards: { [key: string]: Artboard }
) {
  const artboardInfoList: {
    base64Str: string;
    fileName: string;
    children: (Layer | LayerPlaceholder)[];
  }[] = [];
  const allArtboardList = createSingleArtboardInfoList(data, allArtboards);
  for (let artboardItem of allArtboardList) {
    const artboard = artboardItem.artboard;
    const slice = getImageSlice(artboard);
    const format = slice ? slice.format : "png";
    const scale = slice ? slice.scale : 1;
    const artboardBase64Str = createImageBase64WithLayer(
      artboard,
      format,
      scale
    );
    artboardInfoList.push({
      base64Str: artboardBase64Str,
      fileName: `${artboard.id}.${format}`,
      children: artboardItem.children,
    });
  }
  return artboardInfoList;
}

web端获取sketch元数据

上传完之后,来到我们的web端,打开对应的上传过的设计稿,渲染之前是获取sketch元数据,这里我们使用的JSZIP。

const getSketchData = (file: any) => {
  file.arrayBuffer().then(async (buffer: ArrayBuffer) => {
    if (buffer.byteLength === 0) {
      isTreeLoading.value = false;
      const docuList = document.getElementById("doc-multiple-content");
      if (!docuList) return;
      docuList.innerHTML = "";
      return error("设计稿源文件存在问题");
    }
    const zipFile = await JSZip.loadAsync(buffer);
    const docJson = await readJsonFile(zipFile, "document.json");
    const pages = docJson.pages;
    for (const page of pages) {
      const pageJson = await readPageFileRefJson(zipFile, page);
      const contentLayers = analysisSketchPageLabelObjects(pageJson, "artboard");
      pagesArr.value.push(contentLayers);
    }
    getLeftDirectoryTree();
  });
};

设计稿平铺页面,每个预览图其实对应的是我们sketch中的画板。

生成整张Android代码

xml布局的根标签我们使用通用的约束布局ConstraintLayout,每个子容器使用的线性布局LinearLayout,其他个子元素都有对应的布局和属性生成,关键代码如下:

import { ExportData, LayerData, State } from "./types";

const state: State = {
  zoom: 1,
  unit: "dp",
  scale: 1,
  artboardIndex: 0,
  colorFormat: "color-hex",
  current: undefined,
  codeType: "css",
  selectedIndex: 0,
  targetIndex: 0,
  tempTargetRect: undefined
};

type LayerItem = { id: string; type: string };
type ProjectData = ExportData & { colorNames: { [key: string]: string } };
let project: ProjectData = <ProjectData>{
  resolution: 1
};

// 生成矢量图
function createShapeGroup(layerData: any): string {
  return `
    <View
      android:id="@+id/${createAndroidId(layerData.objectID)}"
      android:layout_marginLeft="${unitSize(layerData.rect.x, false)}"
      android:layout_marginTop="${unitSize(layerData.rect.y, false)}"
      android:layout_width="${unitSize(layerData.rect.width, false)}"
      android:layout_height="${unitSize(layerData.rect.height, false)}" 
      ${getAndroidBackground(layerData)}/>  
  `;
}

// 生成Android TextView
function createTextView(layerData: any): string {
  return `
    <TextView
      android:id="@+id/${createAndroidId(layerData.objectID)}"
      android:layout_marginLeft="${unitSize(layerData.rect.x, false)}"
      android:layout_marginTop="${unitSize(layerData.rect.y, false)}"
      android:layout_width="${unitSize(layerData.rect.width, false)}"
      android:layout_height="${unitSize(layerData.rect.height, false)}"
      android:fontFamily="${layerData.fontFamily}"
      android:text="${layerData.content}"
      android:textColor="${layerData.fontColor["rgba-hex"]}"
      android:textFace="${layerData.fontFace}"
      android:textSize="${unitFontSize(layerData.fontSize)}" 
      ${getAndroidBackground(layerData)}/>
  `;
}

// 生成Android ImageView
function createImageView(layerData: any): string {
  return `
    <ImageView
      android:id="@+id/${createAndroidId(layerData.objectID)}"
      android:layout_marginLeft="${unitSize(layerData.rect.x, false)}"
      android:layout_marginTop="${unitSize(layerData.rect.y, false)}"
      android:layout_width="${unitSize(layerData.rect.width, false)}"
      android:layout_height="${unitSize(layerData.rect.height, false)}"
      android:src="${layerData.imageRef}" 
      ${getAndroidBackground(layerData)} />
  `;
}

// 生成Android Slice XML布局
export function getAndroidCss(layerData: any): string {
  if (layerData.type == "text") {
    return createTextView(layerData);
  } else if (layerData.type == "bitmap") {
    return createImageView(layerData);
  } else if (layerData.type == "group") {
    return createShapeGroup(layerData);
  } else if (
    layerData.type == "rectangle" ||
    layerData.type == "shapePath" ||
    layerData.type == "oval"
  ) {
    return createShapeGroup(layerData);
  }
  return "";
}

// 记录无重复
const layerIdsSet = new Set<String>();

// 生成Android代码片段递归方法
function createAndroidXmlDeep(artboard: any, layerData: any): string {
  layerIdsSet.add(layerData.objectID);
  if (layerData.type === "text") {
    return createTextView(layerData);
  } else if (layerData.type === "bitmap") {
    return createImageView(layerData);
  } else if (
    layerData.type == "rectangle" ||
    layerData.type == "shapePath" ||
    layerData.type == "oval"
  ) {
    return createShapeGroup(layerData);
  } else if (layerData.type === "group") {
    return `
      <LinearLayout
        android:id="@+id/${createAndroidId(layerData.objectID)}"
        android:layout_marginLeft="${unitSize(layerData.rect.x, false)}"
        android:layout_marginTop="${unitSize(layerData.rect.y, false)}"
        android:layout_width="${unitSize(layerData.rect.width, false)}"
        android:layout_height="${unitSize(layerData.rect.height, false)}" 
        ${getAndroidBackground(layerData)}>
        ${
          !!layerData.children.length
            ? layerData.children.map((item: LayerItem) => {
                const layerItem = artboard.layerObjects[item.id];
                if (layerItem) return createAndroidXmlDeep(artboard, layerItem);
                return "";
              })
            : ""
        }
      </LinearLayout>
    `;
  }
  return "";
}

// 生成整张画板Android布局代码
export function createAndroidXmlWithArtboard(artboard: any) {
  const groupLayerIdList = artboard.layers.filter((objectId: string) => {
    const layerItem = artboard.layerObjects[objectId];
    if (layerItem) return layerItem.type === "group" && !!layerItem.children.length;
    return false;
  });

  // 清空set,重新统计
  layerIdsSet.clear();

  // 带Group的代码片段
  const groupCodeStr = !!groupLayerIdList.length
    ? groupLayerIdList.map((objectId: string) => {
        const layerDataItem = artboard.layerObjects[objectId];
        return createAndroidXmlDeep(artboard, layerDataItem);
      })
    : "";

  // 筛选出游离图层(Layer)
  const freeLayerIds: string[] = artboard.layers.filter((it: string) => !layerIdsSet.has(it));

  // 生成游离图层的代码片段
  const freeCodeStr = freeLayerIds.map((objectId: string) => {
    const layerDataItem = artboard.layerObjects[objectId];
    return createAndroidXmlDeep(artboard, layerDataItem);
  });

  const constraintLayoutStr = ` 
    <androidx.constraintlayout.widget.ConstraintLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      ${groupCodeStr}
      ${freeCodeStr}
    </androidx.constraintlayout.widget.ConstraintLayout> 
  `;

  const outerStr = constraintLayoutStr.replace(/,/g, "");
  return outerStr;
}

// 创建图层图层
function createAndroidId(objectID: string) {
  const regObjectID: string = objectID.replace(/-/g, "");
  return `id_${regObjectID.toLowerCase()}`;
}

// 生成宽高属性代码
function getAndroidWithHeight(layerData: LayerData) {
  return `
    android:layout_width="${unitSize(layerData.rect.width, false)}"
    android:layout_height="${unitSize(layerData.rect.height, false)}"`;
}

// 生成marginLeft,marginTop属性代码
function getAndroidMarginLeftTop(layerData: LayerData) {
  return `
    android:layout_marginLeft="${unitSize(layerData.rect.x, false)}"
    android:layout_marginTop="${unitSize(layerData.rect.y, false)}"`;
}

// 生成背景色属性代码
function getAndroidBackground(layerData: LayerData) {
  if (!layerData.fills || layerData.fills.length == 0) return "";
  const colorItem = layerData.fills.find((it) => it.fillType === "Color");
  if (colorItem) {
    return `android:background="${colorItem.color["argb-hex"]}"`;
  }
  return "";
}

function unitSize(value: number, isText?: boolean): string {
  let pt = value / project.resolution;
  let sz = Math.round(pt * state.scale * 100) / 100;
  let units = state.unit.split("/");
  let unit = units[0];
  if (units.length > 1 && isText) {
    unit = units[1];
  }
  return sz + unit;
}

function unitFontSize(value: number): string {
  const pt = value / project.resolution;
  const sz = Math.round(pt * state.scale * 100) / 100;
  return sz + "sp";
}

生成效果图如下:

[图片上传失败...(image-b8f2d2-1680092605582)]

参考资料

相关文章

网友评论

      本文标题:通过Sketch生成整张Android代码

      本文链接:https://www.haomeiwen.com/subject/ejllqdtx.html