前言
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组件库"
}
设计稿源文件上传
先说下我们设计方案,大致分如下三个步骤:
- Sketch 源文件上传(服务端处理源文件上传 + 转 ZIP 文件 + 解压文件)
- 预览图逐个图片文件上传(由服务端暂存本地后上传文件夹)
- 切图 逐个图片文件上传(由服务端暂存本地后上传文件夹)
解压 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)]
网友评论