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

news2024/11/25 12:59:19

工具效果:

第一步,把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), 会依次按类型查找图层, 可以最大化放宽对图层标记类型:

buttonBackground = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
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代码:

#if UNITY_EDITOR
using Aspose.PSD.FileFormats.Psd.Layers.FillLayers;
using System;
using System.IO;
using System.Linq;
using System.Text;
using TMPro;
using UnityEditor;
using UnityEngine;

namespace UGF.EditorTools.Psd2UGUI
{
    public enum GUIType
    {
        Null = 0,
        Image,
        RawImage,
        Text,
        Button,
        Dropdown,
        InputField,
        Toggle,
        Slider,
        ScrollView,
        Mask,
        FillColor, //纯色填充
        TMPText,
        TMPButton,
        TMPDropdown,
        TMPInputField,
        TMPToggle,

        //UI的子类型, 以101开始。 0-100预留给UI类型, 新类型从尾部追加
        Background = 101, //通用背景

        //Button的子类型
        Button_Highlight,
        Button_Press,
        Button_Select,
        Button_Disable,
        Button_Text,

        //Dropdown/TMPDropdown的子类型
        Dropdown_Label,
        Dropdown_Arrow,

        //InputField/TMPInputField的子类型
        InputField_Placeholder,
        InputField_Text,

        //Toggle的子类型
        Toggle_Checkmark,
        Toggle_Label,

        //Slider的子类型
        Slider_Fill,
        Slider_Handle,

        //ScrollView的子类型
        ScrollView_Viewport, //列表可视区域的遮罩图
        ScrollView_HorizontalBarBG, //水平滑动栏背景
        ScrollView_HorizontalBar,//水平滑块
        ScrollView_VerticalBarBG, //垂直滑动栏背景
        ScrollView_VerticalBar, //垂直滑动块
    }
    [Serializable]
    public class UGUIParseRule
    {
        public GUIType UIType;
        public string[] TypeMatches; //类型匹配标识
        public GameObject UIPrefab; //UI模板
        public string UIHelper; //UIHelper类型全名
        public string Comment;//注释
    }
    [CustomEditor(typeof(UGUIParser))]
    public class UGUIParserEditor : Editor
    {
        private SerializedProperty readmeProperty;

        private void OnEnable()
        {
            readmeProperty = serializedObject.FindProperty("readmeDoc");
        }
        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            if (GUILayout.Button("导出使用文档"))
            {
                (target as UGUIParser).ExportReadmeDoc();
            }
            EditorGUILayout.LabelField("使用说明:");
            readmeProperty.stringValue = EditorGUILayout.TextArea(readmeProperty.stringValue, GUILayout.Height(100));
            serializedObject.ApplyModifiedProperties();
            base.OnInspectorGUI();
        }
    }
    [CreateAssetMenu(fileName = "Psd2UIFormConfig", menuName = "ScriptableObject/Psd2UIForm Config【Psd2UIForm工具配置】")]
    public class UGUIParser : ScriptableObject
    {
        public const int UITYPE_MAX = 100;
        [SerializeField] GUIType defaultTextType = GUIType.Text;
        [SerializeField] GUIType defaultImageType = GUIType.Image;
        [SerializeField] GameObject uiFormTemplate;
        [SerializeField] UGUIParseRule[] rules;
        [HideInInspector][SerializeField] string readmeDoc = "使用说明";

        public GUIType DefaultText => defaultTextType;
        public GUIType DefaultImage => defaultImageType;
        public GameObject UIFormTemplate => uiFormTemplate;
        private static UGUIParser mInstance = null;
        public static UGUIParser Instance
        {
            get
            {
                if (mInstance == null)
                {
                    var guid = AssetDatabase.FindAssets("t:UGUIParser").FirstOrDefault();
                    mInstance = AssetDatabase.LoadAssetAtPath<UGUIParser>(AssetDatabase.GUIDToAssetPath(guid));
                }
                return mInstance;
            }
        }
        public static bool IsMainUIType(GUIType tp)
        {
            return (int)tp <= UITYPE_MAX;
        }
        public Type GetHelperType(GUIType uiType)
        {
            if (uiType == GUIType.Null) return null;
            var rule = GetRule(uiType);
            if (rule == null || string.IsNullOrWhiteSpace(rule.UIHelper)) return null;

            return Type.GetType(rule.UIHelper);
        }
        public UGUIParseRule GetRule(GUIType uiType)
        {
            foreach (var rule in rules)
            {
                if (rule.UIType == uiType) return rule;
            }
            return null;
        }
        /// <summary>
        /// 根据图层命名解析UI类型
        /// </summary>
        /// <param name="layer"></param>
        /// <param name="comType"></param>
        /// <returns></returns>
        public bool TryParse(PsdLayerNode layer, out UGUIParseRule result)
        {
            result = null;
            var layerName = layer.BindPsdLayer.Name;
            if (Path.HasExtension(layerName))
            {
                var tpTag = Path.GetExtension(layerName).Substring(1).ToLower();
                foreach (var rule in rules)
                {
                    foreach (var item in rule.TypeMatches)
                    {
                        if (tpTag.CompareTo(item.ToLower()) == 0)
                        {
                            result = rule;
                            return true;
                        }
                    }
                }
            }

            switch (layer.LayerType)
            {
                case PsdLayerType.TextLayer:
                    result = rules.First(itm => itm.UIType == defaultTextType);
                    break;
                case PsdLayerType.LayerGroup:
                    result = rules.First(itm => itm.UIType == GUIType.Null);
                    break;
                default:
                    result = rules.First(itm => itm.UIType == defaultImageType);
                    break;
            }
            return result != null;
        }

        /// <summary>
        /// 根据图层大小和位置设置UI节点大小和位置
        /// </summary>
        /// <param name="layerNode"></param>
        /// <param name="uiNode"></param>
        /// <param name="pos">是否设置位置</param>
        public static void SetRectTransform(PsdLayerNode layerNode, UnityEngine.Component uiNode, bool pos = true, bool width = true, bool height = true, int extSize = 0)
        {
            if (uiNode != null && layerNode != null)
            {
                var rect = layerNode.LayerRect;
                var rectTransform = uiNode.GetComponent<RectTransform>();
                if (width) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x + extSize);
                if (height) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y + extSize);
                if (pos)
                {
                    rectTransform.position = rect.position + rectTransform.rect.size * (rectTransform.pivot - Vector2.one * 0.5f)*0.01f;
                }
            }
        }

        /// <summary>
        /// 把LayerNode图片保存到本地并返回
        /// </summary>
        /// <param name="layerNode"></param>
        /// <returns></returns>
        public static Texture2D LayerNode2Texture(PsdLayerNode layerNode)
        {
            if (layerNode != null)
            {
                var spAssetName = layerNode.ExportImageAsset(false);
                var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(spAssetName);
                return texture;
            }
            return null;
        }
        /// <summary>
        /// 把LayerNode图片保存到本地并返回
        /// </summary>
        /// <param name="layerNode"></param>
        /// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>
        /// <returns></returns>
        public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)
        {
            if (layerNode != null)
            {
                var spAssetName = layerNode.ExportImageAsset(true);
                var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);
                if (sprite != null)
                {
                    if (auto9Slice)
                    {
                        var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;
                        var rawReadable = spImpt.isReadable;
                        if (!rawReadable)
                        {
                            spImpt.isReadable = true;
                            spImpt.SaveAndReimport();
                        }
                        if (spImpt.spriteBorder == Vector4.zero)
                        {
                            spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);
                            spImpt.isReadable = rawReadable;
                            spImpt.SaveAndReimport();
                        }
                    }
                    return sprite;
                }
            }
            return null;
        }
        /// <summary>
        /// 自动计算贴图的 9宫 Border
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="alphaThreshold">0-255</param>
        /// <returns></returns>
        public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)
        {
            int width = texture.width;
            int height = texture.height;

            Color32[] pixels = texture.GetPixels32();
            int minX = width;
            int minY = height;
            int maxX = 0;
            int maxY = 0;

            // 寻找不透明像素的最小和最大边界
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int pixelIndex = y * width + x;
                    Color32 pixel = pixels[pixelIndex];

                    if (pixel.a >= alphaThreshold)
                    {
                        minX = Mathf.Min(minX, x);
                        minY = Mathf.Min(minY, y);
                        maxX = Mathf.Max(maxX, x);
                        maxY = Mathf.Max(maxY, y);
                    }
                }
            }

            // 计算最优的borderSize
            int borderSizeX = (maxX - minX) / 3;
            int borderSizeY = (maxY - minY) / 3;
            int borderSize = Mathf.Min(borderSizeX, borderSizeY);

            // 根据边界和Border Size计算Nine Slice Border
            int left = minX + borderSize;
            int right = maxX - borderSize;
            int top = minY + borderSize;
            int bottom = maxY - borderSize;

            // 确保边界在纹理范围内
            left = Mathf.Clamp(left, 0, width - 1);
            right = Mathf.Clamp(right, 0, width - 1);
            top = Mathf.Clamp(top, 0, height - 1);
            bottom = Mathf.Clamp(bottom, 0, height - 1);

            return new Vector4(left, top, width - right, height - bottom);
        }

        /// <summary>
        /// 把PS的字体样式同步设置到UGUI Text
        /// </summary>
        /// <param name="txtLayer"></param>
        /// <param name="text"></param>
        public static void SetTextStyle(PsdLayerNode txtLayer, UnityEngine.UI.Text text)
        {
            if (text == null) return;
            text.gameObject.SetActive(txtLayer != null);
            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))
            {
                var tFont = FindFontAsset(fName);
                if (tFont != null) text.font = tFont;
                text.text = str;
                text.fontSize = size;
                text.fontStyle = style;
                text.color = col;
                text.lineSpacing = lineSpace;
            }
        }
        /// <summary>
        /// 把PS的字体样式同步设置到TextMeshProUGUI
        /// </summary>
        /// <param name="txtLayer"></param>
        /// <param name="text"></param>
        public static void SetTextStyle(PsdLayerNode txtLayer, TextMeshProUGUI text)
        {
            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))
            {
                var tFont = FindTMPFontAsset(fName);
                if (tFont != null) text.font = tFont;
                text.text = str;
                text.fontSize = size;
                text.fontStyle = tmpStyle;
                text.color = col;
                text.characterSpacing = charSpace;
                text.lineSpacing = lineSpace;
            }
        }
        /// <summary>
        /// 根据字体名查找TMP_FontAsset
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns></returns>
        public static TMP_FontAsset FindTMPFontAsset(string fontName)
        {
            var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");
            foreach (var guid in fontGuids)
            {
                var fontPath = AssetDatabase.GUIDToAssetPath(guid);
                var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);
                if (font != null && font.faceInfo.familyName == fontName)
                {
                    return font;
                }
            }
            return null;
        }
        /// <summary>
        /// 根据字体名查找Font Asset
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns></returns>
        public static UnityEngine.Font FindFontAsset(string fontName)
        {
            var fontGuids = AssetDatabase.FindAssets("t:font");
            foreach (var guid in fontGuids)
            {
                var fontPath = AssetDatabase.GUIDToAssetPath(guid);
                var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;
                if (font != null && font.fontTTFName == fontName)
                {
                    return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);
                }
            }
            return null;
        }

        internal static UnityEngine.Color LayerNode2Color(PsdLayerNode fillColor, Color defaultColor)
        {
            if (fillColor != null && fillColor.BindPsdLayer is FillLayer fillLayer)
            {
                var layerColor = fillLayer.GetPixel(fillLayer.Width / 2, fillLayer.Height / 2);
                return new UnityEngine.Color(layerColor.R, layerColor.G, layerColor.B, fillLayer.Opacity) / (float)255;
            }
            return defaultColor;
        }
        /// <summary>
        /// 导出UI设计师使用规则文档
        /// </summary>
        /// <exception cref="NotImplementedException"></exception>
        internal void ExportReadmeDoc()
        {
            var exportDir = EditorUtility.SaveFolderPanel("选择文档导出路径", Application.dataPath, null);
            if (string.IsNullOrWhiteSpace(exportDir) || !Directory.Exists(exportDir))
            {
                return;
            }

            var docFile = UtilityBuiltin.ResPath.GetCombinePath(exportDir, "Psd2UGUI设计师使用文档.doc");
            var strBuilder = new StringBuilder();
            strBuilder.AppendLine("使用说明:");
            strBuilder.AppendLine(this.readmeDoc);
            strBuilder.AppendLine(Environment.NewLine + Environment.NewLine);
            strBuilder.AppendLine("UI类型标识: 图层/组命名以'.类型'结尾");
            strBuilder.AppendLine("UI类型标识列表:");

            foreach (var rule in rules)
            {
                if (rule.UIType == GUIType.Null) continue;

                strBuilder.AppendLine($"{rule.UIType}: {rule.Comment}");
                strBuilder.Append("类型标识: ");
                foreach (var tag in rule.TypeMatches)
                {
                    strBuilder.Append($".{tag}, ");
                }
                strBuilder.AppendLine();
                strBuilder.AppendLine();
            }

            try
            {
                File.WriteAllText(docFile, strBuilder.ToString(), System.Text.Encoding.UTF8);
                EditorUtility.RevealInFinder(docFile);
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }

        }
    }
}
#endif

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

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

// 判断图层是否包含特效
function hasLayerEffect(layer) {
    app.activeDocument.activeLayer = layer;
	var hasEffect = false;
	try {
		var ref = new ActionReference();
		var keyLayerEffects = app.charIDToTypeID( 'Lefx' );
		ref.putProperty( app.charIDToTypeID( 'Prpr' ), keyLayerEffects );
		ref.putEnumerated( app.charIDToTypeID( 'Lyr ' ), app.charIDToTypeID( 'Ordn' ), app.charIDToTypeID( 'Trgt' ) );
		var desc = executeActionGet( ref );
		if ( desc.hasKey( keyLayerEffects ) ) {
			hasEffect = true;
		}
	}catch(e) {
		hasEffect = false;
	}
	return hasEffect;
}

function convertLayersToSmartObjects(layers) 
{
    for (var i = layers.length - 1; i >= 0; i--) 
    {
        var layer = layers[i];
        if (layer.typename === "LayerSet")
        {
               convertLayersToSmartObjects(layer.layers); // Recursively convert layers in layer sets
        } 
        else
        {
             if (hasLayerEffect(layer)){
                if(layer.kind === LayerKind.TEXT)convertToSmartObject(layer); // Convert layers with layer effects to smart objects
                else layer.rasterize(RasterizeType.SHAPE);
             }
        }
    }
}
// 把图层转换为智能对象,功能等同右键图层->转为智能对象
function convertToSmartObject(layer) {
    app.activeDocument.activeLayer = layer;
    // 创建一个新的智能对象
    var idnewPlacedLayer = stringIDToTypeID("newPlacedLayer");
    executeAction(idnewPlacedLayer, undefined, DialogModes.NO);
    
}
// 导出处理后的PSD文件
function exportPSD() {
  var doc = app.activeDocument;
  var savePath = Folder.selectDialog("选择psd导出路径");
  if (savePath != null) {
    var saveOptions = new PhotoshopSaveOptions();
    saveOptions.embedColorProfile = true;
    saveOptions.alphaChannels = true;

    var saveFile = new File(savePath + "/" + doc.name);
    doc.saveAs(saveFile, saveOptions, true, Extension.LOWERCASE);
    alert("PSD已成功导出!");
  }
}
function convertAndExport(){
    convertLayersToSmartObjects (app.activeDocument.layers);
    //exportPSD();
}
app.activeDocument.suspendHistory("Convert2SmartObject", "convertAndExport();");
//~ 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编辑器代码:

#if UNITY_EDITOR
using Aspose.PSD.FileFormats.Psd;
using Aspose.PSD.FileFormats.Psd.Layers;
using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
using Aspose.PSD.ImageLoadOptions;
using GameFramework;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityGameFramework.Runtime;

namespace UGF.EditorTools.Psd2UGUI
{
    #region Crack
    [HarmonyPatch(typeof(System.Xml.XmlElement), nameof(System.Xml.XmlElement.InnerText), MethodType.Getter)]
    class CrackAspose
    {
        static void Postfix(ref string __result)
        {
            if (__result == "20220516")
            {
                __result = "20500516";
            }
            //else if (__result == "20210827")
            //{
            //    __result = "20250827";
            //}
        }
    }
    #endregion
    [CustomEditor(typeof(Psd2UIFormConverter))]
    public class Psd2UIFormConverterInspector : UnityEditor.Editor
    {
        Psd2UIFormConverter targetLogic;

        GUIContent parsePsd2NodesBt;
        GUIContent exportUISpritesBt;
        GUIContent generateUIFormBt;
        GUILayoutOption btHeight;
        private void OnEnable()
        {
            btHeight = GUILayout.Height(30);
            targetLogic = target as Psd2UIFormConverter;
            parsePsd2NodesBt = new GUIContent("解析psd图层", "把psd图层解析为可编辑节点树");
            exportUISpritesBt = new GUIContent("导出Images", "导出勾选的psd图层为碎图");
            generateUIFormBt = new GUIContent("生成UIForm", "根据解析后的节点树生成UIForm Prefab");
            if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))
            {
                Debug.LogWarning($"UIForm输出路径为空!");
            }
        }
        private void OnDisable()
        {
            Psd2UIFormSettings.Save();
        }
        public override void OnInspectorGUI()
        {
            EditorGUILayout.BeginVertical("box");
            {
                EditorGUILayout.BeginHorizontal();
                {
                    EditorGUILayout.LabelField("自动压缩图片:", GUILayout.Width(150));
                    Psd2UIFormSettings.Instance.CompressImage = EditorGUILayout.Toggle(Psd2UIFormSettings.Instance.CompressImage);
                    EditorGUILayout.EndHorizontal();
                }
                EditorGUILayout.BeginHorizontal();
                {
                    EditorGUILayout.LabelField("UI图片导出路径:", GUILayout.Width(150));
                    Psd2UIFormSettings.Instance.UIImagesOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIImagesOutputDir);
                    if (GUILayout.Button("选择路径", GUILayout.Width(80)))
                    {
                        var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIImagesOutputDir, null);
                        if (!string.IsNullOrWhiteSpace(retPath))
                        {
                            if (!retPath.StartsWith("Assets/"))
                            {
                                retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
                            }
                            Psd2UIFormSettings.Instance.UIImagesOutputDir = retPath;
                            Psd2UIFormSettings.Save();
                        }
                        GUIUtility.ExitGUI();
                    }
                    EditorGUILayout.EndHorizontal();
                }

                EditorGUILayout.BeginHorizontal();
                {
                    Psd2UIFormSettings.Instance.UseUIFormOutputDir = EditorGUILayout.ToggleLeft("使用UIForm导出路径:", Psd2UIFormSettings.Instance.UseUIFormOutputDir, GUILayout.Width(150));
                    EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.UseUIFormOutputDir);
                    {
                        Psd2UIFormSettings.Instance.UIFormOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormOutputDir);
                        if (GUILayout.Button("选择路径", GUILayout.Width(80)))
                        {
                            var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormOutputDir, null);
                            if (!string.IsNullOrWhiteSpace(retPath))
                            {
                                if (!retPath.StartsWith("Assets/"))
                                {
                                    retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
                                }
                                Psd2UIFormSettings.Instance.UIFormOutputDir = retPath;
                                Psd2UIFormSettings.Save();
                            }
                            GUIUtility.ExitGUI();
                        }
                        EditorGUI.EndDisabledGroup();
                    }
                    EditorGUILayout.EndHorizontal();
                }
                //EditorGUILayout.BeginHorizontal();
                //{
                //    Psd2UIFormSettings.Instance.AutoCreateUIFormScript = EditorGUILayout.ToggleLeft("生成UIForm代码:", Psd2UIFormSettings.Instance.AutoCreateUIFormScript, GUILayout.Width(150));
                //    EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.AutoCreateUIFormScript);
                //    {
                //        Psd2UIFormSettings.Instance.UIFormScriptOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormScriptOutputDir);
                //        if (GUILayout.Button("选择路径", GUILayout.Width(80)))
                //        {
                //            var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormScriptOutputDir, null);
                //            if (!string.IsNullOrWhiteSpace(retPath))
                //            {
                //                if (!retPath.StartsWith("Assets/"))
                //                {
                //                    retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
                //                }
                //                Psd2UIFormSettings.Instance.UIFormScriptOutputDir = retPath;
                //                Psd2UIFormSettings.Save();
                //            }
                //            GUIUtility.ExitGUI();
                //        }
                //        EditorGUI.EndDisabledGroup();
                //    }
                //    EditorGUILayout.EndHorizontal();
                //}
                EditorGUILayout.EndVertical();
            }


            EditorGUILayout.BeginHorizontal();
            {
                if (GUILayout.Button(parsePsd2NodesBt, btHeight))
                {
                    Psd2UIFormConverter.ParsePsd2LayerPrefab(targetLogic.PsdAssetName, targetLogic);
                }
                if (GUILayout.Button(exportUISpritesBt, btHeight))
                {
                    targetLogic.ExportSprites();
                }
                EditorGUILayout.EndHorizontal();
            }
            if (GUILayout.Button(generateUIFormBt, btHeight))
            {
                targetLogic.GenerateUIForm();
            }
            base.OnInspectorGUI();
        }
        public override bool HasPreviewGUI()
        {
            return targetLogic.BindPsdAsset != null;
        }
        public override void OnPreviewGUI(Rect r, GUIStyle background)
        {
            GUI.DrawTexture(r, targetLogic.BindPsdAsset.texture, ScaleMode.ScaleToFit);
            //base.OnPreviewGUI(r, background);
        }
    }
    /// <summary>
    /// Psd文件转成UIForm prefab
    /// </summary>
    [ExecuteInEditMode]
    [RequireComponent(typeof(SpriteRenderer))]
    public class Psd2UIFormConverter : MonoBehaviour
    {
        const string RecordLayerOperation = "Change Export Image";
        public static Psd2UIFormConverter Instance { get; private set; }
        [ReadOnlyField][SerializeField] public string psdAssetChangeTime;//文件修改时间标识
        [Tooltip("UIForm名字")][SerializeField] private string uiFormName;
        [Tooltip("关联的psd文件")][SerializeField] private UnityEngine.Sprite psdAsset;
        [Header("Debug:")][SerializeField] bool drawLayerRectGizmos = true;
        [SerializeField] UnityEngine.Color drawLayerRectGizmosColor = UnityEngine.Color.green;

        private PsdImage psdInstance;//psd文件解析实例
        private GUIStyle uiTypeLabelStyle;
        public string PsdAssetName => psdAsset != null ? AssetDatabase.GetAssetPath(psdAsset) : null;
        public UnityEngine.Sprite BindPsdAsset => psdAsset;
        public Vector2Int UIFormCanvasSize { get; private set; } = new Vector2Int(750, 1334);
        private void OnEnable()
        {
            Instance = this;
            uiTypeLabelStyle = new GUIStyle();
            uiTypeLabelStyle.fontSize = 13;
            uiTypeLabelStyle.fontStyle = UnityEngine.FontStyle.BoldAndItalic;
            UnityEngine.ColorUtility.TryParseHtmlString("#7ED994", out var color);
            uiTypeLabelStyle.normal.textColor = color;

            EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;

            if (psdInstance == null && !string.IsNullOrWhiteSpace(PsdAssetName))
            {
                RefreshNodesBindLayer();
            }
        }

        private void Start()
        {
            if (this.CheckPsdAssetHasChanged())
            {
                if (EditorUtility.DisplayDialog("PSD -> UIForm", $"{gameObject.name}关联的psd文件[{this.PsdAssetName}]已改变,是否重新解析节点树?", "是", "否"))
                {
                    if (Psd2UIFormConverter.ParsePsd2LayerPrefab(this.PsdAssetName, this))
                    {
                        RefreshNodesBindLayer();
                    }

                }
            }
            else
            {
                RefreshNodesBindLayer();
            }
        }

        private void OnDrawGizmos()
        {
            if (drawLayerRectGizmos)
            {
                var nodes = this.GetComponentsInChildren<PsdLayerNode>();
                Gizmos.color = drawLayerRectGizmosColor;
                foreach (var item in nodes)
                {
                    if (item.NeedExportImage())
                    {
                        Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);
                    }

                }
            }
        }

        private void OnHierarchyGUI(int instanceID, Rect selectionRect)
        {
            if (Event.current == null) return;
            var node = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
            if (node == null || node == this.gameObject) return;
            if (!node.TryGetComponent<PsdLayerNode>(out var layer)) return;

            Rect tmpRect = selectionRect;
            tmpRect.x = 35;
            tmpRect.width = 10;
            Undo.RecordObject(layer, RecordLayerOperation);
            EditorGUI.BeginChangeCheck();
            {
                layer.markToExport = EditorGUI.Toggle(tmpRect, layer.markToExport);
                if (EditorGUI.EndChangeCheck())
                {
                    if (Selection.gameObjects.Length > 1) SetExportImageTg(Selection.gameObjects, layer.markToExport);
                    EditorUtility.SetDirty(layer);
                }
            }
            tmpRect.width = Mathf.Clamp(selectionRect.xMax * 0.2f, 100, 200);
            tmpRect.x = selectionRect.xMax - tmpRect.width;

            //EditorGUI.LabelField(tmpRect, layer.UIType.ToString(), uiTypeLabelStyle);
            if (EditorGUI.DropdownButton(tmpRect, new GUIContent(layer.UIType.ToString()), FocusType.Passive))
            {
                var dropdownMenu = PopEnumMenu<GUIType>(layer.UIType, selectUIType =>
                {
                    layer.SetUIType(selectUIType);
                    EditorUtility.SetDirty(layer);
                });

                dropdownMenu.ShowAsContext();
            }
        }
        private GenericMenu PopEnumMenu<T>(T currentValue, Action<T> onSelectEnum) where T : Enum
        {
            var names = Enum.GetValues(typeof(T));
            var dropdownMenu = new GenericMenu();
            foreach (T item in names)
            {
                dropdownMenu.AddItem(new GUIContent(item.ToString()), item.Equals(currentValue), () => { onSelectEnum(item); });
            }
            return dropdownMenu;
        }
        /// <summary>
        /// 批量勾选导出图片
        /// </summary>
        /// <param name="selects"></param>
        /// <param name="exportImg"></param>
        private void SetExportImageTg(GameObject[] selects, bool exportImg)
        {
            var selectLayerNodes = selects.Where(item => item?.GetComponent<PsdLayerNode>() != null).ToArray();
            foreach (var layer in selectLayerNodes)
            {
                layer.GetComponent<PsdLayerNode>().markToExport = exportImg;
            }
        }
        private void OnDestroy()
        {
            EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyGUI;
            if (this.psdInstance != null && !psdInstance.Disposed)
            {
                psdInstance.Dispose();
            }
        }

        private void RefreshNodesBindLayer()
        {
            if (psdInstance == null || psdInstance.Disposed)
            {
                if (!File.Exists(PsdAssetName))
                {
                    Debug.LogError($"刷新节点绑定图层失败! psd文件不存在");
                    return;
                }
                var psdOpts = new PsdLoadOptions()
                {
                    LoadEffectsResource = true,
                    ReadOnlyMode = false,
                };
                psdInstance = Aspose.PSD.Image.Load(PsdAssetName, psdOpts) as PsdImage;
                UIFormCanvasSize.Set(psdInstance.Size.Width, psdInstance.Size.Height);
            }
            var layers = GetComponentsInChildren<PsdLayerNode>(true);
            foreach (var layer in layers)
            {
                layer.InitPsdLayers(psdInstance);
            }

            var spRender = gameObject.GetOrAddComponent<SpriteRenderer>();
            spRender.sprite = this.psdAsset;
        }
        #region
        
        const string AsposeLicenseKey = "此处为Aspose.PSD证书";
        static bool licenseInitiated = false;


        [InitializeOnLoadMethod]
        static void InitAsposeLicense()
        {
            if (licenseInitiated) return;
            var harmonyHook = new Harmony("Crack.Aspose");
            harmonyHook.PatchAll();

            new Aspose.PSD.License().SetLicense(new MemoryStream(Convert.FromBase64String(AsposeLicenseKey)));
            licenseInitiated = true;
            harmonyHook.UnpatchAll();

            //GetAllLayerType();
        }
        static void GetAllLayerType()
        {
            var psdLib = Utility.Assembly.GetAssemblies().FirstOrDefault(item => item.GetName().Name == "Aspose.PSD");
            var layers = psdLib.GetTypes().Where(tp => tp.IsSubclassOf(typeof(Layer)) && !tp.IsAbstract);
            string layerEnumNames = "";
            foreach (var item in layers)
            {
                layerEnumNames += $"{item.Name},\n";
            }
            Debug.Log(layerEnumNames);
        }
        #endregion Aspose License
        [MenuItem("Assets/GF Editor Tool/Psd2UIForm Editor", priority = 0)]
        static void Psd2UIFormPrefabMenu()
        {
            if (Selection.activeObject == null) return;
            var assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
            if (Path.GetExtension(assetPath).ToLower().CompareTo(".psd") != 0)
            {
                Debug.LogWarning($"选择的文件({assetPath})不是psd格式, 工具只支持psd转换为UIForm");
                return;
            }
            string psdLayerPrefab = GetPsdLayerPrefabPath(assetPath);
            if (!File.Exists(psdLayerPrefab))
            {
                if (ParsePsd2LayerPrefab(assetPath))
                {
                    OpenPsdLayerEditor(psdLayerPrefab);
                }
            }
            else
            {
                OpenPsdLayerEditor(psdLayerPrefab);
            }
        }

        public bool CheckPsdAssetHasChanged()
        {
            if (psdAsset == null) return false;
            var fileTag = GetAssetChangeTag(PsdAssetName);
            return psdAssetChangeTime.CompareTo(fileTag) != 0;
        }
        public static string GetAssetChangeTag(string fileName)
        {
            return new FileInfo(fileName).LastWriteTime.ToString("yyyyMMddHHmmss");
        }
        /// <summary>
        /// 打开psd图层信息prefab
        /// </summary>
        /// <param name="psdLayerPrefab"></param>
        public static void OpenPsdLayerEditor(string psdLayerPrefab)
        {
            PrefabStageUtility.OpenPrefab(psdLayerPrefab);
        }
        /// <summary>
        /// 把Psd图层解析成节点prefab
        /// </summary>
        /// <param name="psdPath"></param>
        /// <returns></returns>
        public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null)
        {
            if (!File.Exists(psdFile))
            {
                Debug.LogError($"Error: Psd文件不存在:{psdFile}");
                return false;
            }
            var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;
            if (texImporter.textureType != TextureImporterType.Sprite)
            {
                texImporter.textureType = TextureImporterType.Sprite;
                texImporter.mipmapEnabled = false;
                texImporter.alphaIsTransparency = true;
                texImporter.SaveAndReimport();
            }

            var prefabFile = GetPsdLayerPrefabPath(psdFile);
            var rootName = Path.GetFileNameWithoutExtension(prefabFile);

            bool needDestroyInstance = instanceRoot == null;
            if (instanceRoot != null)
            {
                ParsePsdLayer2Root(psdFile, instanceRoot);
                instanceRoot.RefreshNodesBindLayer();
                return true;
            }
            else
            {
                Psd2UIFormConverter rootLayer = CreatePsdLayerRoot(rootName);
                rootLayer.SetPsdAsset(psdFile);
                ParsePsdLayer2Root(psdFile, rootLayer);

                PrefabUtility.SaveAsPrefabAsset(rootLayer.gameObject, prefabFile, out bool savePrefabSuccess);
                if (needDestroyInstance) GameObject.DestroyImmediate(rootLayer.gameObject);
                AssetDatabase.Refresh();
                if (savePrefabSuccess && AssetDatabase.GUIDFromAssetPath(StageUtility.GetCurrentStage().assetPath) != AssetDatabase.GUIDFromAssetPath(prefabFile))
                {
                    PrefabStageUtility.OpenPrefab(prefabFile);
                }

                return savePrefabSuccess;
            }
        }
        private static void ParsePsdLayer2Root(string psdFile, Psd2UIFormConverter converter)
        {
            //清空已有节点重新解析
            for (int i = converter.transform.childCount - 1; i >= 0; i--)
            {
                GameObject.DestroyImmediate(converter.transform.GetChild(i).gameObject);
            }

            var psdOpts = new PsdLoadOptions()
            {
                LoadEffectsResource = true,
                ReadOnlyMode = false
            };
            using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage)
            {
                List<GameObject> layerNodes = new List<GameObject> { converter.gameObject };

                for (int i = 0; i < psd.Layers.Length; i++)
                {
                    var layer = psd.Layers[i];
                    var curLayerType = layer.GetLayerType();

                    if (curLayerType == PsdLayerType.SectionDividerLayer)
                    {
                        var layerGroup = (layer as SectionDividerLayer).GetRelatedLayerGroup();
                        var layerGroupIdx = ArrayUtility.IndexOf(psd.Layers, layerGroup);
                        var layerGropNode = CreatePsdLayerNode(layerGroup, layerGroupIdx);
                        layerNodes.Add(layerGropNode.gameObject);
                    }
                    else if (curLayerType == PsdLayerType.LayerGroup)
                    {
                        var lastLayerNode = layerNodes.Last();
                        layerNodes.Remove(lastLayerNode);

                        if (layerNodes.Count > 0)
                        {
                            var parentLayerNode = layerNodes.Last();
                            lastLayerNode.transform.SetParent(parentLayerNode.transform);
                        }
                    }
                    else
                    {
                        var newLayerNode = CreatePsdLayerNode(layer, i);
                        newLayerNode.transform.SetParent(layerNodes.Last().transform);
                        newLayerNode.transform.localPosition = Vector3.zero;
                    }
                }
            }
            converter.psdAssetChangeTime = GetAssetChangeTag(psdFile);
            var childrenNodes = converter.GetComponentsInChildren<PsdLayerNode>(true);
            foreach (var item in childrenNodes)
            {
                item.RefreshUIHelper(false);
            }
            EditorUtility.SetDirty(converter.gameObject);
        }
        private void SetPsdAsset(string psdFile)
        {
            this.psdAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(psdFile);
            if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIImagesOutputDir))
            {
                Psd2UIFormSettings.Instance.UIImagesOutputDir = Path.GetDirectoryName(psdFile);
            }
            if (string.IsNullOrWhiteSpace(this.uiFormName))
            {
                this.uiFormName = this.psdAsset.name;
            }
        }

        /// <summary>
        /// 获取解析好的psd layers文件
        /// </summary>
        /// <param name="psd"></param>
        /// <returns></returns>
        public static string GetPsdLayerPrefabPath(string psd)
        {
            return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");
        }
        private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName)
        {
            var node = new GameObject(rootName);
            node.gameObject.tag = "EditorOnly";
            var layerRoot = node.AddComponent<Psd2UIFormConverter>();
            return layerRoot;
        }
        private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx)
        {
            string nodeName = layer.Name;
            if (string.IsNullOrWhiteSpace(nodeName))
            {
                nodeName = $"PsdLayer-{bindLayerIdx}";
            }
            else
            {
                if (Path.HasExtension(layer.Name))
                {
                    nodeName = Path.GetFileNameWithoutExtension(layer.Name);
                }
            }
            var node = new GameObject(nodeName);
            node.gameObject.tag = "EditorOnly";
            var layerNode = node.AddComponent<PsdLayerNode>();
            layerNode.BindPsdLayerIndex = bindLayerIdx;
            InitLayerNodeData(layerNode, layer);
            return layerNode;
        }
        /// <summary>
        /// 根据psd图层信息解析并初始化图层UI类型、是否导出等信息
        /// </summary>
        /// <param name="layerNode"></param>
        /// <param name="layer"></param>
        private static void InitLayerNodeData(PsdLayerNode layerNode, Layer layer)
        {
            if (layer == null || layer.Disposed) return;
            var layerTp = layer.GetLayerType();
            layerNode.BindPsdLayer = layer;
            if (UGUIParser.Instance.TryParse(layerNode, out var initRule))
            {
                layerNode.SetUIType(initRule.UIType, false);
            }
            layerNode.markToExport = layerTp != PsdLayerType.LayerGroup && !(layerTp == PsdLayerType.TextLayer && layerNode.UIType.ToString().EndsWith("Text") && layerNode.UIType != GUIType.FillColor);
            layerNode.gameObject.SetActive(layer.IsVisible);
        }

        /// <summary>
        /// 导出psd图层为Sprites碎图
        /// </summary>
        /// <param name="psdAssetName"></param>
        internal void ExportSprites()
        {
            //var pngOpts = new PngOptions()
            //{
            //    ColorType = Aspose.PSD.FileFormats.Png.PngColorType.Truecolor
            //};
            //this.psdInstance.Save("Assets/AAAGame/Sprites/UI/Preview.png", pngOpts);

            //return;
            var exportLayers = this.GetComponentsInChildren<PsdLayerNode>().Where(node => node.NeedExportImage());
            var exportDir = GetUIFormImagesOutputDir();
            if (!Directory.Exists(exportDir))
            {
                Directory.CreateDirectory(exportDir);
            }
            int exportIdx = 0;
            int totalCount = exportLayers.Count();
            foreach (var layer in exportLayers)
            {
                var assetName = layer.ExportImageAsset();
                if (assetName == null)
                {
                    Debug.LogWarning($"导出图层[name:{layer.name}, layerIdx:{layer.BindPsdLayerIndex}]图片失败!");
                }
                ++exportIdx;
                EditorUtility.DisplayProgressBar($"导出进度({exportIdx}/{totalCount})", $"导出UI图片:{assetName}", exportIdx / (float)totalCount);
            }
            EditorUtility.ClearProgressBar();
            AssetDatabase.Refresh();
        }
        /// <summary>
        /// 根据解析后的节点树生成UIForm Prefab
        /// </summary>
        internal void GenerateUIForm()
        {
            if (Psd2UIFormSettings.Instance.UseUIFormOutputDir && string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))
            {
                Debug.LogError($"生成UIForm失败! UIForm导出路径为空:{Psd2UIFormSettings.Instance.UIFormOutputDir}");
                return;
            }
            if (Psd2UIFormSettings.Instance.UseUIFormOutputDir)
            {
                ExportUIPrefab(Psd2UIFormSettings.Instance.UIFormOutputDir);
            }
            else
            {
                string lastSaveDir = string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.LastUIFormOutputDir) ? "Assets" : Psd2UIFormSettings.Instance.LastUIFormOutputDir;
                string selectDir = EditorUtility.SaveFolderPanel("保存目录", lastSaveDir, null);
                if (!string.IsNullOrWhiteSpace(selectDir))
                {
                    if (!selectDir.StartsWith("Assets/"))
                        selectDir = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, selectDir);
                    Psd2UIFormSettings.Instance.LastUIFormOutputDir = selectDir;
                    ExportUIPrefab(selectDir);
                }
            }
        }
        private bool ExportUIPrefab(string outputDir)
        {
            if (!string.IsNullOrWhiteSpace(outputDir))
            {
                if (!Directory.Exists(outputDir))
                {
                    try
                    {
                        Directory.CreateDirectory(outputDir);
                        AssetDatabase.Refresh();
                    }
                    catch (Exception err)
                    {
                        Debug.LogError($"导出UI prefab失败:{err.Message}");
                        return false;
                    }
                }
            }
            if (string.IsNullOrWhiteSpace(uiFormName))
            {
                Debug.LogError("导出UI Prefab失败: UI Form Name为空, 请填写UI Form Name.");
                return false;
            }
            var prefabName = UtilityBuiltin.ResPath.GetCombinePath(outputDir, $"{uiFormName}.prefab");
            if (File.Exists(prefabName))
            {
                if (!EditorUtility.DisplayDialog("警告", $"prefab文件已存在, 是否覆盖:{prefabName}", "覆盖生成", "取消生成"))
                {
                    return false;
                }
            }
            var uiHelpers = GetAvailableUIHelpers();
            if (uiHelpers == null || uiHelpers.Length < 1)
            {
                return false;
            }
            var uiFormRoot = GameObject.Instantiate(UGUIParser.Instance.UIFormTemplate, Vector3.zero, Quaternion.identity);
            uiFormRoot.name = uiFormName;

            int curIdx = 0;
            int totalCount = uiHelpers.Length;
            foreach (var uiHelper in uiHelpers)
            {
                EditorUtility.DisplayProgressBar($"生成UIFrom:({curIdx++}/{totalCount})", $"正在生成UI元素:{uiHelper.name}", curIdx /
                (float)totalCount);
                var uiElement = uiHelper.CreateUI();
                if (uiElement == null) continue;

                var goPath = GetGameObjectInstanceIdPath(uiHelper.gameObject, out var goNames);
                var parentNode = GetOrCreateNodeByInstanceIdPath(uiFormRoot, goPath, goNames);
                uiElement.transform.SetParent(parentNode.transform, true);

                uiElement.transform.position += new Vector3(this.UIFormCanvasSize.x * 0.5f, this.UIFormCanvasSize.y * 0.5f, 0);
            }
            var uiStrKeys = uiFormRoot.GetComponentsInChildren<UIStringKey>(true);
            for (int i = uiStrKeys.Length - 1; i >= 0; i--)
            {
                DestroyImmediate(uiStrKeys[i]);
            }
            
            var uiPrefab = PrefabUtility.SaveAsPrefabAsset(uiFormRoot, prefabName, out bool saveSuccess);
            if (saveSuccess)
            {
                DestroyImmediate(uiFormRoot);
                Selection.activeGameObject = uiPrefab;
            }
            EditorUtility.ClearProgressBar();
            return true;
        }

        private GameObject GetOrCreateNodeByInstanceIdPath(GameObject uiFormRoot, string[] goPath, string[] goNames)
        {
            GameObject result = uiFormRoot;
            if (goPath != null && goNames != null)
            {
                for (int i = 0; i < goPath.Length; i++)
                {
                    var nodeId = goPath[i];
                    var nodeName = goNames[i];
                    GameObject targetNode = null;
                    foreach (Transform child in result.transform)
                    {
                        if (child.gameObject == result) continue;

                        var idKey = child.GetComponent<UIStringKey>();
                        if (idKey != null && nodeId == idKey.Key)
                        {
                            targetNode = child.gameObject;
                            break;
                        }
                    }
                    if (targetNode == null)
                    {
                        targetNode = new GameObject(nodeName);
                        targetNode.transform.SetParent(result.transform, false);
                        targetNode.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
                        var targetNodeKey = targetNode.GetOrAddComponent<UIStringKey>();
                        targetNodeKey.Key = nodeId;
                    }
                    result = targetNode;
                }
            }
            return result;
        }

        private string[] GetGameObjectInstanceIdPath(GameObject go, out string[] names)
        {
            names = null;
            if (go == null || go.transform.parent == null || go.transform.parent == this.transform) return null;

            var parentGo = go.transform.parent;
            string[] result = new string[1] { parentGo.gameObject.GetInstanceID().ToString() };
            names = new string[1] { parentGo.gameObject.name };
            while (parentGo.parent != null && parentGo.parent != this.transform)
            {
                ArrayUtility.Insert(ref result, 0, parentGo.parent.gameObject.GetInstanceID().ToString());
                ArrayUtility.Insert(ref names, 0, parentGo.parent.gameObject.name);
                parentGo = parentGo.parent;
            }
            return result;
        }
        private UIHelperBase[] GetAvailableUIHelpers()
        {
            var uiHelpers = this.GetComponentsInChildren<UIHelperBase>();
            uiHelpers = uiHelpers.Where(ui => ui.LayerNode.IsMainUIType).ToArray();

            List<int> dependInstIds = new List<int>();
            foreach (var item in uiHelpers)
            {
                foreach (var depend in item.GetDependencies())
                {
                    int dependId = depend.gameObject.GetInstanceID();
                    if (!dependInstIds.Contains(dependId))
                    {
                        dependInstIds.Add(dependId);
                    }
                }
            }
            for (int i = uiHelpers.Length - 1; i >= 0; i--)
            {
                var uiHelper = uiHelpers[i];
                if (dependInstIds.Contains(uiHelper.gameObject.GetInstanceID()))
                {
                    ArrayUtility.RemoveAt(ref uiHelpers, i);
                }
            }
            return uiHelpers;
        }
        /// <summary>
        /// 把图片设置为为Sprite或Texture类型
        /// </summary>
        /// <param name="dir"></param>
        public static void ConvertTexturesType(string[] texAssets, bool isImage = true)
        {
            foreach (var item in texAssets)
            {
                var texImporter = AssetImporter.GetAtPath(item) as TextureImporter;
                if (texImporter == null)
                {
                    Debug.LogError($"TextureImporter为空:{item}");
                    continue;
                }
                if (isImage)
                {
                    texImporter.textureType = TextureImporterType.Sprite;
                    texImporter.spriteImportMode = SpriteImportMode.Single;
                    texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
                    texImporter.alphaIsTransparency = true;
                    texImporter.mipmapEnabled = false;
                }
                else
                {
                    texImporter.textureType = TextureImporterType.Default;
                    texImporter.textureShape = TextureImporterShape.Texture2D;
                    texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
                    texImporter.alphaIsTransparency = true;
                    texImporter.mipmapEnabled = false;
                }

                texImporter.SaveAndReimport();
            }
        }

        /// <summary>
        /// 压缩图片文件
        /// </summary>
        /// <param name="asset">文件名(相对路径Assets)</param>
        /// <returns></returns>
        public static bool CompressImageFile(string asset)
        {
            var assetPath = asset.StartsWith("Assets/") ? Path.GetFullPath(asset, Directory.GetParent(Application.dataPath).FullName) : asset;
            var compressTool = Utility.Assembly.GetType("UGF.EditorTools.CompressTool");
            if (compressTool == null) return false;

            var compressMethod = compressTool.GetMethod("CompressImageOffline", new Type[] { typeof(string), typeof(string) });
            if (compressMethod == null) return false;

            return (bool)compressMethod.Invoke(null, new object[] { assetPath, assetPath });
        }
        /// <summary>
        /// 获取UIForm对应的图片导出目录
        /// </summary>
        /// <returns></returns>
        public string GetUIFormImagesOutputDir()
        {
            return UtilityBuiltin.ResPath.GetCombinePath(Psd2UIFormSettings.Instance.UIImagesOutputDir, uiFormName);
        }

        public SmartObjectLayer ConvertToSmartObjectLayer(Layer layer)
        {
            var smartObj = psdInstance.SmartObjectProvider.ConvertToSmartObject(new Layer[] { layer });
            return smartObj;
        }
    }
}
#endif

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

#if UNITY_EDITOR
using UnityEngine;
using Aspose.PSD.FileFormats.Psd.Layers;
using Aspose.PSD.ImageOptions;
using UnityEditor;
using System.IO;
using System.Linq;
using Aspose.PSD.FileFormats.Psd;
using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
using GameFramework;

namespace UGF.EditorTools.Psd2UGUI
{
    [CanEditMultipleObjects]
    [CustomEditor(typeof(PsdLayerNode))]
    public class PsdLayerNodeInspector : Editor
    {
        PsdLayerNode targetLogic;
        private void OnEnable()
        {
            targetLogic = target as PsdLayerNode;
            targetLogic.RefreshLayerTexture();
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();
            base.OnInspectorGUI();
            EditorGUI.BeginChangeCheck();
            {
                targetLogic.UIType = (GUIType)EditorGUILayout.EnumPopup("UI Type", targetLogic.UIType);
                if (EditorGUI.EndChangeCheck())
                {
                    targetLogic.SetUIType(targetLogic.UIType);
                }
            }
            EditorGUILayout.BeginHorizontal();
            {
                if (GUILayout.Button("导出图片"))
                {
                    foreach (var item in targets)
                    {
                        if (item == null) continue;

                        (item as PsdLayerNode)?.ExportImageAsset();
                    }
                }
                EditorGUILayout.EndHorizontal();
            }
            serializedObject.ApplyModifiedProperties();
        }

        public override bool HasPreviewGUI()
        {
            var layerNode = (target as PsdLayerNode);
            return layerNode != null && layerNode.PreviewTexture != null;
        }

        public override void OnPreviewGUI(Rect r, GUIStyle background)
        {
            var layerNode = (target as PsdLayerNode);
            GUI.DrawTexture(r, layerNode.PreviewTexture, ScaleMode.ScaleToFit);
            //base.OnPreviewGUI(r, background);
        }
        public override string GetInfoString()
        {
            var layerNode = (target as PsdLayerNode);
            return layerNode.LayerInfo;
        }
    }
    [ExecuteInEditMode]
    [DisallowMultipleComponent]
    public class PsdLayerNode : MonoBehaviour
    {
        [ReadOnlyField] public int BindPsdLayerIndex = -1;
        [ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;
        [SerializeField] public bool markToExport;
        [HideInInspector] public GUIType UIType;
        public Texture2D PreviewTexture { get; private set; }
        public string LayerInfo { get; private set; }
        public Rect LayerRect { get; private set; }
        public PsdLayerType LayerType { get => mLayerType; }
        public bool IsMainUIType => UGUIParser.IsMainUIType(UIType);
        /// <summary>
        /// 绑定的psd图层
        /// </summary>
        private Layer mBindPsdLayer;

        public Layer BindPsdLayer
        {
            get => mBindPsdLayer;
            set
            {
                mBindPsdLayer = value;
                mLayerType = mBindPsdLayer.GetLayerType();
                //if (IsTextLayer(out var txtLayer) && !txtLayer.TextBoundBox.IsEmpty)
                //{
                //    LayerRect = AsposePsdExtension.PsdRect2UnityRect(txtLayer.TextBoundBox, Psd2UIFormConverter.Instance.UIFormCanvasSize);
                //}
                //else
                {
                    LayerRect = mBindPsdLayer.GetLayerRect();
                }
                LayerInfo = $"{LayerRect}";
            }
        }
        private void OnDestroy()
        {
            if (PreviewTexture != null)
            {
                DestroyImmediate(PreviewTexture);
            }
        }
        public void SetUIType(GUIType uiType, bool triggerParseFunc = true)
        {
            this.UIType = uiType;
            RemoveUIHelper();

            if (triggerParseFunc)
            {
                RefreshUIHelper(true);
            }
        }
        public void RefreshUIHelper(bool refreshParent = false)
        {
            if (UIType == GUIType.Null) return;

            var uiHelperTp = UGUIParser.Instance.GetHelperType(UIType);
            if (uiHelperTp != null)
            {
                var helper = gameObject.GetOrAddComponent(uiHelperTp) as UIHelperBase;
                helper.ParseAndAttachUIElements();
            }
            if (refreshParent)
            {
                var parentHelper = transform.parent?.GetComponent<UIHelperBase>();
                parentHelper?.ParseAndAttachUIElements();
            }
            EditorUtility.SetDirty(this);
        }
        private void RemoveUIHelper()
        {
            var uiHelpers = this.GetComponents<UIHelperBase>();
            if (uiHelpers != null)
            {
                foreach (var uiHelper in uiHelpers)
                {
                    DestroyImmediate(uiHelper);
                }
            }
            EditorUtility.SetDirty(this);
        }
        /// <summary>
        /// 是否需要导出此图层
        /// </summary>
        /// <returns></returns>
        public bool NeedExportImage()
        {
            return gameObject.activeSelf && markToExport;
        }
        /// <summary>
        /// 导出图片
        /// </summary>
        /// <param name="forceSpriteType">强制贴图类型为Sprite</param>
        /// <returns></returns>
        public string ExportImageAsset(bool forceSpriteType = false)
        {
            string assetName = null;
            if (this.RefreshLayerTexture())
            {
                var bytes = PreviewTexture.EncodeToPNG();
                var imgName = Utility.Text.Format("{0}_{1}", string.IsNullOrWhiteSpace(name) ? UIType : name, BindPsdLayerIndex);
                var exportDir = Psd2UIFormConverter.Instance.GetUIFormImagesOutputDir();
                if (!Directory.Exists(exportDir))
                {
                    try
                    {
                        Directory.CreateDirectory(exportDir);
                        AssetDatabase.Refresh();
                    }
                    catch (System.Exception)
                    {
                        return null;
                    }
                }
                var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");
                File.WriteAllBytes(imgFileName, bytes);
                if (Psd2UIFormSettings.Instance.CompressImage)
                {
                    bool compressResult = Psd2UIFormConverter.CompressImageFile(imgFileName);
                    if (compressResult)
                    {
                        Debug.Log($"成功压缩图片:{imgFileName}");
                    }
                    else
                    {
                        Debug.LogWarning($"压缩图片失败:{imgFileName}");
                    }
                }
                assetName = imgFileName;
                bool isImage = !(this.UIType == GUIType.FillColor || this.UIType == GUIType.RawImage);
                AssetDatabase.Refresh();
                Psd2UIFormConverter.ConvertTexturesType(new string[] { imgFileName }, isImage || forceSpriteType);
            }

            return assetName;
        }
        public bool RefreshLayerTexture(bool forceRefresh = false)
        {
            if (!forceRefresh && PreviewTexture != null)
            {
                return true;
            }

            if (BindPsdLayer == null || BindPsdLayer.Disposed) return false;

            var pngOpt = new PngOptions
            {
                ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha
            };
            if (BindPsdLayer.CanSave(pngOpt))
            {
                if (PreviewTexture != null)
                {
                    DestroyImmediate(PreviewTexture);
                }
                PreviewTexture = this.ConvertPsdLayer2Texture2D();
            }
            return PreviewTexture != null;
        }

        /// <summary>
        /// 把psd图层转成Texture2D
        /// </summary>
        /// <param name="psdLayer"></param>
        /// <returns>Texture2D</returns>
        public Texture2D ConvertPsdLayer2Texture2D()
        {
            if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;

            MemoryStream ms = new MemoryStream();
            var pngOpt = new Aspose.PSD.ImageOptions.PngOptions()
            {
                ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha,
                FullFrame = true
            };
            if (BindPsdLayer.Opacity >= 255 || LayerType == PsdLayerType.LayerGroup)
            {
                BindPsdLayer.Save(ms, pngOpt);
            }
            else
            {
                var smartLayer = Psd2UIFormConverter.Instance.ConvertToSmartObjectLayer(BindPsdLayer);
                smartLayer.Save(ms, pngOpt);
            }

            //var bitmap = BindPsdLayer.ToBitmap();
            //bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);

            var buffer = new byte[ms.Length];
            ms.Position = 0;
            ms.Read(buffer, 0, buffer.Length);

            Texture2D texture = new Texture2D(BindPsdLayer.Width, BindPsdLayer.Height);
            texture.alphaIsTransparency = true;
            texture.LoadImage(buffer);
            texture.Apply();
            ms.Dispose();
            return texture;
        }

        /// <summary>
        /// 从第一层子节点按类型查找LayerNode
        /// </summary>
        /// <param name="uiTp"></param>
        /// <returns></returns>
        public PsdLayerNode FindSubLayerNode(GUIType uiTp)
        {
            for (int i = 0; i < transform.childCount; i++)
            {
                var child = transform.GetChild(i)?.GetComponent<PsdLayerNode>();

                if (child != null && child.UIType == uiTp) return child;
            }
            return null;
        }
        /// <summary>
        /// 依次查找给定多个类型,返回最先找到的类型
        /// </summary>
        /// <param name="uiTps"></param>
        /// <returns></returns>
        public PsdLayerNode FindSubLayerNode(params GUIType[] uiTps)
        {
            foreach (var tp in uiTps)
            {
                var result = FindSubLayerNode(tp);
                if (result != null) return result;
            }
            return null;
        }
        public PsdLayerNode FindLayerNodeInChildren(GUIType uiTp)
        {
            var layers = GetComponentsInChildren<PsdLayerNode>(true);
            if (layers != null && layers.Length > 0)
            {
                return layers.FirstOrDefault(layer => layer.UIType == uiTp);
            }
            return null;
        }

        /// <summary>
        /// 判断该图层是否为文本图层
        /// </summary>
        /// <param name="layer"></param>
        /// <returns></returns>
        public bool IsTextLayer(out TextLayer layer)
        {
            layer = null;
            if (BindPsdLayer == null) return false;

            if (BindPsdLayer is SmartObjectLayer smartLayer)
            {
                layer = smartLayer.GetSmartObjectInnerTextLayer() as TextLayer;
                return layer != null;
            }
            else if (BindPsdLayer is TextLayer txtLayer)
            {
                layer = txtLayer;
                return layer != null;
            }
            return false;
        }
        internal void InitPsdLayers(PsdImage psdInstance)
        {
            BindPsdLayer = psdInstance.Layers[BindPsdLayerIndex];
        }
        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)
        {
            text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;
            if (IsTextLayer(out var txtLayer))
            {
                text = txtLayer.Text;
                fontSize = (int)txtLayer.Font.Size;
                fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
                {
                    fontStyle = UnityEngine.FontStyle.BoldAndItalic;
                }
                else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
                {
                    fontStyle = UnityEngine.FontStyle.Bold;

                }
                else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
                {
                    fontStyle = UnityEngine.FontStyle.Italic;
                }
                else
                {
                    fontStyle = UnityEngine.FontStyle.Normal;
                }

                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
                {
                    tmpFontStyle |= TMPro.FontStyles.Italic;
                }
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
                {
                    tmpFontStyle |= TMPro.FontStyles.Bold;
                }
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))
                {
                    tmpFontStyle |= TMPro.FontStyles.Underline;
                }
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))
                {
                    tmpFontStyle |= TMPro.FontStyles.Strikethrough;
                }
                fontName = txtLayer.Font.Name;
                if (txtLayer.TextData.Items.Length > 0)
                {
                    var txtData = txtLayer.TextData.Items[0];
                    characterSpace = txtData.Style.Tracking * 0.1f;
                    lineSpace = (float)txtData.Style.Leading * 0.1f;
                }
                return true;
            }
            return false;
        }
    }
}

#endif

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

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

#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityGameFramework.Runtime;

namespace UGF.EditorTools.Psd2UGUI
{
    public abstract class UIHelperBase : MonoBehaviour
    {
        public PsdLayerNode LayerNode => this.GetComponent<PsdLayerNode>();
        private void OnEnable()
        {
            ParseAndAttachUIElements();
        }
        /// <summary>
        /// 解析并关联UI元素,并且返回已经关联过的图层(已关联图层不再处理)
        /// </summary>
        /// <param name="layerNode"></param>
        /// <returns></returns>
        public abstract void ParseAndAttachUIElements();

        /// <summary>
        /// 获取UI依赖的LayerNodes
        /// </summary>
        /// <returns></returns>
        public abstract PsdLayerNode[] GetDependencies();
        /// <summary>
        /// 把UI实例进行UI元素初始化
        /// </summary>
        /// <param name="uiRoot"></param>
        protected abstract void InitUIElements(GameObject uiRoot);
        /// <summary>
        /// 筛选出UI依赖的非空LayerNode
        /// </summary>
        /// <param name="nodes"></param>
        /// <returns></returns>
        protected PsdLayerNode[] CalculateDependencies(params PsdLayerNode[] nodes)
        {
            if (nodes == null || nodes.Length == 0) return null;

            for (int i = nodes.Length - 1; i >= 0; i--)
            {
                var node = nodes[i];
                if (node == null || node == LayerNode) ArrayUtility.RemoveAt(ref nodes, i);
            }
            return nodes;
        }
        internal GameObject CreateUI(GameObject uiInstance = null)
        {
            if ((int)this.LayerNode.UIType > UGUIParser.UITYPE_MAX || LayerNode.UIType == GUIType.Null) return null;

            if (uiInstance == null)
            {
                var rule = UGUIParser.Instance.GetRule(this.LayerNode.UIType);
                if (rule == null || rule.UIPrefab == null)
                {
                    Debug.LogWarning($"创建UI类型{LayerNode.UIType}失败:Rule配置项不存在或UIPrefab为空");
                    return null;
                }
                uiInstance = GameObject.Instantiate(rule.UIPrefab, Vector3.zero, Quaternion.identity);
                if (LayerNode.IsMainUIType)
                {
                    uiInstance.name = this.name;
                    var key = uiInstance.GetOrAddComponent<UIStringKey>();
                    key.Key = this.gameObject.GetInstanceID().ToString();
                }
            }
            
            InitUIElements(uiInstance);
            return uiInstance;
        }

    }
}
#endif

1. Text解析器:

#if UNITY_EDITOR
using UnityEngine;

namespace UGF.EditorTools.Psd2UGUI
{
    [DisallowMultipleComponent]
    public class TextHelper : UIHelperBase
    {
        [SerializeField] PsdLayerNode text;

        public override PsdLayerNode[] GetDependencies()
        {
            return CalculateDependencies(text);
        }

        public override void ParseAndAttachUIElements()
        {
            if (LayerNode.IsTextLayer(out var _))
            {
                text = LayerNode;
            }
            else
            {
                LayerNode.SetUIType(UGUIParser.Instance.DefaultImage);
            }
        }

        protected override void InitUIElements(GameObject uiRoot)
        {
            var textCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Text>();
            UGUIParser.SetTextStyle(text, textCom);

            UGUIParser.SetRectTransform(text, textCom);
        }
    }
}
#endif

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

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)
        {
            text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;
            if (IsTextLayer(out var txtLayer))
            {
                text = txtLayer.Text;
                fontSize = (int)txtLayer.Font.Size;
                fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
                {
                    fontStyle = UnityEngine.FontStyle.BoldAndItalic;
                }
                else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
                {
                    fontStyle = UnityEngine.FontStyle.Bold;

                }
                else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
                {
                    fontStyle = UnityEngine.FontStyle.Italic;
                }
                else
                {
                    fontStyle = UnityEngine.FontStyle.Normal;
                }

                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
                {
                    tmpFontStyle |= TMPro.FontStyles.Italic;
                }
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
                {
                    tmpFontStyle |= TMPro.FontStyles.Bold;
                }
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))
                {
                    tmpFontStyle |= TMPro.FontStyles.Underline;
                }
                if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))
                {
                    tmpFontStyle |= TMPro.FontStyles.Strikethrough;
                }
                fontName = txtLayer.Font.Name;
                if (txtLayer.TextData.Items.Length > 0)
                {
                    var txtData = txtLayer.TextData.Items[0];
                    characterSpace = txtData.Style.Tracking * 0.1f;
                    lineSpace = (float)txtData.Style.Leading * 0.1f;
                }
                return true;
            }
            return false;
        }

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

/// <summary>
        /// 根据字体名查找TMP_FontAsset
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns></returns>
        public static TMP_FontAsset FindTMPFontAsset(string fontName)
        {
            var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");
            foreach (var guid in fontGuids)
            {
                var fontPath = AssetDatabase.GUIDToAssetPath(guid);
                var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);
                if (font != null && font.faceInfo.familyName == fontName)
                {
                    return font;
                }
            }
            return null;
        }
        /// <summary>
        /// 根据字体名查找Font Asset
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns></returns>
        public static UnityEngine.Font FindFontAsset(string fontName)
        {
            var fontGuids = AssetDatabase.FindAssets("t:font");
            foreach (var guid in fontGuids)
            {
                var fontPath = AssetDatabase.GUIDToAssetPath(guid);
                var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;
                if (font != null && font.fontTTFName == fontName)
                {
                    return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);
                }
            }
            return null;
        }

2. Image解析器:

#if UNITY_EDITOR
using UnityEngine;

namespace UGF.EditorTools.Psd2UGUI
{
    [DisallowMultipleComponent]
    public class ImageHelper : UIHelperBase
    {
        [SerializeField] PsdLayerNode image;

        public override PsdLayerNode[] GetDependencies()
        {
            return CalculateDependencies(image);
        }

        public override void ParseAndAttachUIElements()
        {
            image = LayerNode;
        }

        protected override void InitUIElements(GameObject uiRoot)
        {
            var imgCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Image>();
            UGUIParser.SetRectTransform(image,imgCom);
            imgCom.sprite = UGUIParser.LayerNode2Sprite(image, imgCom.type == UnityEngine.UI.Image.Type.Sliced);
        }
    }
}
#endif

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

/// <summary>
        /// 把LayerNode图片保存到本地并返回
        /// </summary>
        /// <param name="layerNode"></param>
        /// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>
        /// <returns></returns>
        public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)
        {
            if (layerNode != null)
            {
                var spAssetName = layerNode.ExportImageAsset(true);
                var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);
                if (sprite != null)
                {
                    if (auto9Slice)
                    {
                        var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;
                        var rawReadable = spImpt.isReadable;
                        if (!rawReadable)
                        {
                            spImpt.isReadable = true;
                            spImpt.SaveAndReimport();
                        }
                        if (spImpt.spriteBorder == Vector4.zero)
                        {
                            spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);
                            spImpt.isReadable = rawReadable;
                            spImpt.SaveAndReimport();
                        }
                    }
                    return sprite;
                }
            }
            return null;
        }

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

/// <summary>
        /// 自动计算贴图的 9宫 Border
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="alphaThreshold">0-255</param>
        /// <returns></returns>
        public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)
        {
            int width = texture.width;
            int height = texture.height;

            Color32[] pixels = texture.GetPixels32();
            int minX = width;
            int minY = height;
            int maxX = 0;
            int maxY = 0;

            // 寻找不透明像素的最小和最大边界
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int pixelIndex = y * width + x;
                    Color32 pixel = pixels[pixelIndex];

                    if (pixel.a >= alphaThreshold)
                    {
                        minX = Mathf.Min(minX, x);
                        minY = Mathf.Min(minY, y);
                        maxX = Mathf.Max(maxX, x);
                        maxY = Mathf.Max(maxY, y);
                    }
                }
            }

            // 计算最优的borderSize
            int borderSizeX = (maxX - minX) / 3;
            int borderSizeY = (maxY - minY) / 3;
            int borderSize = Mathf.Min(borderSizeX, borderSizeY);

            // 根据边界和Border Size计算Nine Slice Border
            int left = minX + borderSize;
            int right = maxX - borderSize;
            int top = minY + borderSize;
            int bottom = maxY - borderSize;

            // 确保边界在纹理范围内
            left = Mathf.Clamp(left, 0, width - 1);
            right = Mathf.Clamp(right, 0, width - 1);
            top = Mathf.Clamp(top, 0, height - 1);
            bottom = Mathf.Clamp(bottom, 0, height - 1);

            return new Vector4(left, top, width - right, height - bottom);
        }

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

#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.UI;

namespace UGF.EditorTools.Psd2UGUI
{
    [DisallowMultipleComponent]
    public class DropdownHelper : UIHelperBase
    {
        [SerializeField] PsdLayerNode background;
        [SerializeField] PsdLayerNode label;
        [SerializeField] PsdLayerNode arrow;
        [SerializeField] PsdLayerNode scrollView;
        [SerializeField] PsdLayerNode toggleItem;
        public override PsdLayerNode[] GetDependencies()
        {
            return CalculateDependencies(background, label, arrow, scrollView, toggleItem);
        }

        public override void ParseAndAttachUIElements()
        {
            background = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
            label = LayerNode.FindSubLayerNode(GUIType.Dropdown_Label, GUIType.Text, GUIType.TMPText);
            arrow = LayerNode.FindSubLayerNode(GUIType.Dropdown_Arrow);
            scrollView = LayerNode.FindSubLayerNode(GUIType.ScrollView);
            toggleItem = LayerNode.FindSubLayerNode(GUIType.Toggle);
        }

        protected override void InitUIElements(GameObject uiRoot)
        {
            var dpd = uiRoot.GetComponent<Dropdown>();
            UGUIParser.SetRectTransform(background, dpd);
            var bgImg = dpd.targetGraphic as Image;
            bgImg.sprite = UGUIParser.LayerNode2Sprite(background, bgImg.type == Image.Type.Sliced) ?? bgImg.sprite;

            UGUIParser.SetTextStyle(label, dpd.captionText);
            UGUIParser.SetRectTransform(label, dpd.captionText);
            var arrowImg = dpd.transform.Find("Arrow")?.GetComponent<Image>();
            if (arrowImg != null)
            {
                UGUIParser.SetRectTransform(arrow, arrowImg);
                arrowImg.sprite = UGUIParser.LayerNode2Sprite(arrow, arrowImg.type == Image.Type.Sliced);
            }
            if (scrollView != null)
            {
                var svTmp = uiRoot.GetComponentInChildren<ScrollRect>(true).GetComponent<RectTransform>();
                if (svTmp != null)
                {
                    var sViewGo = scrollView.GetComponent<ScrollViewHelper>()?.CreateUI(svTmp.gameObject);
                    if (sViewGo != null)
                    {
                        var sViewRect = sViewGo.GetComponent<RectTransform>();
                        UGUIParser.SetRectTransform(scrollView, sViewRect);
                        sViewRect.anchorMin = Vector2.zero;
                        sViewRect.anchorMax = new Vector2(1, 0);
                        sViewRect.anchoredPosition = new Vector2(0, -2);
                    }
                    if (toggleItem != null)
                    {
                        var itemTmp = dpd.itemText != null ? dpd.itemText.transform.parent : null;
                        if (itemTmp != null)
                        {
                            toggleItem.GetComponent<ToggleHelper>()?.CreateUI(itemTmp.gameObject);
                        }
                    }
                }
            }
        }
    }
}
#endif

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/702716.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea双击打不开怎么办?

今天在敲代码的时候&#xff0c;突然出现了一个bug&#xff0c;idea打不开了&#xff0c;这个问题怎么解决&#xff1f; 只要找到C:\Users\Administrator .IntelliJIdea2019.3这个文件把所有文件都给删了就可以重启了 哈哈&#xff0c;重启成功

Bayes贝叶斯定理

问题的关键在于&#xff1a;人们是否考虑过大背景/先验/问题的前提&#xff0c;从而做出一个大致的估计。这就引出了我们关于理性的探讨&#xff0c;理性不是说知道事实&#xff0c;而是认识到哪些因素是有关的。 x.1 一个关于贝叶斯定理的例子 引入一个steve假设。我们已知大…

开源进展 | WeIdentity v3.1.1发布,提供无存储依赖的纯功能接口

作为连接实体对象&#xff08;人或物&#xff09;的现实身份与链上身份的可信映射&#xff0c;实现实体对象之间安全可信的数据授权与交换&#xff0c;分布式身份技术解决方案在推动区块链应用繁荣及可信数据流转的过程中扮演着重要角色。 WeIdentity是由微众银行自主研发并完全…

parcel打包工具搭建热开发项目环境

parcel是一款WEB端打包工具 能够提供热开发的项目环境 使用了的话不然Webpack 但相对搭建项目会更快一些 我们现在本地创建一个目录 然后 用编辑器打开我们创建的目录 运行终端 在终端中输入 npm init 初始化一个项目 运行完毕之后 我们就会得到一个package.json文件 然后…

Spring Boot 中的 RedisCacheManager 是什么,原理,如何使用

Spring Boot 中的 RedisCacheManager 是什么&#xff0c;原理&#xff0c;如何使用 介绍 在现代应用程序中&#xff0c;缓存是提高应用程序性能的重要组成部分。Spring Boot 提供了一个强大的缓存框架&#xff0c;它支持多种缓存提供程序&#xff0c;包括 Redis、Ehcache、Ca…

国金QMT量化交易系统的Bug及应对策略

国金QMT量化交易系统中的 账号成交状态变化主推 deal_callback() &#xff0c; 当账号成交状态有变化时&#xff0c;这个函数被客户端调用。 我的策略是&#xff0c;在handlebar()里面挂单&#xff0c;等待成交&#xff0c;而判断成交的方式是根据系统主推deal_callback()通知…

数据结构与算法:查找、排序、动态规划、数学

1 查找表 查找表是同一数据类型构成的集合。只进行查找操作的称为静态查找表&#xff1b;在查找的同时进行插入和删除操作的称为动态查找表。 查找算法衡量好坏的依据为&#xff1a;查找成功时&#xff0c;查找的关键字和查找表中比较过的数据元素的个数的平均值&#xff0c;…

MKS SERVO4257D 闭环步进电机_系列10 arduino 例程

第1部分 产品介绍 MKS SERVO 28D/35D/42D/57D 系列闭环步进电机是创客基地为满足市场需求而自主研发的一款产品。具备脉冲接口和RS485/CAN串行接口&#xff0c;支持MODBUS-RTU通讯协议&#xff0c;内置高效FOC矢量算法&#xff0c;采用高精度编码器&#xff0c;通过位置反馈&a…

HiEV独家|余承东力推L3标准,华为ADS更高阶产品将至

作者 | 张祥威 编辑 | 德新 L3标准出台提速&#xff0c;智驾江湖的厮杀将更加惨烈。 近日&#xff0c;多位接近华为的人士告诉HiEV&#xff0c;余承东正在力推自动驾驶L3标准尽快出台&#xff0c;华为的多位技术专家深度参与了L3标准制定。 本月稍早前&#xff0c;余承东在重庆…

计算机网络-数据链路层下篇

目录 计算机网络 七、MAC地址&#xff0c;IP地址及ARP地址 &#xff08;一&#xff09;MAC地址 &#xff08;二&#xff09;IP地址 &#xff08;三&#xff09;ARP地址 八、集线器和交换机的区别 九、以太网交换机自学习和转发帧的流程 十、以太网交换机的生成树协议ST…

一步一步学OAK之十三:实现RGB相机上的空间对象跟踪

前面我们实现了在RGB相机上进行物体的对象跟踪&#xff0c;能够实时跟踪我们想要追踪的物探&#xff0c;但是&#xff0c;如果我们要想知道这个物体的三维空间坐标&#xff0c;该如何实现呢&#xff1f;要想实现这个功能&#xff0c;我们需要用到DepthAI API提供的MobileNetSpa…

无人机动力测试台-50公斤级-Flight Stand 50

Flight Stand 50测试台通过测量电机和螺旋桨的拉力、扭矩、转速、电流、电压、温度、螺旋桨效率和电机效率来精准地描述和评估无人机动力系统的性能。 产品应用 Flight Stand 50测试台可以用于以下方向&#xff1a; 实时动态测试 FS50 Pro的1000 Hz采样率使测试成为可能&am…

使用 OAT 工具替换 OceanBase 云平台节点

OceanBase 环境基本都会先安装 OCP 来部署、监控、运维数据库集群。但如果有机器过保等问题&#xff0c;就需要有平稳的 OCP 节点的替换方案。 作者&#xff1a;张瑞远 上海某公司 DBA&#xff0c;曾经从事银行、证券数仓设计、开发、优化类工作&#xff0c;现主要从事电信级 I…

android:DataPicker控件使用

一、前言&#xff1a;我真的服了&#xff0c;刚开始再发布运行的时候一直报这个错误“ Attempt to invoke virtual method void android.widget.TextView.setText(java.lang.CharSequence) on a null object reference”说空指针。我也上网查了&#xff0c;网上说在这个错误不是…

mysql行数据转为列数据

最近在开发过程中遇到问题&#xff0c;需要将数据库中一张表信息进行行转列操作&#xff0c;再将每列&#xff08;即每个字段&#xff09;作为与其他表进行联表查询的字段进行显示。 借此机会&#xff0c;在网上查阅了相关方法&#xff0c;现总结出一种比较简单易懂的方法备用…

BI商业智能工具改变企业发展态势

BI商业智能工具在当今企业环境中扮演着越来越重要的角色&#xff0c;成为企业实现高速增长的关键因素之一。这些工具能够帮助企业应对海量数据挑战&#xff0c;提供更高效的数据处理和分析能力&#xff0c;为企业决策提供有力支持。以瓴羊Quick BI为例&#xff0c;它凭借其强大…

记事本软件误删后如何找回?

随着智能手机的普及&#xff0c;各种优秀的手机软件层出不穷&#xff0c;成为我们生活和工作中的得力助手。其中&#xff0c;记事本软件在手机上的应用也越来越受欢迎。 一款记事本可以给用户带来许多便利和帮助。与传统的纸质记事本相比&#xff0c;手机记事本具有更多的功能…

Java Spring多线程

Java Spring多线程 开启一个线程1 继承java.lang.Thread类2 实现java.lang.Runnable接口3 实现Callable接口4 实现线程池ThreadPoolExecutor Java线程池Executors 的类型Future与线程池 开启一个线程 https://blog.csdn.net/qq_44715943/article/details/116714584 1 继承java.…

React hooks文档笔记(二) 添加交互性

添加交互性 1. 事件传播1.1 停止传播1.2 阻止默认事件 2. [Hook] useState 状态3. 渲染和提交3.1 触发渲染3.2 React渲染组件3.3 提交对 DOM 的更改3.4 浏览器绘制 4. 渲染快照状态队列例子 5. 更新state中的对象 1. 事件传播 js的事件流&#xff1a; 事件捕获&#xff1a;从…

Spring学习(二)(Spring创建和使用)

经过前⾯的学习我们已经知道了&#xff0c;Spring 就是⼀个包含了众多⼯具⽅法的 IoC 容器。既然是容器那么 它就具备两个最基本的功能&#xff1a; 将对象(Bean)存储到容器&#xff08;Spring&#xff09;中&#xff1b; 从容器中将对象取出来。那么该怎么将Bean存储的Spring以…