当前位置:   article > 正文

【Unity编辑器扩展】(三)PSD转UGUI Prefab, 一键拼UI解放美术/程序(完结)_unity psd

unity psd

解放UI程序/美术? psd文件一键转ui prefab 支持所有ui类型 支持textmeshpro

psd一键转ugui prefab工具 设计原理和详细使用方法

工具效果:

第一步,把psd图层转换为可编辑的节点树,并自动解析UI类型、自动绑定UI子元素:

 第二步, 点击“生成UIForm"按钮生成UI预制体 (若有UI类型遗漏可在下拉菜单手动点选UI类型):

 验证一键生成UI效果:

书接上回:【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出_psd导入unity_TopGames的博客-CSDN博客

先上总结:

工具包含的功能:

1. 支持UGUI和TextMeshProGUI并新增FillColor(纯色)共16种UI类型:

Button、TMP Button、 Dropdown、TMP Dropdown、FillColor、Image、InputField、TMP InputField、Mask、RawImage、Scroll View、Slider、Text、TMP Text、Toggle、TMP Toggle。

2. 支持自定义UI类型匹配词,支持扩展自定义解析器Helper,易扩展。

3. 支持批量导出图片和单独导出某个图层图片,美术仅提供psd,无需切图。

4. 支持自动同步UI元素位置和文本字体、字号、行列间距、字体颜色。解放繁琐的手动调节流程。

5. 自动根据UI类型导出图片为Sprite、Texture2D类型,并支持选择导出后是否压缩图片,若UI需要9宫拉伸自动对Sprite设置9宫边界。

6. 支持编辑(手动调节层级或UI类型),如果UI设计师有遗漏标记类型,程序可手动点选类型,类型刷新后工具自动绑定UI元素。

7. 支持编辑阶段预览psd图层、组。

8. 支持任意UI类型嵌套组合,psd图层层级导出为UI预制体后保持一致。

Aspose.PSD库虽然很强大,但它毕竟是脱离PS的独立解析库,对于PS的有些功能支持并不完善,比如图层特效(如描边、浮雕、阴影等),把单个图层转换为图片图层的特效会丢失。对于文本图层,转换为图片后会有字体样式改变的问题。比如PS文本用的是宋体字体,转换为图片后变成了默认的雅黑字体。

好在Aspose.PSD支持半个PS智能对象,为什么说是半个,因为Aspose.PSD完美支持PS智能对象图层,但是,通过Aspose.PSD把带有特效的PS图层转换为智能对象后会丢失图层特效。

为了解决这一问题,不得不对之前的设计做出让步,写一个自动转换图层为智能对象的PS脚本,以避免设计师手动转换会有遗漏。UI设计师交付psd前通过脚本自动把所有文本图层和带有特效的图层转换为智能对象。这样才能绕过Aspose.PSD导出图片丢失图层特效的问题。

尽管有了使用PS脚本这个小瑕疵,但相比让UI设计师单独切图并手动标识各个切图位置大小、字体字号颜色等,他们仍然觉得这是一个巨大的解放。同样,对于技术来说,也节省大量时间。即使设计师遗漏了UI类型标记,也可以通过下拉框选择图层的UI类型,仅需简单标记类型就可以一键生成UI预制体。

Aspose.PSD仍在每月一个版本更新迭代,期待功能完善,摆脱所有瑕疵。

PSD转UGUI功能/工作流及原理:

 一、PSD规范要求(UI设计师)

由于UI大多属于复合型UI(如上图),即由多种UI元素类型组合而成。例如,Dropdown(下拉菜单),主要由下拉框+下拉列表+下拉列表Item三个主体组成,而三个主体又是由其他多个UI元素组成。

UI是由一个或多个UI元素构成,因此多个元素之间必须有父子节点的关系。而PS图层中没有这种关系,只能通过组(Group)把多个图层包起来,而组本身是一个空图层。

例如一个Button,通常包含一个背景图和一个按钮文本。图层结构如下:

实际上UI设计师原本也是需要用组来管理图层和切图的,这一规范并不是问题。主要是UI类型标记,通过对图层命名以".类型",工具通过对图层类型的识别以及每种UI有单独的解析Helper,最大程度上智能判定识别UI元素类型,对于无迹可寻的元素仍然需要设计师手动标记UI类型。

例如Button解析器(ButtonHelper), 会依次按类型查找图层, 可以最大化放宽对图层标记类型:

  1. buttonBackground = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
  2. buttonText = LayerNode.FindSubLayerNode(GUIType.Button_Text, GUIType.Text, GUIType.TMPText);

二、解析规则配置

 支持配置文本图层和非文本图层的默认类型,例如文本图层默认识别为Text或TextMeshProGUI类型,普通图层默认识别为Image或RawImage类型。

UI Type: 主UI类型和子UI类型。支持的类型如下:

 UIPrefab: UI模板预制体。

TypeMatches:UI类型匹配名, 例如Button的匹配项有.bt,.btn,.button。图层名以这些字符结尾就会被识别为Button。

UIHelper: UI的解析逻辑。不同的UI通过重写解析方法对UI元素和对应PS图层进行绑定,以及生成最终的UI GameObject。

Comment:注释说明,用于一键导出说明文档给UI设计师参考。

总的来说,规则配置文件是为了更灵活宽松,可以自由自定义多个UI类型的别名。

以下是一键导出的文档内容:

使用说明:

单元素UI:即单个图层的UI,如Image、Text、单图Button,可以直接在图层命名结尾加上".类型"来标记UI类型。
如"A.btn"表示按钮。

多元素UI: 对于多个图片组成的复合型UI,可以通过使用"组"包裹多个UI元素。在“组”命名结尾加上".类型"来标记UI类型。
组里的图层命名后夹".类型"来标记为UI子元素类型。

各种UI类型支持任意组合:如一个组类型标记为Button,组内包含一个按钮背景图层,一个艺术字图层(非文本图层),就可以组成一个按钮内带有艺术字图片的按钮。

UI类型标识: 图层/组命名以'.类型'结尾

UI类型标识列表:

Image: UI图片, Sprite精灵图,支持九宫拉伸

类型标识: .img, .image,

RawImage: Texture贴图, 不支持九宫拉伸

类型标识: .rimg, .tex, .rawimg, .rawimage,

Text: UGUI普通Text文本

类型标识: .txt, .text, .label,

TMPText: Text Mesh Pro, 加强版文本类型. 通常无需标注此类型,使用Text类型即可

类型标识: .tmptxt, .tmptext, .tmplabel,

Mask: 遮罩图,根据遮罩图alpha对可使区域混合

类型标识: .msk, .mask,

FillColor: 纯色直角矩形图,例如直角矩形纯色图层可以在Unity中设置颜色实现,无需导出纯色图片

类型标识: .col, .color, .fillcolor,

Background: 背景图,  如Button背景,Toggle背景、InputField背景、ScrollView等

类型标识: .bg, .background, .panel,

Button: 按钮, 通常包含按钮背景图、按钮文本

类型标识: .bt, .btn, .button,

TMPButton: 按钮(Text Mesh Pro)

类型标识: .tmpbt, .tmpbtn, .tmpbutton,

Button_Highlight: 按钮高亮时显示的按钮图片(当按钮为多种状态图切换时)

类型标识: .onover, .light, .highlight,

Button_Press: 按住按钮时显示的图片(当按钮为多种状态图切换时)

类型标识: .press, .click, .touch,

Button_Select: 选中按钮时显示的图片(当按钮为多种状态图切换时)

类型标识: .select, .focus,

Button_Disable: 禁用按钮时显示的图片(当按钮为多种状态图切换时)

类型标识: .disable, .forbid,

Button_Text: 按钮文本,必须是文本图层. 如果是艺术字图片可以标记为Image

类型标识: .bttxt, .btlb, .bttext, .btlabel, .buttontext, .buttonlabel,

Dropdown: 下拉菜单, 由下拉框、下拉列表(ScrollVIew)、Toggle类型的item组成

类型标识: .dpd, .dropdown,

TMPDropdown: 按钮(Text Mesh Pro)

类型标识: .tmpdpd, .tmpdropdown,

Dropdown_Label: 下拉框上显示的文本

类型标识: .dpdlb, .dpdlabel, .dpdtxt, .dpdtext, .dropdowntext, .dropdownlabel, .dropdowntxt, .dropdownlb,

Dropdown_Arrow: 下拉框箭头图标

类型标识: .dpdicon, .dpdarrow, .arrow, .dropdownarrow,

InputField: 文本输入框,通常由输入框背景图、提示文本、输入文本组成

类型标识: .ipt, .input, .inputbox, .inputfield,

TMPInputField: 文本输入框(Text Mesh Pro)

类型标识: .tmpipt, .tmpinput, .tmpinputbox, .tmpinputfield,

InputField_Placeholder: 输入框内的提示文本

类型标识: .placeholder, .ipttips, .tips, .inputtips,

InputField_Text: 输入框输入的文本(样式)

类型标识: .ipttxt, .ipttext, .iptlb, .iptlabel, .inputtext, .inputlabel,

Toggle: 单选框/复选框

类型标识: .tg, .toggle, .checkbox,

TMPToggle: 勾选框/单选框/复选框(Text Mesh Pro)

类型标识: .tmptg, .tmptoggle, .tmpcheckbox,

Toggle_Checkmark: 勾选框,勾选状态图标

类型标识: .mark, .tgmark, .togglemark,

Toggle_Label: 勾选框文本

类型标识: .tglb, .tgtxt, .toggletext, .togglelabel,

Slider: 滑动条/进度条,通常由背景图和填充条组成

类型标识: .sld, .slider,

Slider_Fill: 滑动条/进度条的填充条

类型标识: .fill, .sldfill, .sliderfill,

Slider_Handle: 滑动条的拖动滑块

类型标识: .handle, .sldhandle, .sliderhandle,

ScrollView: 滚动列表,通常由背景图、垂直/水平滚条背景图以及垂直/水平滚动条组成

类型标识: .sv, .scrollview, .lst, .listview,

ScrollView_Viewport: 滚动列表的视口遮罩图

类型标识: .vpt, .viewport, .svmask, .lstmask, .listviewport, .scrollviewport,

ScrollView_HorizontalBarBG: 滚动列表的水平滑动条背景图

类型标识: .hbarbg, .hbarbackground, .hbarpanel,

ScrollView_HorizontalBar: 滚动列表的水平滑动条

类型标识: .hbar, .svhbar, .lsthbar,

ScrollView_VerticalBarBG: 滚动列表的垂直滑动条背景图

类型标识: .vbarbg, .vbarbackground, .vbarpanel,

ScrollView_VerticalBar: 滚动列表的垂直滑动条

类型标识: .vbar, .svvbar, .lstvbar,

 UGUI Parser代码:

  1. #if UNITY_EDITOR
  2. using Aspose.PSD.FileFormats.Psd.Layers.FillLayers;
  3. using System;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using TMPro;
  8. using UnityEditor;
  9. using UnityEngine;
  10. namespace UGF.EditorTools.Psd2UGUI
  11. {
  12. public enum GUIType
  13. {
  14. Null = 0,
  15. Image,
  16. RawImage,
  17. Text,
  18. Button,
  19. Dropdown,
  20. InputField,
  21. Toggle,
  22. Slider,
  23. ScrollView,
  24. Mask,
  25. FillColor, //纯色填充
  26. TMPText,
  27. TMPButton,
  28. TMPDropdown,
  29. TMPInputField,
  30. TMPToggle,
  31. //UI的子类型, 以101开始。 0-100预留给UI类型, 新类型从尾部追加
  32. Background = 101, //通用背景
  33. //Button的子类型
  34. Button_Highlight,
  35. Button_Press,
  36. Button_Select,
  37. Button_Disable,
  38. Button_Text,
  39. //Dropdown/TMPDropdown的子类型
  40. Dropdown_Label,
  41. Dropdown_Arrow,
  42. //InputField/TMPInputField的子类型
  43. InputField_Placeholder,
  44. InputField_Text,
  45. //Toggle的子类型
  46. Toggle_Checkmark,
  47. Toggle_Label,
  48. //Slider的子类型
  49. Slider_Fill,
  50. Slider_Handle,
  51. //ScrollView的子类型
  52. ScrollView_Viewport, //列表可视区域的遮罩图
  53. ScrollView_HorizontalBarBG, //水平滑动栏背景
  54. ScrollView_HorizontalBar,//水平滑块
  55. ScrollView_VerticalBarBG, //垂直滑动栏背景
  56. ScrollView_VerticalBar, //垂直滑动块
  57. }
  58. [Serializable]
  59. public class UGUIParseRule
  60. {
  61. public GUIType UIType;
  62. public string[] TypeMatches; //类型匹配标识
  63. public GameObject UIPrefab; //UI模板
  64. public string UIHelper; //UIHelper类型全名
  65. public string Comment;//注释
  66. }
  67. [CustomEditor(typeof(UGUIParser))]
  68. public class UGUIParserEditor : Editor
  69. {
  70. private SerializedProperty readmeProperty;
  71. private void OnEnable()
  72. {
  73. readmeProperty = serializedObject.FindProperty("readmeDoc");
  74. }
  75. public override void OnInspectorGUI()
  76. {
  77. serializedObject.Update();
  78. if (GUILayout.Button("导出使用文档"))
  79. {
  80. (target as UGUIParser).ExportReadmeDoc();
  81. }
  82. EditorGUILayout.LabelField("使用说明:");
  83. readmeProperty.stringValue = EditorGUILayout.TextArea(readmeProperty.stringValue, GUILayout.Height(100));
  84. serializedObject.ApplyModifiedProperties();
  85. base.OnInspectorGUI();
  86. }
  87. }
  88. [CreateAssetMenu(fileName = "Psd2UIFormConfig", menuName = "ScriptableObject/Psd2UIForm Config【Psd2UIForm工具配置】")]
  89. public class UGUIParser : ScriptableObject
  90. {
  91. public const int UITYPE_MAX = 100;
  92. [SerializeField] GUIType defaultTextType = GUIType.Text;
  93. [SerializeField] GUIType defaultImageType = GUIType.Image;
  94. [SerializeField] GameObject uiFormTemplate;
  95. [SerializeField] UGUIParseRule[] rules;
  96. [HideInInspector][SerializeField] string readmeDoc = "使用说明";
  97. public GUIType DefaultText => defaultTextType;
  98. public GUIType DefaultImage => defaultImageType;
  99. public GameObject UIFormTemplate => uiFormTemplate;
  100. private static UGUIParser mInstance = null;
  101. public static UGUIParser Instance
  102. {
  103. get
  104. {
  105. if (mInstance == null)
  106. {
  107. var guid = AssetDatabase.FindAssets("t:UGUIParser").FirstOrDefault();
  108. mInstance = AssetDatabase.LoadAssetAtPath<UGUIParser>(AssetDatabase.GUIDToAssetPath(guid));
  109. }
  110. return mInstance;
  111. }
  112. }
  113. public static bool IsMainUIType(GUIType tp)
  114. {
  115. return (int)tp <= UITYPE_MAX;
  116. }
  117. public Type GetHelperType(GUIType uiType)
  118. {
  119. if (uiType == GUIType.Null) return null;
  120. var rule = GetRule(uiType);
  121. if (rule == null || string.IsNullOrWhiteSpace(rule.UIHelper)) return null;
  122. return Type.GetType(rule.UIHelper);
  123. }
  124. public UGUIParseRule GetRule(GUIType uiType)
  125. {
  126. foreach (var rule in rules)
  127. {
  128. if (rule.UIType == uiType) return rule;
  129. }
  130. return null;
  131. }
  132. /// <summary>
  133. /// 根据图层命名解析UI类型
  134. /// </summary>
  135. /// <param name="layer"></param>
  136. /// <param name="comType"></param>
  137. /// <returns></returns>
  138. public bool TryParse(PsdLayerNode layer, out UGUIParseRule result)
  139. {
  140. result = null;
  141. var layerName = layer.BindPsdLayer.Name;
  142. if (Path.HasExtension(layerName))
  143. {
  144. var tpTag = Path.GetExtension(layerName).Substring(1).ToLower();
  145. foreach (var rule in rules)
  146. {
  147. foreach (var item in rule.TypeMatches)
  148. {
  149. if (tpTag.CompareTo(item.ToLower()) == 0)
  150. {
  151. result = rule;
  152. return true;
  153. }
  154. }
  155. }
  156. }
  157. switch (layer.LayerType)
  158. {
  159. case PsdLayerType.TextLayer:
  160. result = rules.First(itm => itm.UIType == defaultTextType);
  161. break;
  162. case PsdLayerType.LayerGroup:
  163. result = rules.First(itm => itm.UIType == GUIType.Null);
  164. break;
  165. default:
  166. result = rules.First(itm => itm.UIType == defaultImageType);
  167. break;
  168. }
  169. return result != null;
  170. }
  171. /// <summary>
  172. /// 根据图层大小和位置设置UI节点大小和位置
  173. /// </summary>
  174. /// <param name="layerNode"></param>
  175. /// <param name="uiNode"></param>
  176. /// <param name="pos">是否设置位置</param>
  177. public static void SetRectTransform(PsdLayerNode layerNode, UnityEngine.Component uiNode, bool pos = true, bool width = true, bool height = true, int extSize = 0)
  178. {
  179. if (uiNode != null && layerNode != null)
  180. {
  181. var rect = layerNode.LayerRect;
  182. var rectTransform = uiNode.GetComponent<RectTransform>();
  183. if (width) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x + extSize);
  184. if (height) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y + extSize);
  185. if (pos)
  186. {
  187. rectTransform.position = rect.position + rectTransform.rect.size * (rectTransform.pivot - Vector2.one * 0.5f)*0.01f;
  188. }
  189. }
  190. }
  191. /// <summary>
  192. /// 把LayerNode图片保存到本地并返回
  193. /// </summary>
  194. /// <param name="layerNode"></param>
  195. /// <returns></returns>
  196. public static Texture2D LayerNode2Texture(PsdLayerNode layerNode)
  197. {
  198. if (layerNode != null)
  199. {
  200. var spAssetName = layerNode.ExportImageAsset(false);
  201. var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(spAssetName);
  202. return texture;
  203. }
  204. return null;
  205. }
  206. /// <summary>
  207. /// 把LayerNode图片保存到本地并返回
  208. /// </summary>
  209. /// <param name="layerNode"></param>
  210. /// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>
  211. /// <returns></returns>
  212. public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)
  213. {
  214. if (layerNode != null)
  215. {
  216. var spAssetName = layerNode.ExportImageAsset(true);
  217. var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);
  218. if (sprite != null)
  219. {
  220. if (auto9Slice)
  221. {
  222. var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;
  223. var rawReadable = spImpt.isReadable;
  224. if (!rawReadable)
  225. {
  226. spImpt.isReadable = true;
  227. spImpt.SaveAndReimport();
  228. }
  229. if (spImpt.spriteBorder == Vector4.zero)
  230. {
  231. spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);
  232. spImpt.isReadable = rawReadable;
  233. spImpt.SaveAndReimport();
  234. }
  235. }
  236. return sprite;
  237. }
  238. }
  239. return null;
  240. }
  241. /// <summary>
  242. /// 自动计算贴图的 9宫 Border
  243. /// </summary>
  244. /// <param name="texture"></param>
  245. /// <param name="alphaThreshold">0-255</param>
  246. /// <returns></returns>
  247. public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)
  248. {
  249. int width = texture.width;
  250. int height = texture.height;
  251. Color32[] pixels = texture.GetPixels32();
  252. int minX = width;
  253. int minY = height;
  254. int maxX = 0;
  255. int maxY = 0;
  256. // 寻找不透明像素的最小和最大边界
  257. for (int y = 0; y < height; y++)
  258. {
  259. for (int x = 0; x < width; x++)
  260. {
  261. int pixelIndex = y * width + x;
  262. Color32 pixel = pixels[pixelIndex];
  263. if (pixel.a >= alphaThreshold)
  264. {
  265. minX = Mathf.Min(minX, x);
  266. minY = Mathf.Min(minY, y);
  267. maxX = Mathf.Max(maxX, x);
  268. maxY = Mathf.Max(maxY, y);
  269. }
  270. }
  271. }
  272. // 计算最优的borderSize
  273. int borderSizeX = (maxX - minX) / 3;
  274. int borderSizeY = (maxY - minY) / 3;
  275. int borderSize = Mathf.Min(borderSizeX, borderSizeY);
  276. // 根据边界和Border Size计算Nine Slice Border
  277. int left = minX + borderSize;
  278. int right = maxX - borderSize;
  279. int top = minY + borderSize;
  280. int bottom = maxY - borderSize;
  281. // 确保边界在纹理范围内
  282. left = Mathf.Clamp(left, 0, width - 1);
  283. right = Mathf.Clamp(right, 0, width - 1);
  284. top = Mathf.Clamp(top, 0, height - 1);
  285. bottom = Mathf.Clamp(bottom, 0, height - 1);
  286. return new Vector4(left, top, width - right, height - bottom);
  287. }
  288. /// <summary>
  289. /// 把PS的字体样式同步设置到UGUI Text
  290. /// </summary>
  291. /// <param name="txtLayer"></param>
  292. /// <param name="text"></param>
  293. public static void SetTextStyle(PsdLayerNode txtLayer, UnityEngine.UI.Text text)
  294. {
  295. if (text == null) return;
  296. text.gameObject.SetActive(txtLayer != null);
  297. if (txtLayer != null && txtLayer.ParseTextLayerInfo(out var str, out var size, out var charSpace, out float lineSpace, out var col, out var style, out var tmpStyle, out var fName))
  298. {
  299. var tFont = FindFontAsset(fName);
  300. if (tFont != null) text.font = tFont;
  301. text.text = str;
  302. text.fontSize = size;
  303. text.fontStyle = style;
  304. text.color = col;
  305. text.lineSpacing = lineSpace;
  306. }
  307. }
  308. /// <summary>
  309. /// 把PS的字体样式同步设置到TextMeshProUGUI
  310. /// </summary>
  311. /// <param name="txtLayer"></param>
  312. /// <param name="text"></param>
  313. public static void SetTextStyle(PsdLayerNode txtLayer, TextMeshProUGUI text)
  314. {
  315. if (txtLayer != null && txtLayer.ParseTextLayerInfo(out var str, out var size, out var charSpace, out float lineSpace, out var col, out var style, out var tmpStyle, out var fName))
  316. {
  317. var tFont = FindTMPFontAsset(fName);
  318. if (tFont != null) text.font = tFont;
  319. text.text = str;
  320. text.fontSize = size;
  321. text.fontStyle = tmpStyle;
  322. text.color = col;
  323. text.characterSpacing = charSpace;
  324. text.lineSpacing = lineSpace;
  325. }
  326. }
  327. /// <summary>
  328. /// 根据字体名查找TMP_FontAsset
  329. /// </summary>
  330. /// <param name="fontName"></param>
  331. /// <returns></returns>
  332. public static TMP_FontAsset FindTMPFontAsset(string fontName)
  333. {
  334. var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");
  335. foreach (var guid in fontGuids)
  336. {
  337. var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  338. var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);
  339. if (font != null && font.faceInfo.familyName == fontName)
  340. {
  341. return font;
  342. }
  343. }
  344. return null;
  345. }
  346. /// <summary>
  347. /// 根据字体名查找Font Asset
  348. /// </summary>
  349. /// <param name="fontName"></param>
  350. /// <returns></returns>
  351. public static UnityEngine.Font FindFontAsset(string fontName)
  352. {
  353. var fontGuids = AssetDatabase.FindAssets("t:font");
  354. foreach (var guid in fontGuids)
  355. {
  356. var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  357. var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;
  358. if (font != null && font.fontTTFName == fontName)
  359. {
  360. return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);
  361. }
  362. }
  363. return null;
  364. }
  365. internal static UnityEngine.Color LayerNode2Color(PsdLayerNode fillColor, Color defaultColor)
  366. {
  367. if (fillColor != null && fillColor.BindPsdLayer is FillLayer fillLayer)
  368. {
  369. var layerColor = fillLayer.GetPixel(fillLayer.Width / 2, fillLayer.Height / 2);
  370. return new UnityEngine.Color(layerColor.R, layerColor.G, layerColor.B, fillLayer.Opacity) / (float)255;
  371. }
  372. return defaultColor;
  373. }
  374. /// <summary>
  375. /// 导出UI设计师使用规则文档
  376. /// </summary>
  377. /// <exception cref="NotImplementedException"></exception>
  378. internal void ExportReadmeDoc()
  379. {
  380. var exportDir = EditorUtility.SaveFolderPanel("选择文档导出路径", Application.dataPath, null);
  381. if (string.IsNullOrWhiteSpace(exportDir) || !Directory.Exists(exportDir))
  382. {
  383. return;
  384. }
  385. var docFile = UtilityBuiltin.ResPath.GetCombinePath(exportDir, "Psd2UGUI设计师使用文档.doc");
  386. var strBuilder = new StringBuilder();
  387. strBuilder.AppendLine("使用说明:");
  388. strBuilder.AppendLine(this.readmeDoc);
  389. strBuilder.AppendLine(Environment.NewLine + Environment.NewLine);
  390. strBuilder.AppendLine("UI类型标识: 图层/组命名以'.类型'结尾");
  391. strBuilder.AppendLine("UI类型标识列表:");
  392. foreach (var rule in rules)
  393. {
  394. if (rule.UIType == GUIType.Null) continue;
  395. strBuilder.AppendLine($"{rule.UIType}: {rule.Comment}");
  396. strBuilder.Append("类型标识: ");
  397. foreach (var tag in rule.TypeMatches)
  398. {
  399. strBuilder.Append($".{tag}, ");
  400. }
  401. strBuilder.AppendLine();
  402. strBuilder.AppendLine();
  403. }
  404. try
  405. {
  406. File.WriteAllText(docFile, strBuilder.ToString(), System.Text.Encoding.UTF8);
  407. EditorUtility.RevealInFinder(docFile);
  408. }
  409. catch (Exception e)
  410. {
  411. Debug.LogException(e);
  412. }
  413. }
  414. }
  415. }
  416. #endif

 三、PS脚本编写,一键转换特效图层/文本图层为智能对象

为了辅助UI设计师,避免手动转换智能对象会有遗漏,设计师交付PSD文件前需要执行自动化脚本,把特效图层/字体转为智能对象,这样即使不同设备字库丢失也能保持字体原本样式。PS脚本是用js语言编写,没有代码提示是最大的障碍。好在没有复杂逻辑,只是遍历当前打开的psd文档图层,判断图层是否带有特效或是否为文本图层,把符合条件的图层转换为智能对象:

  1. // 判断图层是否包含特效
  2. function hasLayerEffect(layer) {
  3. app.activeDocument.activeLayer = layer;
  4. var hasEffect = false;
  5. try {
  6. var ref = new ActionReference();
  7. var keyLayerEffects = app.charIDToTypeID( 'Lefx' );
  8. ref.putProperty( app.charIDToTypeID( 'Prpr' ), keyLayerEffects );
  9. ref.putEnumerated( app.charIDToTypeID( 'Lyr ' ), app.charIDToTypeID( 'Ordn' ), app.charIDToTypeID( 'Trgt' ) );
  10. var desc = executeActionGet( ref );
  11. if ( desc.hasKey( keyLayerEffects ) ) {
  12. hasEffect = true;
  13. }
  14. }catch(e) {
  15. hasEffect = false;
  16. }
  17. return hasEffect;
  18. }
  19. function convertLayersToSmartObjects(layers)
  20. {
  21. for (var i = layers.length - 1; i >= 0; i--)
  22. {
  23. var layer = layers[i];
  24. if (layer.typename === "LayerSet")
  25. {
  26. convertLayersToSmartObjects(layer.layers); // Recursively convert layers in layer sets
  27. }
  28. else
  29. {
  30. if (hasLayerEffect(layer)){
  31. if(layer.kind === LayerKind.TEXT)convertToSmartObject(layer); // Convert layers with layer effects to smart objects
  32. else layer.rasterize(RasterizeType.SHAPE);
  33. }
  34. }
  35. }
  36. }
  37. // 把图层转换为智能对象,功能等同右键图层->转为智能对象
  38. function convertToSmartObject(layer) {
  39. app.activeDocument.activeLayer = layer;
  40. // 创建一个新的智能对象
  41. var idnewPlacedLayer = stringIDToTypeID("newPlacedLayer");
  42. executeAction(idnewPlacedLayer, undefined, DialogModes.NO);
  43. }
  44. // 导出处理后的PSD文件
  45. function exportPSD() {
  46. var doc = app.activeDocument;
  47. var savePath = Folder.selectDialog("选择psd导出路径");
  48. if (savePath != null) {
  49. var saveOptions = new PhotoshopSaveOptions();
  50. saveOptions.embedColorProfile = true;
  51. saveOptions.alphaChannels = true;
  52. var saveFile = new File(savePath + "/" + doc.name);
  53. doc.saveAs(saveFile, saveOptions, true, Extension.LOWERCASE);
  54. alert("PSD已成功导出!");
  55. }
  56. }
  57. function convertAndExport(){
  58. convertLayersToSmartObjects (app.activeDocument.layers);
  59. //exportPSD();
  60. }
  61. app.activeDocument.suspendHistory("Convert2SmartObject", "convertAndExport();");
  62. //~ convertLayersToSmartObjects (app.activeDocument.layers);

四、Psd转UGUI编辑器

1. Unity中右键PSD文件把PS图层转换成节点,每个节点绑定一个对应图层。

2. 解析UI设计师为UI标记的类型,自动标识图层是否需要导出,自动绑定UI子元素。

3. 查漏补缺,对于没有标记类型并且没有正确识别绑定的UI元素进行手动选择类型。

 编辑器根节点提供各项持久化保存设置,并且支持自动压缩图片。压缩方法可参考之前写过的压缩工具:【Unity编辑器扩展】包体优化神器,图片压缩,批量生成图集/图集变体,动画压缩_unity 图片压缩_TopGames的博客-CSDN博客

4. 解析psd图层:重新解析psd为节点树状图。

5. 导出Images:把编辑器下勾选的图层节点导出为图片资源。

6. 生成UIForm:把当前的节点树解析生成为UI界面预制体。

Psd2UIForm编辑器代码:

  1. #if UNITY_EDITOR
  2. using Aspose.PSD.FileFormats.Psd;
  3. using Aspose.PSD.FileFormats.Psd.Layers;
  4. using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
  5. using Aspose.PSD.ImageLoadOptions;
  6. using GameFramework;
  7. using HarmonyLib;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Text;
  13. using UnityEditor;
  14. using UnityEditor.SceneManagement;
  15. using UnityEngine;
  16. using UnityGameFramework.Runtime;
  17. namespace UGF.EditorTools.Psd2UGUI
  18. {
  19. #region Crack
  20. [HarmonyPatch(typeof(System.Xml.XmlElement), nameof(System.Xml.XmlElement.InnerText), MethodType.Getter)]
  21. class CrackAspose
  22. {
  23. static void Postfix(ref string __result)
  24. {
  25. if (__result == "20220516")
  26. {
  27. __result = "20500516";
  28. }
  29. //else if (__result == "20210827")
  30. //{
  31. // __result = "20250827";
  32. //}
  33. }
  34. }
  35. #endregion
  36. [CustomEditor(typeof(Psd2UIFormConverter))]
  37. public class Psd2UIFormConverterInspector : UnityEditor.Editor
  38. {
  39. Psd2UIFormConverter targetLogic;
  40. GUIContent parsePsd2NodesBt;
  41. GUIContent exportUISpritesBt;
  42. GUIContent generateUIFormBt;
  43. GUILayoutOption btHeight;
  44. private void OnEnable()
  45. {
  46. btHeight = GUILayout.Height(30);
  47. targetLogic = target as Psd2UIFormConverter;
  48. parsePsd2NodesBt = new GUIContent("解析psd图层", "把psd图层解析为可编辑节点树");
  49. exportUISpritesBt = new GUIContent("导出Images", "导出勾选的psd图层为碎图");
  50. generateUIFormBt = new GUIContent("生成UIForm", "根据解析后的节点树生成UIForm Prefab");
  51. if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))
  52. {
  53. Debug.LogWarning($"UIForm输出路径为空!");
  54. }
  55. }
  56. private void OnDisable()
  57. {
  58. Psd2UIFormSettings.Save();
  59. }
  60. public override void OnInspectorGUI()
  61. {
  62. EditorGUILayout.BeginVertical("box");
  63. {
  64. EditorGUILayout.BeginHorizontal();
  65. {
  66. EditorGUILayout.LabelField("自动压缩图片:", GUILayout.Width(150));
  67. Psd2UIFormSettings.Instance.CompressImage = EditorGUILayout.Toggle(Psd2UIFormSettings.Instance.CompressImage);
  68. EditorGUILayout.EndHorizontal();
  69. }
  70. EditorGUILayout.BeginHorizontal();
  71. {
  72. EditorGUILayout.LabelField("UI图片导出路径:", GUILayout.Width(150));
  73. Psd2UIFormSettings.Instance.UIImagesOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIImagesOutputDir);
  74. if (GUILayout.Button("选择路径", GUILayout.Width(80)))
  75. {
  76. var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIImagesOutputDir, null);
  77. if (!string.IsNullOrWhiteSpace(retPath))
  78. {
  79. if (!retPath.StartsWith("Assets/"))
  80. {
  81. retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
  82. }
  83. Psd2UIFormSettings.Instance.UIImagesOutputDir = retPath;
  84. Psd2UIFormSettings.Save();
  85. }
  86. GUIUtility.ExitGUI();
  87. }
  88. EditorGUILayout.EndHorizontal();
  89. }
  90. EditorGUILayout.BeginHorizontal();
  91. {
  92. Psd2UIFormSettings.Instance.UseUIFormOutputDir = EditorGUILayout.ToggleLeft("使用UIForm导出路径:", Psd2UIFormSettings.Instance.UseUIFormOutputDir, GUILayout.Width(150));
  93. EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.UseUIFormOutputDir);
  94. {
  95. Psd2UIFormSettings.Instance.UIFormOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormOutputDir);
  96. if (GUILayout.Button("选择路径", GUILayout.Width(80)))
  97. {
  98. var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormOutputDir, null);
  99. if (!string.IsNullOrWhiteSpace(retPath))
  100. {
  101. if (!retPath.StartsWith("Assets/"))
  102. {
  103. retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
  104. }
  105. Psd2UIFormSettings.Instance.UIFormOutputDir = retPath;
  106. Psd2UIFormSettings.Save();
  107. }
  108. GUIUtility.ExitGUI();
  109. }
  110. EditorGUI.EndDisabledGroup();
  111. }
  112. EditorGUILayout.EndHorizontal();
  113. }
  114. //EditorGUILayout.BeginHorizontal();
  115. //{
  116. // Psd2UIFormSettings.Instance.AutoCreateUIFormScript = EditorGUILayout.ToggleLeft("生成UIForm代码:", Psd2UIFormSettings.Instance.AutoCreateUIFormScript, GUILayout.Width(150));
  117. // EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.AutoCreateUIFormScript);
  118. // {
  119. // Psd2UIFormSettings.Instance.UIFormScriptOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormScriptOutputDir);
  120. // if (GUILayout.Button("选择路径", GUILayout.Width(80)))
  121. // {
  122. // var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormScriptOutputDir, null);
  123. // if (!string.IsNullOrWhiteSpace(retPath))
  124. // {
  125. // if (!retPath.StartsWith("Assets/"))
  126. // {
  127. // retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
  128. // }
  129. // Psd2UIFormSettings.Instance.UIFormScriptOutputDir = retPath;
  130. // Psd2UIFormSettings.Save();
  131. // }
  132. // GUIUtility.ExitGUI();
  133. // }
  134. // EditorGUI.EndDisabledGroup();
  135. // }
  136. // EditorGUILayout.EndHorizontal();
  137. //}
  138. EditorGUILayout.EndVertical();
  139. }
  140. EditorGUILayout.BeginHorizontal();
  141. {
  142. if (GUILayout.Button(parsePsd2NodesBt, btHeight))
  143. {
  144. Psd2UIFormConverter.ParsePsd2LayerPrefab(targetLogic.PsdAssetName, targetLogic);
  145. }
  146. if (GUILayout.Button(exportUISpritesBt, btHeight))
  147. {
  148. targetLogic.ExportSprites();
  149. }
  150. EditorGUILayout.EndHorizontal();
  151. }
  152. if (GUILayout.Button(generateUIFormBt, btHeight))
  153. {
  154. targetLogic.GenerateUIForm();
  155. }
  156. base.OnInspectorGUI();
  157. }
  158. public override bool HasPreviewGUI()
  159. {
  160. return targetLogic.BindPsdAsset != null;
  161. }
  162. public override void OnPreviewGUI(Rect r, GUIStyle background)
  163. {
  164. GUI.DrawTexture(r, targetLogic.BindPsdAsset.texture, ScaleMode.ScaleToFit);
  165. //base.OnPreviewGUI(r, background);
  166. }
  167. }
  168. /// <summary>
  169. /// Psd文件转成UIForm prefab
  170. /// </summary>
  171. [ExecuteInEditMode]
  172. [RequireComponent(typeof(SpriteRenderer))]
  173. public class Psd2UIFormConverter : MonoBehaviour
  174. {
  175. const string RecordLayerOperation = "Change Export Image";
  176. public static Psd2UIFormConverter Instance { get; private set; }
  177. [ReadOnlyField][SerializeField] public string psdAssetChangeTime;//文件修改时间标识
  178. [Tooltip("UIForm名字")][SerializeField] private string uiFormName;
  179. [Tooltip("关联的psd文件")][SerializeField] private UnityEngine.Sprite psdAsset;
  180. [Header("Debug:")][SerializeField] bool drawLayerRectGizmos = true;
  181. [SerializeField] UnityEngine.Color drawLayerRectGizmosColor = UnityEngine.Color.green;
  182. private PsdImage psdInstance;//psd文件解析实例
  183. private GUIStyle uiTypeLabelStyle;
  184. public string PsdAssetName => psdAsset != null ? AssetDatabase.GetAssetPath(psdAsset) : null;
  185. public UnityEngine.Sprite BindPsdAsset => psdAsset;
  186. public Vector2Int UIFormCanvasSize { get; private set; } = new Vector2Int(750, 1334);
  187. private void OnEnable()
  188. {
  189. Instance = this;
  190. uiTypeLabelStyle = new GUIStyle();
  191. uiTypeLabelStyle.fontSize = 13;
  192. uiTypeLabelStyle.fontStyle = UnityEngine.FontStyle.BoldAndItalic;
  193. UnityEngine.ColorUtility.TryParseHtmlString("#7ED994", out var color);
  194. uiTypeLabelStyle.normal.textColor = color;
  195. EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
  196. if (psdInstance == null && !string.IsNullOrWhiteSpace(PsdAssetName))
  197. {
  198. RefreshNodesBindLayer();
  199. }
  200. }
  201. private void Start()
  202. {
  203. if (this.CheckPsdAssetHasChanged())
  204. {
  205. if (EditorUtility.DisplayDialog("PSD -> UIForm", $"{gameObject.name}关联的psd文件[{this.PsdAssetName}]已改变,是否重新解析节点树?", "是", "否"))
  206. {
  207. if (Psd2UIFormConverter.ParsePsd2LayerPrefab(this.PsdAssetName, this))
  208. {
  209. RefreshNodesBindLayer();
  210. }
  211. }
  212. }
  213. else
  214. {
  215. RefreshNodesBindLayer();
  216. }
  217. }
  218. private void OnDrawGizmos()
  219. {
  220. if (drawLayerRectGizmos)
  221. {
  222. var nodes = this.GetComponentsInChildren<PsdLayerNode>();
  223. Gizmos.color = drawLayerRectGizmosColor;
  224. foreach (var item in nodes)
  225. {
  226. if (item.NeedExportImage())
  227. {
  228. Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);
  229. }
  230. }
  231. }
  232. }
  233. private void OnHierarchyGUI(int instanceID, Rect selectionRect)
  234. {
  235. if (Event.current == null) return;
  236. var node = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
  237. if (node == null || node == this.gameObject) return;
  238. if (!node.TryGetComponent<PsdLayerNode>(out var layer)) return;
  239. Rect tmpRect = selectionRect;
  240. tmpRect.x = 35;
  241. tmpRect.width = 10;
  242. Undo.RecordObject(layer, RecordLayerOperation);
  243. EditorGUI.BeginChangeCheck();
  244. {
  245. layer.markToExport = EditorGUI.Toggle(tmpRect, layer.markToExport);
  246. if (EditorGUI.EndChangeCheck())
  247. {
  248. if (Selection.gameObjects.Length > 1) SetExportImageTg(Selection.gameObjects, layer.markToExport);
  249. EditorUtility.SetDirty(layer);
  250. }
  251. }
  252. tmpRect.width = Mathf.Clamp(selectionRect.xMax * 0.2f, 100, 200);
  253. tmpRect.x = selectionRect.xMax - tmpRect.width;
  254. //EditorGUI.LabelField(tmpRect, layer.UIType.ToString(), uiTypeLabelStyle);
  255. if (EditorGUI.DropdownButton(tmpRect, new GUIContent(layer.UIType.ToString()), FocusType.Passive))
  256. {
  257. var dropdownMenu = PopEnumMenu<GUIType>(layer.UIType, selectUIType =>
  258. {
  259. layer.SetUIType(selectUIType);
  260. EditorUtility.SetDirty(layer);
  261. });
  262. dropdownMenu.ShowAsContext();
  263. }
  264. }
  265. private GenericMenu PopEnumMenu<T>(T currentValue, Action<T> onSelectEnum) where T : Enum
  266. {
  267. var names = Enum.GetValues(typeof(T));
  268. var dropdownMenu = new GenericMenu();
  269. foreach (T item in names)
  270. {
  271. dropdownMenu.AddItem(new GUIContent(item.ToString()), item.Equals(currentValue), () => { onSelectEnum(item); });
  272. }
  273. return dropdownMenu;
  274. }
  275. /// <summary>
  276. /// 批量勾选导出图片
  277. /// </summary>
  278. /// <param name="selects"></param>
  279. /// <param name="exportImg"></param>
  280. private void SetExportImageTg(GameObject[] selects, bool exportImg)
  281. {
  282. var selectLayerNodes = selects.Where(item => item?.GetComponent<PsdLayerNode>() != null).ToArray();
  283. foreach (var layer in selectLayerNodes)
  284. {
  285. layer.GetComponent<PsdLayerNode>().markToExport = exportImg;
  286. }
  287. }
  288. private void OnDestroy()
  289. {
  290. EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyGUI;
  291. if (this.psdInstance != null && !psdInstance.Disposed)
  292. {
  293. psdInstance.Dispose();
  294. }
  295. }
  296. private void RefreshNodesBindLayer()
  297. {
  298. if (psdInstance == null || psdInstance.Disposed)
  299. {
  300. if (!File.Exists(PsdAssetName))
  301. {
  302. Debug.LogError($"刷新节点绑定图层失败! psd文件不存在");
  303. return;
  304. }
  305. var psdOpts = new PsdLoadOptions()
  306. {
  307. LoadEffectsResource = true,
  308. ReadOnlyMode = false,
  309. };
  310. psdInstance = Aspose.PSD.Image.Load(PsdAssetName, psdOpts) as PsdImage;
  311. UIFormCanvasSize.Set(psdInstance.Size.Width, psdInstance.Size.Height);
  312. }
  313. var layers = GetComponentsInChildren<PsdLayerNode>(true);
  314. foreach (var layer in layers)
  315. {
  316. layer.InitPsdLayers(psdInstance);
  317. }
  318. var spRender = gameObject.GetOrAddComponent<SpriteRenderer>();
  319. spRender.sprite = this.psdAsset;
  320. }
  321. #region
  322. const string AsposeLicenseKey = "此处为Aspose.PSD证书";
  323. static bool licenseInitiated = false;
  324. [InitializeOnLoadMethod]
  325. static void InitAsposeLicense()
  326. {
  327. if (licenseInitiated) return;
  328. var harmonyHook = new Harmony("Crack.Aspose");
  329. harmonyHook.PatchAll();
  330. new Aspose.PSD.License().SetLicense(new MemoryStream(Convert.FromBase64String(AsposeLicenseKey)));
  331. licenseInitiated = true;
  332. harmonyHook.UnpatchAll();
  333. //GetAllLayerType();
  334. }
  335. static void GetAllLayerType()
  336. {
  337. var psdLib = Utility.Assembly.GetAssemblies().FirstOrDefault(item => item.GetName().Name == "Aspose.PSD");
  338. var layers = psdLib.GetTypes().Where(tp => tp.IsSubclassOf(typeof(Layer)) && !tp.IsAbstract);
  339. string layerEnumNames = "";
  340. foreach (var item in layers)
  341. {
  342. layerEnumNames += $"{item.Name},\n";
  343. }
  344. Debug.Log(layerEnumNames);
  345. }
  346. #endregion Aspose License
  347. [MenuItem("Assets/GF Editor Tool/Psd2UIForm Editor", priority = 0)]
  348. static void Psd2UIFormPrefabMenu()
  349. {
  350. if (Selection.activeObject == null) return;
  351. var assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
  352. if (Path.GetExtension(assetPath).ToLower().CompareTo(".psd") != 0)
  353. {
  354. Debug.LogWarning($"选择的文件({assetPath})不是psd格式, 工具只支持psd转换为UIForm");
  355. return;
  356. }
  357. string psdLayerPrefab = GetPsdLayerPrefabPath(assetPath);
  358. if (!File.Exists(psdLayerPrefab))
  359. {
  360. if (ParsePsd2LayerPrefab(assetPath))
  361. {
  362. OpenPsdLayerEditor(psdLayerPrefab);
  363. }
  364. }
  365. else
  366. {
  367. OpenPsdLayerEditor(psdLayerPrefab);
  368. }
  369. }
  370. public bool CheckPsdAssetHasChanged()
  371. {
  372. if (psdAsset == null) return false;
  373. var fileTag = GetAssetChangeTag(PsdAssetName);
  374. return psdAssetChangeTime.CompareTo(fileTag) != 0;
  375. }
  376. public static string GetAssetChangeTag(string fileName)
  377. {
  378. return new FileInfo(fileName).LastWriteTime.ToString("yyyyMMddHHmmss");
  379. }
  380. /// <summary>
  381. /// 打开psd图层信息prefab
  382. /// </summary>
  383. /// <param name="psdLayerPrefab"></param>
  384. public static void OpenPsdLayerEditor(string psdLayerPrefab)
  385. {
  386. PrefabStageUtility.OpenPrefab(psdLayerPrefab);
  387. }
  388. /// <summary>
  389. /// 把Psd图层解析成节点prefab
  390. /// </summary>
  391. /// <param name="psdPath"></param>
  392. /// <returns></returns>
  393. public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null)
  394. {
  395. if (!File.Exists(psdFile))
  396. {
  397. Debug.LogError($"Error: Psd文件不存在:{psdFile}");
  398. return false;
  399. }
  400. var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;
  401. if (texImporter.textureType != TextureImporterType.Sprite)
  402. {
  403. texImporter.textureType = TextureImporterType.Sprite;
  404. texImporter.mipmapEnabled = false;
  405. texImporter.alphaIsTransparency = true;
  406. texImporter.SaveAndReimport();
  407. }
  408. var prefabFile = GetPsdLayerPrefabPath(psdFile);
  409. var rootName = Path.GetFileNameWithoutExtension(prefabFile);
  410. bool needDestroyInstance = instanceRoot == null;
  411. if (instanceRoot != null)
  412. {
  413. ParsePsdLayer2Root(psdFile, instanceRoot);
  414. instanceRoot.RefreshNodesBindLayer();
  415. return true;
  416. }
  417. else
  418. {
  419. Psd2UIFormConverter rootLayer = CreatePsdLayerRoot(rootName);
  420. rootLayer.SetPsdAsset(psdFile);
  421. ParsePsdLayer2Root(psdFile, rootLayer);
  422. PrefabUtility.SaveAsPrefabAsset(rootLayer.gameObject, prefabFile, out bool savePrefabSuccess);
  423. if (needDestroyInstance) GameObject.DestroyImmediate(rootLayer.gameObject);
  424. AssetDatabase.Refresh();
  425. if (savePrefabSuccess && AssetDatabase.GUIDFromAssetPath(StageUtility.GetCurrentStage().assetPath) != AssetDatabase.GUIDFromAssetPath(prefabFile))
  426. {
  427. PrefabStageUtility.OpenPrefab(prefabFile);
  428. }
  429. return savePrefabSuccess;
  430. }
  431. }
  432. private static void ParsePsdLayer2Root(string psdFile, Psd2UIFormConverter converter)
  433. {
  434. //清空已有节点重新解析
  435. for (int i = converter.transform.childCount - 1; i >= 0; i--)
  436. {
  437. GameObject.DestroyImmediate(converter.transform.GetChild(i).gameObject);
  438. }
  439. var psdOpts = new PsdLoadOptions()
  440. {
  441. LoadEffectsResource = true,
  442. ReadOnlyMode = false
  443. };
  444. using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage)
  445. {
  446. List<GameObject> layerNodes = new List<GameObject> { converter.gameObject };
  447. for (int i = 0; i < psd.Layers.Length; i++)
  448. {
  449. var layer = psd.Layers[i];
  450. var curLayerType = layer.GetLayerType();
  451. if (curLayerType == PsdLayerType.SectionDividerLayer)
  452. {
  453. var layerGroup = (layer as SectionDividerLayer).GetRelatedLayerGroup();
  454. var layerGroupIdx = ArrayUtility.IndexOf(psd.Layers, layerGroup);
  455. var layerGropNode = CreatePsdLayerNode(layerGroup, layerGroupIdx);
  456. layerNodes.Add(layerGropNode.gameObject);
  457. }
  458. else if (curLayerType == PsdLayerType.LayerGroup)
  459. {
  460. var lastLayerNode = layerNodes.Last();
  461. layerNodes.Remove(lastLayerNode);
  462. if (layerNodes.Count > 0)
  463. {
  464. var parentLayerNode = layerNodes.Last();
  465. lastLayerNode.transform.SetParent(parentLayerNode.transform);
  466. }
  467. }
  468. else
  469. {
  470. var newLayerNode = CreatePsdLayerNode(layer, i);
  471. newLayerNode.transform.SetParent(layerNodes.Last().transform);
  472. newLayerNode.transform.localPosition = Vector3.zero;
  473. }
  474. }
  475. }
  476. converter.psdAssetChangeTime = GetAssetChangeTag(psdFile);
  477. var childrenNodes = converter.GetComponentsInChildren<PsdLayerNode>(true);
  478. foreach (var item in childrenNodes)
  479. {
  480. item.RefreshUIHelper(false);
  481. }
  482. EditorUtility.SetDirty(converter.gameObject);
  483. }
  484. private void SetPsdAsset(string psdFile)
  485. {
  486. this.psdAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(psdFile);
  487. if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIImagesOutputDir))
  488. {
  489. Psd2UIFormSettings.Instance.UIImagesOutputDir = Path.GetDirectoryName(psdFile);
  490. }
  491. if (string.IsNullOrWhiteSpace(this.uiFormName))
  492. {
  493. this.uiFormName = this.psdAsset.name;
  494. }
  495. }
  496. /// <summary>
  497. /// 获取解析好的psd layers文件
  498. /// </summary>
  499. /// <param name="psd"></param>
  500. /// <returns></returns>
  501. public static string GetPsdLayerPrefabPath(string psd)
  502. {
  503. return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");
  504. }
  505. private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName)
  506. {
  507. var node = new GameObject(rootName);
  508. node.gameObject.tag = "EditorOnly";
  509. var layerRoot = node.AddComponent<Psd2UIFormConverter>();
  510. return layerRoot;
  511. }
  512. private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx)
  513. {
  514. string nodeName = layer.Name;
  515. if (string.IsNullOrWhiteSpace(nodeName))
  516. {
  517. nodeName = $"PsdLayer-{bindLayerIdx}";
  518. }
  519. else
  520. {
  521. if (Path.HasExtension(layer.Name))
  522. {
  523. nodeName = Path.GetFileNameWithoutExtension(layer.Name);
  524. }
  525. }
  526. var node = new GameObject(nodeName);
  527. node.gameObject.tag = "EditorOnly";
  528. var layerNode = node.AddComponent<PsdLayerNode>();
  529. layerNode.BindPsdLayerIndex = bindLayerIdx;
  530. InitLayerNodeData(layerNode, layer);
  531. return layerNode;
  532. }
  533. /// <summary>
  534. /// 根据psd图层信息解析并初始化图层UI类型、是否导出等信息
  535. /// </summary>
  536. /// <param name="layerNode"></param>
  537. /// <param name="layer"></param>
  538. private static void InitLayerNodeData(PsdLayerNode layerNode, Layer layer)
  539. {
  540. if (layer == null || layer.Disposed) return;
  541. var layerTp = layer.GetLayerType();
  542. layerNode.BindPsdLayer = layer;
  543. if (UGUIParser.Instance.TryParse(layerNode, out var initRule))
  544. {
  545. layerNode.SetUIType(initRule.UIType, false);
  546. }
  547. layerNode.markToExport = layerTp != PsdLayerType.LayerGroup && !(layerTp == PsdLayerType.TextLayer && layerNode.UIType.ToString().EndsWith("Text") && layerNode.UIType != GUIType.FillColor);
  548. layerNode.gameObject.SetActive(layer.IsVisible);
  549. }
  550. /// <summary>
  551. /// 导出psd图层为Sprites碎图
  552. /// </summary>
  553. /// <param name="psdAssetName"></param>
  554. internal void ExportSprites()
  555. {
  556. //var pngOpts = new PngOptions()
  557. //{
  558. // ColorType = Aspose.PSD.FileFormats.Png.PngColorType.Truecolor
  559. //};
  560. //this.psdInstance.Save("Assets/AAAGame/Sprites/UI/Preview.png", pngOpts);
  561. //return;
  562. var exportLayers = this.GetComponentsInChildren<PsdLayerNode>().Where(node => node.NeedExportImage());
  563. var exportDir = GetUIFormImagesOutputDir();
  564. if (!Directory.Exists(exportDir))
  565. {
  566. Directory.CreateDirectory(exportDir);
  567. }
  568. int exportIdx = 0;
  569. int totalCount = exportLayers.Count();
  570. foreach (var layer in exportLayers)
  571. {
  572. var assetName = layer.ExportImageAsset();
  573. if (assetName == null)
  574. {
  575. Debug.LogWarning($"导出图层[name:{layer.name}, layerIdx:{layer.BindPsdLayerIndex}]图片失败!");
  576. }
  577. ++exportIdx;
  578. EditorUtility.DisplayProgressBar($"导出进度({exportIdx}/{totalCount})", $"导出UI图片:{assetName}", exportIdx / (float)totalCount);
  579. }
  580. EditorUtility.ClearProgressBar();
  581. AssetDatabase.Refresh();
  582. }
  583. /// <summary>
  584. /// 根据解析后的节点树生成UIForm Prefab
  585. /// </summary>
  586. internal void GenerateUIForm()
  587. {
  588. if (Psd2UIFormSettings.Instance.UseUIFormOutputDir && string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))
  589. {
  590. Debug.LogError($"生成UIForm失败! UIForm导出路径为空:{Psd2UIFormSettings.Instance.UIFormOutputDir}");
  591. return;
  592. }
  593. if (Psd2UIFormSettings.Instance.UseUIFormOutputDir)
  594. {
  595. ExportUIPrefab(Psd2UIFormSettings.Instance.UIFormOutputDir);
  596. }
  597. else
  598. {
  599. string lastSaveDir = string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.LastUIFormOutputDir) ? "Assets" : Psd2UIFormSettings.Instance.LastUIFormOutputDir;
  600. string selectDir = EditorUtility.SaveFolderPanel("保存目录", lastSaveDir, null);
  601. if (!string.IsNullOrWhiteSpace(selectDir))
  602. {
  603. if (!selectDir.StartsWith("Assets/"))
  604. selectDir = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, selectDir);
  605. Psd2UIFormSettings.Instance.LastUIFormOutputDir = selectDir;
  606. ExportUIPrefab(selectDir);
  607. }
  608. }
  609. }
  610. private bool ExportUIPrefab(string outputDir)
  611. {
  612. if (!string.IsNullOrWhiteSpace(outputDir))
  613. {
  614. if (!Directory.Exists(outputDir))
  615. {
  616. try
  617. {
  618. Directory.CreateDirectory(outputDir);
  619. AssetDatabase.Refresh();
  620. }
  621. catch (Exception err)
  622. {
  623. Debug.LogError($"导出UI prefab失败:{err.Message}");
  624. return false;
  625. }
  626. }
  627. }
  628. if (string.IsNullOrWhiteSpace(uiFormName))
  629. {
  630. Debug.LogError("导出UI Prefab失败: UI Form Name为空, 请填写UI Form Name.");
  631. return false;
  632. }
  633. var prefabName = UtilityBuiltin.ResPath.GetCombinePath(outputDir, $"{uiFormName}.prefab");
  634. if (File.Exists(prefabName))
  635. {
  636. if (!EditorUtility.DisplayDialog("警告", $"prefab文件已存在, 是否覆盖:{prefabName}", "覆盖生成", "取消生成"))
  637. {
  638. return false;
  639. }
  640. }
  641. var uiHelpers = GetAvailableUIHelpers();
  642. if (uiHelpers == null || uiHelpers.Length < 1)
  643. {
  644. return false;
  645. }
  646. var uiFormRoot = GameObject.Instantiate(UGUIParser.Instance.UIFormTemplate, Vector3.zero, Quaternion.identity);
  647. uiFormRoot.name = uiFormName;
  648. int curIdx = 0;
  649. int totalCount = uiHelpers.Length;
  650. foreach (var uiHelper in uiHelpers)
  651. {
  652. EditorUtility.DisplayProgressBar($"生成UIFrom:({curIdx++}/{totalCount})", $"正在生成UI元素:{uiHelper.name}", curIdx /
  653. (float)totalCount);
  654. var uiElement = uiHelper.CreateUI();
  655. if (uiElement == null) continue;
  656. var goPath = GetGameObjectInstanceIdPath(uiHelper.gameObject, out var goNames);
  657. var parentNode = GetOrCreateNodeByInstanceIdPath(uiFormRoot, goPath, goNames);
  658. uiElement.transform.SetParent(parentNode.transform, true);
  659. uiElement.transform.position += new Vector3(this.UIFormCanvasSize.x * 0.5f, this.UIFormCanvasSize.y * 0.5f, 0);
  660. }
  661. var uiStrKeys = uiFormRoot.GetComponentsInChildren<UIStringKey>(true);
  662. for (int i = uiStrKeys.Length - 1; i >= 0; i--)
  663. {
  664. DestroyImmediate(uiStrKeys[i]);
  665. }
  666. var uiPrefab = PrefabUtility.SaveAsPrefabAsset(uiFormRoot, prefabName, out bool saveSuccess);
  667. if (saveSuccess)
  668. {
  669. DestroyImmediate(uiFormRoot);
  670. Selection.activeGameObject = uiPrefab;
  671. }
  672. EditorUtility.ClearProgressBar();
  673. return true;
  674. }
  675. private GameObject GetOrCreateNodeByInstanceIdPath(GameObject uiFormRoot, string[] goPath, string[] goNames)
  676. {
  677. GameObject result = uiFormRoot;
  678. if (goPath != null && goNames != null)
  679. {
  680. for (int i = 0; i < goPath.Length; i++)
  681. {
  682. var nodeId = goPath[i];
  683. var nodeName = goNames[i];
  684. GameObject targetNode = null;
  685. foreach (Transform child in result.transform)
  686. {
  687. if (child.gameObject == result) continue;
  688. var idKey = child.GetComponent<UIStringKey>();
  689. if (idKey != null && nodeId == idKey.Key)
  690. {
  691. targetNode = child.gameObject;
  692. break;
  693. }
  694. }
  695. if (targetNode == null)
  696. {
  697. targetNode = new GameObject(nodeName);
  698. targetNode.transform.SetParent(result.transform, false);
  699. targetNode.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
  700. var targetNodeKey = targetNode.GetOrAddComponent<UIStringKey>();
  701. targetNodeKey.Key = nodeId;
  702. }
  703. result = targetNode;
  704. }
  705. }
  706. return result;
  707. }
  708. private string[] GetGameObjectInstanceIdPath(GameObject go, out string[] names)
  709. {
  710. names = null;
  711. if (go == null || go.transform.parent == null || go.transform.parent == this.transform) return null;
  712. var parentGo = go.transform.parent;
  713. string[] result = new string[1] { parentGo.gameObject.GetInstanceID().ToString() };
  714. names = new string[1] { parentGo.gameObject.name };
  715. while (parentGo.parent != null && parentGo.parent != this.transform)
  716. {
  717. ArrayUtility.Insert(ref result, 0, parentGo.parent.gameObject.GetInstanceID().ToString());
  718. ArrayUtility.Insert(ref names, 0, parentGo.parent.gameObject.name);
  719. parentGo = parentGo.parent;
  720. }
  721. return result;
  722. }
  723. private UIHelperBase[] GetAvailableUIHelpers()
  724. {
  725. var uiHelpers = this.GetComponentsInChildren<UIHelperBase>();
  726. uiHelpers = uiHelpers.Where(ui => ui.LayerNode.IsMainUIType).ToArray();
  727. List<int> dependInstIds = new List<int>();
  728. foreach (var item in uiHelpers)
  729. {
  730. foreach (var depend in item.GetDependencies())
  731. {
  732. int dependId = depend.gameObject.GetInstanceID();
  733. if (!dependInstIds.Contains(dependId))
  734. {
  735. dependInstIds.Add(dependId);
  736. }
  737. }
  738. }
  739. for (int i = uiHelpers.Length - 1; i >= 0; i--)
  740. {
  741. var uiHelper = uiHelpers[i];
  742. if (dependInstIds.Contains(uiHelper.gameObject.GetInstanceID()))
  743. {
  744. ArrayUtility.RemoveAt(ref uiHelpers, i);
  745. }
  746. }
  747. return uiHelpers;
  748. }
  749. /// <summary>
  750. /// 把图片设置为为Sprite或Texture类型
  751. /// </summary>
  752. /// <param name="dir"></param>
  753. public static void ConvertTexturesType(string[] texAssets, bool isImage = true)
  754. {
  755. foreach (var item in texAssets)
  756. {
  757. var texImporter = AssetImporter.GetAtPath(item) as TextureImporter;
  758. if (texImporter == null)
  759. {
  760. Debug.LogError($"TextureImporter为空:{item}");
  761. continue;
  762. }
  763. if (isImage)
  764. {
  765. texImporter.textureType = TextureImporterType.Sprite;
  766. texImporter.spriteImportMode = SpriteImportMode.Single;
  767. texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
  768. texImporter.alphaIsTransparency = true;
  769. texImporter.mipmapEnabled = false;
  770. }
  771. else
  772. {
  773. texImporter.textureType = TextureImporterType.Default;
  774. texImporter.textureShape = TextureImporterShape.Texture2D;
  775. texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
  776. texImporter.alphaIsTransparency = true;
  777. texImporter.mipmapEnabled = false;
  778. }
  779. texImporter.SaveAndReimport();
  780. }
  781. }
  782. /// <summary>
  783. /// 压缩图片文件
  784. /// </summary>
  785. /// <param name="asset">文件名(相对路径Assets)</param>
  786. /// <returns></returns>
  787. public static bool CompressImageFile(string asset)
  788. {
  789. var assetPath = asset.StartsWith("Assets/") ? Path.GetFullPath(asset, Directory.GetParent(Application.dataPath).FullName) : asset;
  790. var compressTool = Utility.Assembly.GetType("UGF.EditorTools.CompressTool");
  791. if (compressTool == null) return false;
  792. var compressMethod = compressTool.GetMethod("CompressImageOffline", new Type[] { typeof(string), typeof(string) });
  793. if (compressMethod == null) return false;
  794. return (bool)compressMethod.Invoke(null, new object[] { assetPath, assetPath });
  795. }
  796. /// <summary>
  797. /// 获取UIForm对应的图片导出目录
  798. /// </summary>
  799. /// <returns></returns>
  800. public string GetUIFormImagesOutputDir()
  801. {
  802. return UtilityBuiltin.ResPath.GetCombinePath(Psd2UIFormSettings.Instance.UIImagesOutputDir, uiFormName);
  803. }
  804. public SmartObjectLayer ConvertToSmartObjectLayer(Layer layer)
  805. {
  806. var smartObj = psdInstance.SmartObjectProvider.ConvertToSmartObject(new Layer[] { layer });
  807. return smartObj;
  808. }
  809. }
  810. }
  811. #endif

7. 图层节点编辑器扩展,提供导出图片按钮以便单独导出选择图层,UI类型切换时自动添加对应的Helper解析器并自动绑定子UI

  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. using Aspose.PSD.FileFormats.Psd.Layers;
  4. using Aspose.PSD.ImageOptions;
  5. using UnityEditor;
  6. using System.IO;
  7. using System.Linq;
  8. using Aspose.PSD.FileFormats.Psd;
  9. using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
  10. using GameFramework;
  11. namespace UGF.EditorTools.Psd2UGUI
  12. {
  13. [CanEditMultipleObjects]
  14. [CustomEditor(typeof(PsdLayerNode))]
  15. public class PsdLayerNodeInspector : Editor
  16. {
  17. PsdLayerNode targetLogic;
  18. private void OnEnable()
  19. {
  20. targetLogic = target as PsdLayerNode;
  21. targetLogic.RefreshLayerTexture();
  22. }
  23. public override void OnInspectorGUI()
  24. {
  25. serializedObject.Update();
  26. base.OnInspectorGUI();
  27. EditorGUI.BeginChangeCheck();
  28. {
  29. targetLogic.UIType = (GUIType)EditorGUILayout.EnumPopup("UI Type", targetLogic.UIType);
  30. if (EditorGUI.EndChangeCheck())
  31. {
  32. targetLogic.SetUIType(targetLogic.UIType);
  33. }
  34. }
  35. EditorGUILayout.BeginHorizontal();
  36. {
  37. if (GUILayout.Button("导出图片"))
  38. {
  39. foreach (var item in targets)
  40. {
  41. if (item == null) continue;
  42. (item as PsdLayerNode)?.ExportImageAsset();
  43. }
  44. }
  45. EditorGUILayout.EndHorizontal();
  46. }
  47. serializedObject.ApplyModifiedProperties();
  48. }
  49. public override bool HasPreviewGUI()
  50. {
  51. var layerNode = (target as PsdLayerNode);
  52. return layerNode != null && layerNode.PreviewTexture != null;
  53. }
  54. public override void OnPreviewGUI(Rect r, GUIStyle background)
  55. {
  56. var layerNode = (target as PsdLayerNode);
  57. GUI.DrawTexture(r, layerNode.PreviewTexture, ScaleMode.ScaleToFit);
  58. //base.OnPreviewGUI(r, background);
  59. }
  60. public override string GetInfoString()
  61. {
  62. var layerNode = (target as PsdLayerNode);
  63. return layerNode.LayerInfo;
  64. }
  65. }
  66. [ExecuteInEditMode]
  67. [DisallowMultipleComponent]
  68. public class PsdLayerNode : MonoBehaviour
  69. {
  70. [ReadOnlyField] public int BindPsdLayerIndex = -1;
  71. [ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;
  72. [SerializeField] public bool markToExport;
  73. [HideInInspector] public GUIType UIType;
  74. public Texture2D PreviewTexture { get; private set; }
  75. public string LayerInfo { get; private set; }
  76. public Rect LayerRect { get; private set; }
  77. public PsdLayerType LayerType { get => mLayerType; }
  78. public bool IsMainUIType => UGUIParser.IsMainUIType(UIType);
  79. /// <summary>
  80. /// 绑定的psd图层
  81. /// </summary>
  82. private Layer mBindPsdLayer;
  83. public Layer BindPsdLayer
  84. {
  85. get => mBindPsdLayer;
  86. set
  87. {
  88. mBindPsdLayer = value;
  89. mLayerType = mBindPsdLayer.GetLayerType();
  90. //if (IsTextLayer(out var txtLayer) && !txtLayer.TextBoundBox.IsEmpty)
  91. //{
  92. // LayerRect = AsposePsdExtension.PsdRect2UnityRect(txtLayer.TextBoundBox, Psd2UIFormConverter.Instance.UIFormCanvasSize);
  93. //}
  94. //else
  95. {
  96. LayerRect = mBindPsdLayer.GetLayerRect();
  97. }
  98. LayerInfo = $"{LayerRect}";
  99. }
  100. }
  101. private void OnDestroy()
  102. {
  103. if (PreviewTexture != null)
  104. {
  105. DestroyImmediate(PreviewTexture);
  106. }
  107. }
  108. public void SetUIType(GUIType uiType, bool triggerParseFunc = true)
  109. {
  110. this.UIType = uiType;
  111. RemoveUIHelper();
  112. if (triggerParseFunc)
  113. {
  114. RefreshUIHelper(true);
  115. }
  116. }
  117. public void RefreshUIHelper(bool refreshParent = false)
  118. {
  119. if (UIType == GUIType.Null) return;
  120. var uiHelperTp = UGUIParser.Instance.GetHelperType(UIType);
  121. if (uiHelperTp != null)
  122. {
  123. var helper = gameObject.GetOrAddComponent(uiHelperTp) as UIHelperBase;
  124. helper.ParseAndAttachUIElements();
  125. }
  126. if (refreshParent)
  127. {
  128. var parentHelper = transform.parent?.GetComponent<UIHelperBase>();
  129. parentHelper?.ParseAndAttachUIElements();
  130. }
  131. EditorUtility.SetDirty(this);
  132. }
  133. private void RemoveUIHelper()
  134. {
  135. var uiHelpers = this.GetComponents<UIHelperBase>();
  136. if (uiHelpers != null)
  137. {
  138. foreach (var uiHelper in uiHelpers)
  139. {
  140. DestroyImmediate(uiHelper);
  141. }
  142. }
  143. EditorUtility.SetDirty(this);
  144. }
  145. /// <summary>
  146. /// 是否需要导出此图层
  147. /// </summary>
  148. /// <returns></returns>
  149. public bool NeedExportImage()
  150. {
  151. return gameObject.activeSelf && markToExport;
  152. }
  153. /// <summary>
  154. /// 导出图片
  155. /// </summary>
  156. /// <param name="forceSpriteType">强制贴图类型为Sprite</param>
  157. /// <returns></returns>
  158. public string ExportImageAsset(bool forceSpriteType = false)
  159. {
  160. string assetName = null;
  161. if (this.RefreshLayerTexture())
  162. {
  163. var bytes = PreviewTexture.EncodeToPNG();
  164. var imgName = Utility.Text.Format("{0}_{1}", string.IsNullOrWhiteSpace(name) ? UIType : name, BindPsdLayerIndex);
  165. var exportDir = Psd2UIFormConverter.Instance.GetUIFormImagesOutputDir();
  166. if (!Directory.Exists(exportDir))
  167. {
  168. try
  169. {
  170. Directory.CreateDirectory(exportDir);
  171. AssetDatabase.Refresh();
  172. }
  173. catch (System.Exception)
  174. {
  175. return null;
  176. }
  177. }
  178. var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");
  179. File.WriteAllBytes(imgFileName, bytes);
  180. if (Psd2UIFormSettings.Instance.CompressImage)
  181. {
  182. bool compressResult = Psd2UIFormConverter.CompressImageFile(imgFileName);
  183. if (compressResult)
  184. {
  185. Debug.Log($"成功压缩图片:{imgFileName}");
  186. }
  187. else
  188. {
  189. Debug.LogWarning($"压缩图片失败:{imgFileName}");
  190. }
  191. }
  192. assetName = imgFileName;
  193. bool isImage = !(this.UIType == GUIType.FillColor || this.UIType == GUIType.RawImage);
  194. AssetDatabase.Refresh();
  195. Psd2UIFormConverter.ConvertTexturesType(new string[] { imgFileName }, isImage || forceSpriteType);
  196. }
  197. return assetName;
  198. }
  199. public bool RefreshLayerTexture(bool forceRefresh = false)
  200. {
  201. if (!forceRefresh && PreviewTexture != null)
  202. {
  203. return true;
  204. }
  205. if (BindPsdLayer == null || BindPsdLayer.Disposed) return false;
  206. var pngOpt = new PngOptions
  207. {
  208. ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha
  209. };
  210. if (BindPsdLayer.CanSave(pngOpt))
  211. {
  212. if (PreviewTexture != null)
  213. {
  214. DestroyImmediate(PreviewTexture);
  215. }
  216. PreviewTexture = this.ConvertPsdLayer2Texture2D();
  217. }
  218. return PreviewTexture != null;
  219. }
  220. /// <summary>
  221. /// 把psd图层转成Texture2D
  222. /// </summary>
  223. /// <param name="psdLayer"></param>
  224. /// <returns>Texture2D</returns>
  225. public Texture2D ConvertPsdLayer2Texture2D()
  226. {
  227. if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;
  228. MemoryStream ms = new MemoryStream();
  229. var pngOpt = new Aspose.PSD.ImageOptions.PngOptions()
  230. {
  231. ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha,
  232. FullFrame = true
  233. };
  234. if (BindPsdLayer.Opacity >= 255 || LayerType == PsdLayerType.LayerGroup)
  235. {
  236. BindPsdLayer.Save(ms, pngOpt);
  237. }
  238. else
  239. {
  240. var smartLayer = Psd2UIFormConverter.Instance.ConvertToSmartObjectLayer(BindPsdLayer);
  241. smartLayer.Save(ms, pngOpt);
  242. }
  243. //var bitmap = BindPsdLayer.ToBitmap();
  244. //bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
  245. var buffer = new byte[ms.Length];
  246. ms.Position = 0;
  247. ms.Read(buffer, 0, buffer.Length);
  248. Texture2D texture = new Texture2D(BindPsdLayer.Width, BindPsdLayer.Height);
  249. texture.alphaIsTransparency = true;
  250. texture.LoadImage(buffer);
  251. texture.Apply();
  252. ms.Dispose();
  253. return texture;
  254. }
  255. /// <summary>
  256. /// 从第一层子节点按类型查找LayerNode
  257. /// </summary>
  258. /// <param name="uiTp"></param>
  259. /// <returns></returns>
  260. public PsdLayerNode FindSubLayerNode(GUIType uiTp)
  261. {
  262. for (int i = 0; i < transform.childCount; i++)
  263. {
  264. var child = transform.GetChild(i)?.GetComponent<PsdLayerNode>();
  265. if (child != null && child.UIType == uiTp) return child;
  266. }
  267. return null;
  268. }
  269. /// <summary>
  270. /// 依次查找给定多个类型,返回最先找到的类型
  271. /// </summary>
  272. /// <param name="uiTps"></param>
  273. /// <returns></returns>
  274. public PsdLayerNode FindSubLayerNode(params GUIType[] uiTps)
  275. {
  276. foreach (var tp in uiTps)
  277. {
  278. var result = FindSubLayerNode(tp);
  279. if (result != null) return result;
  280. }
  281. return null;
  282. }
  283. public PsdLayerNode FindLayerNodeInChildren(GUIType uiTp)
  284. {
  285. var layers = GetComponentsInChildren<PsdLayerNode>(true);
  286. if (layers != null && layers.Length > 0)
  287. {
  288. return layers.FirstOrDefault(layer => layer.UIType == uiTp);
  289. }
  290. return null;
  291. }
  292. /// <summary>
  293. /// 判断该图层是否为文本图层
  294. /// </summary>
  295. /// <param name="layer"></param>
  296. /// <returns></returns>
  297. public bool IsTextLayer(out TextLayer layer)
  298. {
  299. layer = null;
  300. if (BindPsdLayer == null) return false;
  301. if (BindPsdLayer is SmartObjectLayer smartLayer)
  302. {
  303. layer = smartLayer.GetSmartObjectInnerTextLayer() as TextLayer;
  304. return layer != null;
  305. }
  306. else if (BindPsdLayer is TextLayer txtLayer)
  307. {
  308. layer = txtLayer;
  309. return layer != null;
  310. }
  311. return false;
  312. }
  313. internal void InitPsdLayers(PsdImage psdInstance)
  314. {
  315. BindPsdLayer = psdInstance.Layers[BindPsdLayerIndex];
  316. }
  317. internal bool ParseTextLayerInfo(out string text, out int fontSize, out float characterSpace, out float lineSpace, out Color fontColor, out UnityEngine.FontStyle fontStyle, out TMPro.FontStyles tmpFontStyle, out string fontName)
  318. {
  319. text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;
  320. if (IsTextLayer(out var txtLayer))
  321. {
  322. text = txtLayer.Text;
  323. fontSize = (int)txtLayer.Font.Size;
  324. fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;
  325. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  326. {
  327. fontStyle = UnityEngine.FontStyle.BoldAndItalic;
  328. }
  329. else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  330. {
  331. fontStyle = UnityEngine.FontStyle.Bold;
  332. }
  333. else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  334. {
  335. fontStyle = UnityEngine.FontStyle.Italic;
  336. }
  337. else
  338. {
  339. fontStyle = UnityEngine.FontStyle.Normal;
  340. }
  341. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  342. {
  343. tmpFontStyle |= TMPro.FontStyles.Italic;
  344. }
  345. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  346. {
  347. tmpFontStyle |= TMPro.FontStyles.Bold;
  348. }
  349. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))
  350. {
  351. tmpFontStyle |= TMPro.FontStyles.Underline;
  352. }
  353. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))
  354. {
  355. tmpFontStyle |= TMPro.FontStyles.Strikethrough;
  356. }
  357. fontName = txtLayer.Font.Name;
  358. if (txtLayer.TextData.Items.Length > 0)
  359. {
  360. var txtData = txtLayer.TextData.Items[0];
  361. characterSpace = txtData.Style.Tracking * 0.1f;
  362. lineSpace = (float)txtData.Style.Leading * 0.1f;
  363. }
  364. return true;
  365. }
  366. return false;
  367. }
  368. }
  369. }
  370. #endif

五、UI元素解析/生成器Helper

定义HelperBase解析器基类,不同的UI类型重写UI初始化方法,如需支持新的UI类型可以很方便进行扩展支持:

  1. #if UNITY_EDITOR
  2. using System.Collections.Generic;
  3. using UnityEditor;
  4. using UnityEngine;
  5. using UnityGameFramework.Runtime;
  6. namespace UGF.EditorTools.Psd2UGUI
  7. {
  8. public abstract class UIHelperBase : MonoBehaviour
  9. {
  10. public PsdLayerNode LayerNode => this.GetComponent<PsdLayerNode>();
  11. private void OnEnable()
  12. {
  13. ParseAndAttachUIElements();
  14. }
  15. /// <summary>
  16. /// 解析并关联UI元素,并且返回已经关联过的图层(已关联图层不再处理)
  17. /// </summary>
  18. /// <param name="layerNode"></param>
  19. /// <returns></returns>
  20. public abstract void ParseAndAttachUIElements();
  21. /// <summary>
  22. /// 获取UI依赖的LayerNodes
  23. /// </summary>
  24. /// <returns></returns>
  25. public abstract PsdLayerNode[] GetDependencies();
  26. /// <summary>
  27. /// 把UI实例进行UI元素初始化
  28. /// </summary>
  29. /// <param name="uiRoot"></param>
  30. protected abstract void InitUIElements(GameObject uiRoot);
  31. /// <summary>
  32. /// 筛选出UI依赖的非空LayerNode
  33. /// </summary>
  34. /// <param name="nodes"></param>
  35. /// <returns></returns>
  36. protected PsdLayerNode[] CalculateDependencies(params PsdLayerNode[] nodes)
  37. {
  38. if (nodes == null || nodes.Length == 0) return null;
  39. for (int i = nodes.Length - 1; i >= 0; i--)
  40. {
  41. var node = nodes[i];
  42. if (node == null || node == LayerNode) ArrayUtility.RemoveAt(ref nodes, i);
  43. }
  44. return nodes;
  45. }
  46. internal GameObject CreateUI(GameObject uiInstance = null)
  47. {
  48. if ((int)this.LayerNode.UIType > UGUIParser.UITYPE_MAX || LayerNode.UIType == GUIType.Null) return null;
  49. if (uiInstance == null)
  50. {
  51. var rule = UGUIParser.Instance.GetRule(this.LayerNode.UIType);
  52. if (rule == null || rule.UIPrefab == null)
  53. {
  54. Debug.LogWarning($"创建UI类型{LayerNode.UIType}失败:Rule配置项不存在或UIPrefab为空");
  55. return null;
  56. }
  57. uiInstance = GameObject.Instantiate(rule.UIPrefab, Vector3.zero, Quaternion.identity);
  58. if (LayerNode.IsMainUIType)
  59. {
  60. uiInstance.name = this.name;
  61. var key = uiInstance.GetOrAddComponent<UIStringKey>();
  62. key.Key = this.gameObject.GetInstanceID().ToString();
  63. }
  64. }
  65. InitUIElements(uiInstance);
  66. return uiInstance;
  67. }
  68. }
  69. }
  70. #endif

1. Text解析器:

  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. namespace UGF.EditorTools.Psd2UGUI
  4. {
  5. [DisallowMultipleComponent]
  6. public class TextHelper : UIHelperBase
  7. {
  8. [SerializeField] PsdLayerNode text;
  9. public override PsdLayerNode[] GetDependencies()
  10. {
  11. return CalculateDependencies(text);
  12. }
  13. public override void ParseAndAttachUIElements()
  14. {
  15. if (LayerNode.IsTextLayer(out var _))
  16. {
  17. text = LayerNode;
  18. }
  19. else
  20. {
  21. LayerNode.SetUIType(UGUIParser.Instance.DefaultImage);
  22. }
  23. }
  24. protected override void InitUIElements(GameObject uiRoot)
  25. {
  26. var textCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Text>();
  27. UGUIParser.SetTextStyle(text, textCom);
  28. UGUIParser.SetRectTransform(text, textCom);
  29. }
  30. }
  31. }
  32. #endif

从ps文本图层获取文本字体、字号、颜色、字间距等信息,然后从Unity工程中查找对应的字体文件并赋值给Text组件:

  1. internal bool ParseTextLayerInfo(out string text, out int fontSize, out float characterSpace, out float lineSpace, out Color fontColor, out UnityEngine.FontStyle fontStyle, out TMPro.FontStyles tmpFontStyle, out string fontName)
  2. {
  3. text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;
  4. if (IsTextLayer(out var txtLayer))
  5. {
  6. text = txtLayer.Text;
  7. fontSize = (int)txtLayer.Font.Size;
  8. fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;
  9. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  10. {
  11. fontStyle = UnityEngine.FontStyle.BoldAndItalic;
  12. }
  13. else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  14. {
  15. fontStyle = UnityEngine.FontStyle.Bold;
  16. }
  17. else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  18. {
  19. fontStyle = UnityEngine.FontStyle.Italic;
  20. }
  21. else
  22. {
  23. fontStyle = UnityEngine.FontStyle.Normal;
  24. }
  25. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  26. {
  27. tmpFontStyle |= TMPro.FontStyles.Italic;
  28. }
  29. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  30. {
  31. tmpFontStyle |= TMPro.FontStyles.Bold;
  32. }
  33. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))
  34. {
  35. tmpFontStyle |= TMPro.FontStyles.Underline;
  36. }
  37. if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))
  38. {
  39. tmpFontStyle |= TMPro.FontStyles.Strikethrough;
  40. }
  41. fontName = txtLayer.Font.Name;
  42. if (txtLayer.TextData.Items.Length > 0)
  43. {
  44. var txtData = txtLayer.TextData.Items[0];
  45. characterSpace = txtData.Style.Tracking * 0.1f;
  46. lineSpace = (float)txtData.Style.Leading * 0.1f;
  47. }
  48. return true;
  49. }
  50. return false;
  51. }

根据字体内部名查找ttf字体和TextMeshPro字体资源:

  1. /// <summary>
  2. /// 根据字体名查找TMP_FontAsset
  3. /// </summary>
  4. /// <param name="fontName"></param>
  5. /// <returns></returns>
  6. public static TMP_FontAsset FindTMPFontAsset(string fontName)
  7. {
  8. var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");
  9. foreach (var guid in fontGuids)
  10. {
  11. var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  12. var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);
  13. if (font != null && font.faceInfo.familyName == fontName)
  14. {
  15. return font;
  16. }
  17. }
  18. return null;
  19. }
  20. /// <summary>
  21. /// 根据字体名查找Font Asset
  22. /// </summary>
  23. /// <param name="fontName"></param>
  24. /// <returns></returns>
  25. public static UnityEngine.Font FindFontAsset(string fontName)
  26. {
  27. var fontGuids = AssetDatabase.FindAssets("t:font");
  28. foreach (var guid in fontGuids)
  29. {
  30. var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  31. var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;
  32. if (font != null && font.fontTTFName == fontName)
  33. {
  34. return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);
  35. }
  36. }
  37. return null;
  38. }

2. Image解析器:

  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. namespace UGF.EditorTools.Psd2UGUI
  4. {
  5. [DisallowMultipleComponent]
  6. public class ImageHelper : UIHelperBase
  7. {
  8. [SerializeField] PsdLayerNode image;
  9. public override PsdLayerNode[] GetDependencies()
  10. {
  11. return CalculateDependencies(image);
  12. }
  13. public override void ParseAndAttachUIElements()
  14. {
  15. image = LayerNode;
  16. }
  17. protected override void InitUIElements(GameObject uiRoot)
  18. {
  19. var imgCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Image>();
  20. UGUIParser.SetRectTransform(image,imgCom);
  21. imgCom.sprite = UGUIParser.LayerNode2Sprite(image, imgCom.type == UnityEngine.UI.Image.Type.Sliced);
  22. }
  23. }
  24. }
  25. #endif

自动把ps图层导出为Sprite资源,若Image为Sliced模式则自动计算并设置Sprite 9宫边界:

  1. /// <summary>
  2. /// 把LayerNode图片保存到本地并返回
  3. /// </summary>
  4. /// <param name="layerNode"></param>
  5. /// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>
  6. /// <returns></returns>
  7. public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)
  8. {
  9. if (layerNode != null)
  10. {
  11. var spAssetName = layerNode.ExportImageAsset(true);
  12. var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);
  13. if (sprite != null)
  14. {
  15. if (auto9Slice)
  16. {
  17. var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;
  18. var rawReadable = spImpt.isReadable;
  19. if (!rawReadable)
  20. {
  21. spImpt.isReadable = true;
  22. spImpt.SaveAndReimport();
  23. }
  24. if (spImpt.spriteBorder == Vector4.zero)
  25. {
  26. spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);
  27. spImpt.isReadable = rawReadable;
  28. spImpt.SaveAndReimport();
  29. }
  30. }
  31. return sprite;
  32. }
  33. }
  34. return null;
  35. }

根据图片的Alpha通道计算出9宫边界,通常设置9宫边界还会考虑图片纹理因素,但程序难以智能识别,这里自动9宫只是适用于普通情况,还需要根据实际效果进行手动调整:

  1. /// <summary>
  2. /// 自动计算贴图的 9宫 Border
  3. /// </summary>
  4. /// <param name="texture"></param>
  5. /// <param name="alphaThreshold">0-255</param>
  6. /// <returns></returns>
  7. public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)
  8. {
  9. int width = texture.width;
  10. int height = texture.height;
  11. Color32[] pixels = texture.GetPixels32();
  12. int minX = width;
  13. int minY = height;
  14. int maxX = 0;
  15. int maxY = 0;
  16. // 寻找不透明像素的最小和最大边界
  17. for (int y = 0; y < height; y++)
  18. {
  19. for (int x = 0; x < width; x++)
  20. {
  21. int pixelIndex = y * width + x;
  22. Color32 pixel = pixels[pixelIndex];
  23. if (pixel.a >= alphaThreshold)
  24. {
  25. minX = Mathf.Min(minX, x);
  26. minY = Mathf.Min(minY, y);
  27. maxX = Mathf.Max(maxX, x);
  28. maxY = Mathf.Max(maxY, y);
  29. }
  30. }
  31. }
  32. // 计算最优的borderSize
  33. int borderSizeX = (maxX - minX) / 3;
  34. int borderSizeY = (maxY - minY) / 3;
  35. int borderSize = Mathf.Min(borderSizeX, borderSizeY);
  36. // 根据边界和Border Size计算Nine Slice Border
  37. int left = minX + borderSize;
  38. int right = maxX - borderSize;
  39. int top = minY + borderSize;
  40. int bottom = maxY - borderSize;
  41. // 确保边界在纹理范围内
  42. left = Mathf.Clamp(left, 0, width - 1);
  43. right = Mathf.Clamp(right, 0, width - 1);
  44. top = Mathf.Clamp(top, 0, height - 1);
  45. bottom = Mathf.Clamp(bottom, 0, height - 1);
  46. return new Vector4(left, top, width - right, height - bottom);
  47. }

3. Dropdown解析器,对于多种元素组成的复合型、嵌套型UI,可以很好的支持,并且可以任意嵌套组合,没有限制和约束。例如Dropdown内包含了一个ScrollView和一个Toggle类型的Item,就可以直接用ScrollView Helper和Toggle Helper分别对其解析:

  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. using UnityEngine.UI;
  4. namespace UGF.EditorTools.Psd2UGUI
  5. {
  6. [DisallowMultipleComponent]
  7. public class DropdownHelper : UIHelperBase
  8. {
  9. [SerializeField] PsdLayerNode background;
  10. [SerializeField] PsdLayerNode label;
  11. [SerializeField] PsdLayerNode arrow;
  12. [SerializeField] PsdLayerNode scrollView;
  13. [SerializeField] PsdLayerNode toggleItem;
  14. public override PsdLayerNode[] GetDependencies()
  15. {
  16. return CalculateDependencies(background, label, arrow, scrollView, toggleItem);
  17. }
  18. public override void ParseAndAttachUIElements()
  19. {
  20. background = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
  21. label = LayerNode.FindSubLayerNode(GUIType.Dropdown_Label, GUIType.Text, GUIType.TMPText);
  22. arrow = LayerNode.FindSubLayerNode(GUIType.Dropdown_Arrow);
  23. scrollView = LayerNode.FindSubLayerNode(GUIType.ScrollView);
  24. toggleItem = LayerNode.FindSubLayerNode(GUIType.Toggle);
  25. }
  26. protected override void InitUIElements(GameObject uiRoot)
  27. {
  28. var dpd = uiRoot.GetComponent<Dropdown>();
  29. UGUIParser.SetRectTransform(background, dpd);
  30. var bgImg = dpd.targetGraphic as Image;
  31. bgImg.sprite = UGUIParser.LayerNode2Sprite(background, bgImg.type == Image.Type.Sliced) ?? bgImg.sprite;
  32. UGUIParser.SetTextStyle(label, dpd.captionText);
  33. UGUIParser.SetRectTransform(label, dpd.captionText);
  34. var arrowImg = dpd.transform.Find("Arrow")?.GetComponent<Image>();
  35. if (arrowImg != null)
  36. {
  37. UGUIParser.SetRectTransform(arrow, arrowImg);
  38. arrowImg.sprite = UGUIParser.LayerNode2Sprite(arrow, arrowImg.type == Image.Type.Sliced);
  39. }
  40. if (scrollView != null)
  41. {
  42. var svTmp = uiRoot.GetComponentInChildren<ScrollRect>(true).GetComponent<RectTransform>();
  43. if (svTmp != null)
  44. {
  45. var sViewGo = scrollView.GetComponent<ScrollViewHelper>()?.CreateUI(svTmp.gameObject);
  46. if (sViewGo != null)
  47. {
  48. var sViewRect = sViewGo.GetComponent<RectTransform>();
  49. UGUIParser.SetRectTransform(scrollView, sViewRect);
  50. sViewRect.anchorMin = Vector2.zero;
  51. sViewRect.anchorMax = new Vector2(1, 0);
  52. sViewRect.anchoredPosition = new Vector2(0, -2);
  53. }
  54. if (toggleItem != null)
  55. {
  56. var itemTmp = dpd.itemText != null ? dpd.itemText.transform.parent : null;
  57. if (itemTmp != null)
  58. {
  59. toggleItem.GetComponent<ToggleHelper>()?.CreateUI(itemTmp.gameObject);
  60. }
  61. }
  62. }
  63. }
  64. }
  65. }
  66. }
  67. #endif

实现了每种基础UI元素的解析后就可以任意进行UI元素组合,例如Slider中包含Slider背景和填充条,在Slider中添加一个文本图层,解析出来后就是一个内涵Text文本的Slider进度条,解析前后的节点层级始终保持统一:

由于篇幅原因其它UI类型的解析代码就不贴了,UGUI和TextMeshProGUI共16种UI类型全部完美支持。

最后,附上psd源文件效果图和一键生成的UGUI预制体效果对比图,运行时效果(左),psd原图(右):

 工具开源地址:https://github.com/sunsvip/PSD2UGUI_X

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/514936
推荐阅读
相关标签
  

闽ICP备14008679号