引言

在移动互联网时代,跨平台开发已成为主流趋势。Egret Engine作为一款成熟的HTML5游戏引擎,其EUI(Egret UI)系统为开发者提供了强大的界面构建能力。本指南将带你从零开始,逐步构建一个高效、可维护的跨平台界面系统。

一、Egret EUI基础概念

1.1 什么是Egret EUI?

Egret EUI是Egret Engine的UI框架,基于Egret 2D渲染引擎构建。它提供了:

  • 可视化编辑器:Egret Wing(现已整合到Egret Editor中)
  • 组件化开发:丰富的UI组件库
  • 数据绑定:MVVM模式支持
  • 跨平台渲染:一次开发,多端运行(Web、iOS、Android、微信小游戏等)

1.2 核心优势

  1. 高性能渲染:基于Canvas/WebGL的渲染机制
  2. 声明式UI:类似React的组件化思想
  3. 响应式布局:自动适配不同屏幕尺寸
  4. 热更新支持:无需重新打包即可更新UI

二、环境搭建与项目初始化

2.1 开发环境准备

# 1. 安装Node.js (推荐14.x以上版本)
# 下载地址: https://nodejs.org/

# 2. 安装Egret CLI
npm install -g @egret/cli

# 3. 验证安装
egret --version

# 4. 创建新项目
egret create my-eui-project
cd my-eui-project

# 5. 安装依赖
npm install

2.2 项目结构解析

my-eui-project/
├── src/                    # 源代码目录
│   ├── Main.ts            # 主入口文件
│   ├── components/        # 自定义组件
│   ├── views/             # 界面视图
│   ├── models/            # 数据模型
│   └── utils/             # 工具类
├── resource/              # 资源目录
│   ├── assets/            # 图片、音频等
│   └── skins/             # UI皮肤文件
├── libs/                  # 第三方库
├── egretProperties.json   # 项目配置
└── tsconfig.json          # TypeScript配置

2.3 启动开发服务器

# 启动开发服务器(热更新模式)
egret run

# 构建生产环境
egret build --release

# 清理构建产物
egret clean

三、EUI核心组件详解

3.1 基础组件

3.1.1 Group容器组件

// 创建一个简单的Group容器
class MyGroup extends eui.Group {
    public constructor() {
        super();
        this.width = 300;
        this.height = 200;
        this.backgroundColor = 0x336699;
        
        // 添加子组件
        this.addChild(this.createLabel());
        this.addChild(this.createButton());
    }
    
    private createLabel(): eui.Label {
        let label = new eui.Label();
        label.text = "欢迎使用Egret EUI";
        label.textColor = 0xFFFFFF;
        label.horizontalCenter = 0;
        label.verticalCenter = 0;
        return label;
    }
    
    private createButton(): eui.Button {
        let btn = new eui.Button();
        btn.label = "点击我";
        btn.y = 100;
        btn.horizontalCenter = 0;
        btn.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onButtonClick, this);
        return btn;
    }
    
    private onButtonClick(): void {
        console.log("按钮被点击了!");
    }
}

3.1.2 List组件(数据列表)

// 自定义List项渲染器
class MyListItemRenderer extends eui.ItemRenderer {
    private label: eui.Label;
    private icon: eui.Image;
    
    public constructor() {
        super();
        this.width = 200;
        this.height = 60;
        this.touchChildren = true;
        
        // 创建UI
        this.icon = new eui.Image();
        this.icon.width = 50;
        this.icon.height = 50;
        this.icon.x = 5;
        this.icon.y = 5;
        this.addChild(this.icon);
        
        this.label = new eui.Label();
        this.label.x = 60;
        this.label.y = 20;
        this.label.textColor = 0x333333;
        this.addChild(this.label);
    }
    
    // 数据绑定
    protected dataChanged(): void {
        if (this.data) {
            this.label.text = this.data.name;
            this.icon.source = this.data.icon;
        }
    }
}

// 使用List组件
class MyListPage extends eui.Component {
    private list: eui.List;
    private dataProvider: eui.ArrayCollection;
    
    public constructor() {
        super();
        this.createUI();
    }
    
    private createUI(): void {
        // 创建List
        this.list = new eui.List();
        this.list.width = 220;
        this.list.height = 300;
        this.list.itemRenderer = MyListItemRenderer;
        this.list.y = 20;
        this.addChild(this.list);
        
        // 设置数据源
        this.dataProvider = new eui.ArrayCollection([
            { name: "项目1", icon: "icon1.png" },
            { name: "项目2", icon: "icon2.png" },
            { name: "项目3", icon: "icon3.png" },
            { name: "项目4", icon: "icon4.png" }
        ]);
        this.list.dataProvider = this.dataProvider;
        
        // 添加点击事件
        this.list.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onListItemClick, this);
    }
    
    private onListItemClick(event: egret.Event): void {
        let item = event.target as eui.List;
        if (item.selectedItem) {
            console.log("选中了:", item.selectedItem.name);
        }
    }
}

3.2 数据绑定与MVVM模式

3.2.1 基础数据绑定

// 数据模型
class UserModel {
    public name: string = "";
    public age: number = 0;
    public email: string = "";
    
    // 可观察属性
    private _isVIP: boolean = false;
    public get isVIP(): boolean {
        return this._isVIP;
    }
    public set isVIP(value: boolean) {
        if (this._isVIP !== value) {
            this._isVIP = value;
            // 触发属性变化事件
            this.dispatchEventWith("propertyChange", false, { property: "isVIP" });
        }
    }
}

// 视图组件
class UserProfileView extends eui.Component {
    private nameInput: eui.TextInput;
    private ageInput: eui.TextInput;
    private emailInput: eui.TextInput;
    private vipLabel: eui.Label;
    private vipToggle: eui.ToggleSwitch;
    
    private userModel: UserModel;
    
    public constructor() {
        super();
        this.userModel = new UserModel();
        this.createUI();
        this.bindData();
    }
    
    private createUI(): void {
        // 创建输入框
        this.nameInput = new eui.TextInput();
        this.nameInput.prompt = "请输入姓名";
        this.nameInput.y = 20;
        this.addChild(this.nameInput);
        
        this.ageInput = new eui.TextInput();
        this.ageInput.prompt = "请输入年龄";
        this.ageInput.y = 60;
        this.addChild(this.ageInput);
        
        this.emailInput = new eui.TextInput();
        this.emailInput.prompt = "请输入邮箱";
        this.emailInput.y = 100;
        this.addChild(this.emailInput);
        
        // VIP标签和开关
        this.vipLabel = new eui.Label();
        this.vipLabel.text = "VIP会员:";
        this.vipLabel.y = 140;
        this.addChild(this.vipLabel);
        
        this.vipToggle = new eui.ToggleSwitch();
        this.vipToggle.y = 135;
        this.vipToggle.x = 60;
        this.addChild(this.vipToggle);
    }
    
    private bindData(): void {
        // 单向绑定:数据 -> UI
        this.nameInput.text = this.userModel.name;
        this.ageInput.text = this.userModel.age.toString();
        this.emailInput.text = this.userModel.email;
        this.vipToggle.selected = this.userModel.isVIP;
        
        // 双向绑定:UI -> 数据
        this.nameInput.addEventListener(egret.Event.CHANGE, () => {
            this.userModel.name = this.nameInput.text;
        });
        
        this.ageInput.addEventListener(egret.Event.CHANGE, () => {
            this.userModel.age = parseInt(this.ageInput.text) || 0;
        });
        
        this.emailInput.addEventListener(egret.Event.CHANGE, () => {
            this.userModel.email = this.emailInput.text;
        });
        
        this.vipToggle.addEventListener(egret.Event.CHANGE, () => {
            this.userModel.isVIP = this.vipToggle.selected;
            this.updateVIPDisplay();
        });
        
        // 监听模型变化
        this.userModel.addEventListener("propertyChange", this.onModelChange, this);
    }
    
    private onModelChange(event: egret.Event): void {
        if (event.data.property === "isVIP") {
            this.updateVIPDisplay();
        }
    }
    
    private updateVIPDisplay(): void {
        if (this.userModel.isVIP) {
            this.vipLabel.text = "VIP会员: 是";
            this.vipLabel.textColor = 0xFF6600;
        } else {
            this.vipLabel.text = "VIP会员: 否";
            this.vipLabel.textColor = 0x333333;
        }
    }
}

3.2.2 高级数据绑定(使用eui.Binding)

// 使用eui.Binding进行更灵活的数据绑定
class AdvancedBindingExample extends eui.Component {
    private dataModel: any;
    private label1: eui.Label;
    private label2: eui.Label;
    private label3: eui.Label;
    
    public constructor() {
        super();
        this.dataModel = {
            userName: "张三",
            userAge: 25,
            userLevel: "高级用户"
        };
        this.createUI();
        this.setupBindings();
    }
    
    private createUI(): void {
        this.label1 = new eui.Label();
        this.label1.y = 20;
        this.addChild(this.label1);
        
        this.label2 = new eui.Label();
        this.label2.y = 60;
        this.addChild(this.label2);
        
        this.label3 = new eui.Label();
        this.label3.y = 100;
        this.addChild(this.label3);
        
        // 添加一个按钮来测试数据变化
        let btn = new eui.Button();
        btn.label = "更新数据";
        btn.y = 140;
        btn.addEventListener(egret.TouchEvent.TOUCH_TAP, this.updateData, this);
        this.addChild(btn);
    }
    
    private setupBindings(): void {
        // 绑定到对象属性
        eui.Binding.bindProperty(this.dataModel, "userName", this.label1, "text");
        eui.Binding.bindProperty(this.dataModel, "userAge", this.label2, "text");
        
        // 绑定到计算属性
        eui.Binding.bindHandler(this.dataModel, "userLevel", this.updateLevelDisplay, this);
    }
    
    private updateLevelDisplay(value: string): void {
        this.label3.text = `等级: ${value}`;
        // 根据等级改变颜色
        if (value === "高级用户") {
            this.label3.textColor = 0xFF0000;
        } else {
            this.label3.textColor = 0x333333;
        }
    }
    
    private updateData(): void {
        // 更新数据,绑定会自动触发
        this.dataModel.userName = "李四" + Math.floor(Math.random() * 100);
        this.dataModel.userAge = 20 + Math.floor(Math.random() * 30);
        this.dataModel.userLevel = Math.random() > 0.5 ? "高级用户" : "普通用户";
    }
}

四、EUI皮肤系统

4.1 皮肤文件基础

EUI皮肤文件是JSON格式,定义了组件的外观和布局。

// skins/MyButtonSkin.json
{
    "name": "MyButtonSkin",
    "width": 120,
    "height": 40,
    "states": ["up", "down", "disabled"],
    "elements": [
        {
            "name": "bg",
            "type": "eui.Image",
            "source": "button_bg.png",
            "width": 120,
            "height": 40,
            "scale9Grid": "10,10,100,20"
        },
        {
            "name": "labelDisplay",
            "type": "eui.Label",
            "text": "按钮",
            "horizontalCenter": 0,
            "verticalCenter": 0,
            "textColor": 0xFFFFFF,
            "size": 16
        }
    ],
    "states": {
        "up": {
            "bg": { "source": "button_bg.png" }
        },
        "down": {
            "bg": { "source": "button_bg_down.png" }
        },
        "disabled": {
            "bg": { "source": "button_bg_disabled.png" },
            "labelDisplay": { "textColor": 0x999999 }
        }
    }
}

4.2 自定义皮肤组件

// 自定义按钮组件
class CustomButton extends eui.Button {
    private bg: eui.Image;
    private labelDisplay: eui.Label;
    
    public constructor() {
        super();
        this.createSkin();
    }
    
    private createSkin(): void {
        // 创建背景
        this.bg = new eui.Image();
        this.bg.source = "button_bg.png";
        this.bg.width = 120;
        this.bg.height = 40;
        this.bg.scale9Grid = new egret.Rectangle(10, 10, 100, 20);
        this.addChild(this.bg);
        
        // 创建标签
        this.labelDisplay = new eui.Label();
        this.labelDisplay.text = "自定义按钮";
        this.labelDisplay.horizontalCenter = 0;
        this.labelDisplay.verticalCenter = 0;
        this.labelDisplay.textColor = 0xFFFFFF;
        this.labelDisplay.size = 16;
        this.addChild(this.labelDisplay);
        
        // 设置状态
        this.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.onTouchBegin, this);
        this.addEventListener(egret.TouchEvent.TOUCH_END, this.onTouchEnd, this);
        this.addEventListener(egret.TouchEvent.TOUCH_RELEASE_OUTSIDE, this.onTouchEnd, this);
    }
    
    private onTouchBegin(): void {
        this.bg.source = "button_bg_down.png";
        this.scaleX = this.scaleY = 0.95;
    }
    
    private onTouchEnd(): void {
        this.bg.source = "button_bg.png";
        this.scaleX = this.scaleY = 1;
    }
    
    // 重写label属性
    public get label(): string {
        return this.labelDisplay.text;
    }
    
    public set label(value: string) {
        this.labelDisplay.text = value;
    }
}

4.3 主题与皮肤管理

// 主题管理器
class ThemeManager {
    private static instance: ThemeManager;
    private currentTheme: string = "default";
    
    public static getInstance(): ThemeManager {
        if (!ThemeManager.instance) {
            ThemeManager.instance = new ThemeManager();
        }
        return ThemeManager.instance;
    }
    
    // 切换主题
    public switchTheme(themeName: string): void {
        this.currentTheme = themeName;
        
        // 加载对应主题的皮肤
        let skinConfig = this.getSkinConfig(themeName);
        
        // 应用到所有UI组件
        this.applyThemeToAllComponents(skinConfig);
        
        // 保存用户偏好
        this.saveUserPreference();
    }
    
    private getSkinConfig(themeName: string): any {
        // 从资源管理器加载皮肤配置
        return RES.getRes(`skins/${themeName}_config.json`);
    }
    
    private applyThemeToAllComponents(skinConfig: any): void {
        // 遍历所有已创建的UI组件
        // 这里可以使用Egret的全局事件系统来通知组件更新
        egret.Event.dispatchEvent("themeChanged", false, skinConfig);
    }
    
    private saveUserPreference(): void {
        // 保存到本地存储
        egret.localStorage.setItem("theme", this.currentTheme);
    }
    
    // 初始化主题
    public init(): void {
        let savedTheme = egret.localStorage.getItem("theme");
        if (savedTheme) {
            this.switchTheme(savedTheme);
        }
    }
}

五、响应式布局与适配

5.1 基础适配方案

// 屏幕适配管理器
class ScreenAdapter {
    private static instance: ScreenAdapter;
    private designWidth: number = 750;  // 设计稿宽度
    private designHeight: number = 1334; // 设计稿高度
    
    public static getInstance(): ScreenAdapter {
        if (!ScreenAdapter.instance) {
            ScreenAdapter.instance = new ScreenAdapter();
        }
        return ScreenAdapter.instance;
    }
    
    // 初始化适配
    public init(): void {
        // 设置Egret的缩放模式
        egret.StageDelegate.getInstance().setDesignSize(
            this.designWidth, 
            this.designHeight
        );
        
        // 监听屏幕尺寸变化
        window.addEventListener("resize", this.onResize.bind(this));
        this.onResize();
    }
    
    private onResize(): void {
        let stage = egret.MainContext.instance.stage;
        let screenWidth = window.innerWidth;
        let screenHeight = window.innerHeight;
        
        // 计算缩放比例
        let scaleX = screenWidth / this.designWidth;
        let scaleY = screenHeight / this.designHeight;
        let scale = Math.min(scaleX, scaleY);
        
        // 应用缩放
        stage.scaleX = stage.scaleY = scale;
        
        // 居中显示
        stage.x = (screenWidth - this.designWidth * scale) / 2;
        stage.y = (screenHeight - this.designHeight * scale) / 2;
        
        // 触发自定义事件
        egret.Event.dispatchEvent("screenResize", false, {
            width: screenWidth,
            height: screenHeight,
            scale: scale
        });
    }
    
    // 获取适配后的尺寸
    public getAdaptedSize(originalWidth: number, originalHeight: number): { width: number, height: number } {
        let stage = egret.MainContext.instance.stage;
        let scale = stage.scaleX;
        
        return {
            width: originalWidth * scale,
            height: originalHeight * scale
        };
    }
    
    // 获取安全区域(避开刘海屏等)
    public getSafeArea(): { top: number, bottom: number, left: number, right: number } {
        // 这里可以根据平台获取安全区域
        // 在微信小游戏等平台有专门的API
        if (typeof wx !== "undefined" && wx.getSystemInfoSync) {
            let systemInfo = wx.getSystemInfoSync();
            return {
                top: systemInfo.safeArea.top,
                bottom: systemInfo.safeArea.bottom,
                left: systemInfo.safeArea.left,
                right: systemInfo.safeArea.right
            };
        }
        
        return { top: 0, bottom: 0, left: 0, right: 0 };
    }
}

5.2 响应式组件示例

// 响应式容器组件
class ResponsiveContainer extends eui.Group {
    private minWidth: number = 300;
    private maxWidth: number = 800;
    private baseWidth: number = 400;
    
    public constructor() {
        super();
        this.setupResponsive();
    }
    
    private setupResponsive(): void {
        // 监听屏幕变化
        egret.Event.addEventListener("screenResize", this.onScreenResize, this);
        
        // 初始设置
        this.updateLayout();
    }
    
    private onScreenResize(event: egret.Event): void {
        this.updateLayout();
    }
    
    private updateLayout(): void {
        let stage = egret.MainContext.instance.stage;
        let screenWidth = stage.stageWidth;
        
        // 根据屏幕宽度调整容器宽度
        let containerWidth = Math.max(
            this.minWidth,
            Math.min(this.maxWidth, screenWidth * 0.8)
        );
        
        this.width = containerWidth;
        this.horizontalCenter = 0;
        
        // 调整内部组件布局
        this.adjustChildrenLayout();
    }
    
    private adjustChildrenLayout(): void {
        // 根据容器宽度调整子组件
        let childCount = this.numChildren;
        let availableWidth = this.width;
        
        for (let i = 0; i < childCount; i++) {
            let child = this.getChildAt(i);
            if (child instanceof eui.Button) {
                // 按钮自适应宽度
                let btn = child as eui.Button;
                btn.width = availableWidth / 2 - 10;
            }
        }
    }
}

六、性能优化策略

6.1 渲染优化

// 渲染优化管理器
class RenderOptimizer {
    private static instance: RenderOptimizer;
    private dirtyComponents: Set<egret.DisplayObject> = new Set();
    private isRendering: boolean = false;
    
    public static getInstance(): RenderOptimizer {
        if (!RenderOptimizer.instance) {
            RenderOptimizer.instance = new RenderOptimizer();
        }
        return RenderOptimizer.instance;
    }
    
    // 标记组件需要重绘
    public markDirty(component: egret.DisplayObject): void {
        if (!this.isRendering) {
            this.dirtyComponents.add(component);
            this.scheduleRender();
        }
    }
    
    private scheduleRender(): void {
        if (this.isRendering) return;
        
        this.isRendering = true;
        // 使用requestAnimationFrame进行批量渲染
        requestAnimationFrame(() => {
            this.renderDirtyComponents();
            this.isRendering = false;
        });
    }
    
    private renderDirtyComponents(): void {
        this.dirtyComponents.forEach(component => {
            if (component && component.stage) {
                // 触发重绘
                component.dispatchEventWith(egret.Event.RENDER, false);
            }
        });
        this.dirtyComponents.clear();
    }
    
    // 对象池管理(减少GC压力)
    private objectPools: Map<string, any[]> = new Map();
    
    public getFromPool<T>(type: new () => T): T {
        let typeName = type.name;
        let pool = this.objectPools.get(typeName);
        
        if (pool && pool.length > 0) {
            return pool.pop();
        }
        
        return new type();
    }
    
    public returnToPool<T>(obj: T): void {
        let typeName = (obj as any).constructor.name;
        if (!this.objectPools.has(typeName)) {
            this.objectPools.set(typeName, []);
        }
        this.objectPools.get(typeName).push(obj);
    }
}

6.2 资源管理优化

// 智能资源管理器
class SmartResourceManager {
    private loadedAssets: Map<string, any> = new Map();
    private loadingQueue: string[] = [];
    private priorityQueue: string[] = [];
    
    // 按需加载资源
    public async loadAsset(url: string, priority: number = 0): Promise<any> {
        // 检查是否已加载
        if (this.loadedAssets.has(url)) {
            return this.loadedAssets.get(url);
        }
        
        // 添加到加载队列
        if (priority > 0) {
            this.priorityQueue.push(url);
        } else {
            this.loadingQueue.push(url);
        }
        
        // 开始加载
        return this.processLoadQueue();
    }
    
    private async processLoadQueue(): Promise<any> {
        // 优先加载高优先级资源
        while (this.priorityQueue.length > 0) {
            let url = this.priorityQueue.shift();
            await this.loadSingleAsset(url);
        }
        
        // 然后加载普通资源
        while (this.loadingQueue.length > 0) {
            let url = this.loadingQueue.shift();
            await this.loadSingleAsset(url);
        }
    }
    
    private async loadSingleAsset(url: string): Promise<any> {
        try {
            let asset = await RES.getResAsync(url);
            this.loadedAssets.set(url, asset);
            return asset;
        } catch (error) {
            console.error(`加载资源失败: ${url}`, error);
            throw error;
        }
    }
    
    // 预加载常用资源
    public preloadCommonAssets(): void {
        let commonAssets = [
            "skins/default.json",
            "assets/common/button.png",
            "assets/common/icon.png"
        ];
        
        commonAssets.forEach(url => {
            this.loadAsset(url, 1);
        });
    }
    
    // 卸载未使用资源
    public unloadUnusedAssets(): void {
        let usedUrls = this.getUsedUrls();
        
        this.loadedAssets.forEach((asset, url) => {
            if (!usedUrls.has(url)) {
                this.loadedAssets.delete(url);
                // 释放资源
                RES.destroyRes(url);
            }
        });
    }
    
    private getUsedUrls(): Set<string> {
        // 这里可以根据实际使用情况收集已使用的URL
        // 可以通过监听组件创建/销毁来记录
        return new Set();
    }
}

七、跨平台部署

7.1 平台特定配置

// 平台检测与配置
class PlatformManager {
    private static platform: string = "web";
    private static isWeChat: boolean = false;
    private static isNative: boolean = false;
    
    public static init(): void {
        // 检测平台
        if (typeof wx !== "undefined" && wx.getSystemInfoSync) {
            this.platform = "wechat";
            this.isWeChat = true;
        } else if (typeof window !== "undefined" && window.electron) {
            this.platform = "electron";
        } else if (typeof android !== "undefined" || typeof ios !== "undefined") {
            this.platform = "native";
            this.isNative = true;
        } else {
            this.platform = "web";
        }
        
        console.log(`当前平台: ${this.platform}`);
    }
    
    public static getPlatform(): string {
        return this.platform;
    }
    
    public static isWeChatPlatform(): boolean {
        return this.isWeChat;
    }
    
    public static isNativePlatform(): boolean {
        return this.isNative;
    }
    
    // 平台特定功能
    public static shareAppMessage(title: string, imageUrl: string, query: string = ""): void {
        if (this.isWeChat) {
            wx.shareAppMessage({
                title: title,
                imageUrl: imageUrl,
                query: query
            });
        } else {
            console.log("分享功能仅在微信平台可用");
        }
    }
    
    // 获取系统信息
    public static getSystemInfo(): any {
        if (this.isWeChat) {
            return wx.getSystemInfoSync();
        }
        
        return {
            screenWidth: window.innerWidth,
            screenHeight: window.innerHeight,
            pixelRatio: window.devicePixelRatio || 1
        };
    }
}

7.2 构建配置

// egretProperties.json 配置示例
{
    "egret_version": "5.2.33",
    "modules": [
        {
            "name": "eui",
            "path": "libs/eui"
        },
        {
            "name": "dragonBones",
            "path": "libs/dragonBones"
        }
    ],
    "platform": [
        "web",
        "wxgame",
        "android",
        "ios"
    ],
    "publish": {
        "web": {
            "outputDir": "publish/web",
            "command": "build"
        },
        "wxgame": {
            "outputDir": "publish/wxgame",
            "command": "build --target wxgame"
        },
        "android": {
            "outputDir": "publish/android",
            "command": "build --target android"
        }
    }
}

八、实战项目:跨平台商城界面

8.1 项目结构

shopping-mall/
├── src/
│   ├── Main.ts
│   ├── components/
│   │   ├── ProductCard.ts
│   │   ├── CategoryList.ts
│   │   └── CartButton.ts
│   ├── views/
│   │   ├── HomeView.ts
│   │   ├── ProductListView.ts
│   │   └── CartView.ts
│   ├── models/
│   │   ├── ProductModel.ts
│   │   └── CartModel.ts
│   └── services/
│       ├── ProductService.ts
│       └── CartService.ts
├── resource/
│   ├── skins/
│   │   ├── mall/
│   │   │   ├── ProductCardSkin.json
│   │   │   └── CartButtonSkin.json
│   │   └── default/
│   └── assets/
│       ├── images/
│       └── fonts/

8.2 核心组件实现

// 商品卡片组件
class ProductCard extends eui.Component {
    private product: any;
    private nameLabel: eui.Label;
    private priceLabel: eui.Label;
    private image: eui.Image;
    private addButton: eui.Button;
    
    public constructor(productData: any) {
        super();
        this.product = productData;
        this.createUI();
        this.bindEvents();
    }
    
    private createUI(): void {
        // 设置皮肤
        this.skinName = "skins/mall/ProductCardSkin.json";
        
        // 获取组件引用
        this.nameLabel = this.getChildByName("nameLabel") as eui.Label;
        this.priceLabel = this.getChildByName("priceLabel") as eui.Label;
        this.image = this.getChildByName("productImage") as eui.Image;
        this.addButton = this.getChildByName("addButton") as eui.Button;
        
        // 设置数据
        this.updateDisplay();
    }
    
    private updateDisplay(): void {
        this.nameLabel.text = this.product.name;
        this.priceLabel.text = `¥${this.product.price}`;
        this.image.source = this.product.image;
    }
    
    private bindEvents(): void {
        this.addButton.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onAddToCart, this);
        this.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onProductClick, this);
    }
    
    private onAddToCart(): void {
        // 添加到购物车
        CartService.getInstance().addProduct(this.product);
        
        // 显示提示
        this.showAddAnimation();
    }
    
    private onProductClick(): void {
        // 跳转到商品详情
        this.dispatchEventWith("productSelected", true, this.product);
    }
    
    private showAddAnimation(): void {
        // 简单的添加动画
        let btn = this.addButton;
        egret.Tween.get(btn)
            .to({ scaleX: 1.2, scaleY: 1.2 }, 100)
            .to({ scaleX: 1, scaleY: 1 }, 100);
    }
}

// 购物车按钮组件
class CartButton extends eui.Component {
    private countLabel: eui.Label;
    private badge: eui.Group;
    
    public constructor() {
        super();
        this.createUI();
        this.bindEvents();
    }
    
    private createUI(): void {
        this.skinName = "skins/mall/CartButtonSkin.json";
        
        this.countLabel = this.getChildByName("countLabel") as eui.Label;
        this.badge = this.getChildByName("badge") as eui.Group;
        
        // 初始状态
        this.updateBadge();
    }
    
    private bindEvents(): void {
        this.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onCartClick, this);
        
        // 监听购物车变化
        CartService.getInstance().addEventListener("cartChanged", this.onCartChanged, this);
    }
    
    private onCartClick(): void {
        // 打开购物车视图
        this.dispatchEventWith("openCart", true);
    }
    
    private onCartChanged(event: egret.Event): void {
        this.updateBadge();
    }
    
    private updateBadge(): void {
        let count = CartService.getInstance().getTotalCount();
        
        if (count > 0) {
            this.badge.visible = true;
            this.countLabel.text = count.toString();
            
            // 如果数量超过99,显示99+
            if (count > 99) {
                this.countLabel.text = "99+";
            }
        } else {
            this.badge.visible = false;
        }
    }
}

8.3 主界面实现

// 商城主界面
class MallMainView extends eui.Component {
    private header: eui.Group;
    private categoryList: CategoryList;
    private productList: eui.List;
    private cartButton: CartButton;
    private dataProvider: eui.ArrayCollection;
    
    public constructor() {
        super();
        this.createUI();
        this.loadData();
    }
    
    private createUI(): void {
        // 设置背景
        this.backgroundColor = 0xF5F5F5;
        
        // 创建头部
        this.createHeader();
        
        // 创建分类列表
        this.categoryList = new CategoryList();
        this.categoryList.y = 80;
        this.addChild(this.categoryList);
        
        // 创建商品列表
        this.productList = new eui.List();
        this.productList.y = 140;
        this.productList.width = this.stage.stageWidth;
        this.productList.height = this.stage.stageHeight - 200;
        this.productList.itemRenderer = ProductCard;
        this.addChild(this.productList);
        
        // 创建购物车按钮
        this.cartButton = new CartButton();
        this.cartButton.x = this.stage.stageWidth - 80;
        this.cartButton.y = this.stage.stageHeight - 100;
        this.addChild(this.cartButton);
        
        // 绑定事件
        this.bindEvents();
    }
    
    private createHeader(): void {
        this.header = new eui.Group();
        this.header.width = this.stage.stageWidth;
        this.header.height = 60;
        this.header.backgroundColor = 0xFFFFFF;
        
        // 标题
        let title = new eui.Label();
        title.text = "商城";
        title.size = 20;
        title.textColor = 0x333333;
        title.horizontalCenter = 0;
        title.verticalCenter = 0;
        this.header.addChild(title);
        
        this.addChild(this.header);
    }
    
    private bindEvents(): void {
        // 分类选择事件
        this.categoryList.addEventListener("categorySelected", this.onCategorySelected, this);
        
        // 商品选择事件
        this.productList.addEventListener("productSelected", this.onProductSelected, this);
        
        // 购物车按钮事件
        this.cartButton.addEventListener("openCart", this.onOpenCart, this);
    }
    
    private async loadData(): void {
        try {
            // 加载商品数据
            let products = await ProductService.getInstance().getProducts();
            
            // 设置数据源
            this.dataProvider = new eui.ArrayCollection(products);
            this.productList.dataProvider = this.dataProvider;
            
        } catch (error) {
            console.error("加载数据失败:", error);
            this.showError("网络连接失败,请重试");
        }
    }
    
    private onCategorySelected(event: egret.Event): void {
        let categoryId = event.data;
        this.filterProductsByCategory(categoryId);
    }
    
    private onProductSelected(event: egret.Event): void {
        let product = event.data;
        // 跳转到商品详情
        this.showProductDetail(product);
    }
    
    private onOpenCart(): void {
        // 打开购物车视图
        let cartView = new CartView();
        this.stage.addChild(cartView);
    }
    
    private filterProductsByCategory(categoryId: string): void {
        // 过滤商品数据
        let filtered = this.dataProvider.source.filter((product: any) => {
            return product.categoryId === categoryId;
        });
        
        this.dataProvider.source = filtered;
        this.dataProvider.refresh();
    }
    
    private showProductDetail(product: any): void {
        // 创建商品详情视图
        let detailView = new ProductDetailView(product);
        this.stage.addChild(detailView);
    }
    
    private showError(message: string): void {
        // 显示错误提示
        let errorLabel = new eui.Label();
        errorLabel.text = message;
        errorLabel.textColor = 0xFF0000;
        errorLabel.horizontalCenter = 0;
        errorLabel.verticalCenter = 0;
        this.addChild(errorLabel);
        
        // 3秒后自动移除
        egret.setTimeout(() => {
            if (errorLabel.parent) {
                errorLabel.parent.removeChild(errorLabel);
            }
        }, this, 3000);
    }
}

九、调试与测试

9.1 调试工具

// UI调试工具
class UIDebugger {
    private static instance: UIDebugger;
    private debugMode: boolean = false;
    private debugPanel: eui.Group;
    
    public static getInstance(): UIDebugger {
        if (!UIDebugger.instance) {
            UIDebugger.instance = new UIDebugger();
        }
        return UIDebugger.instance;
    }
    
    public enableDebug(): void {
        this.debugMode = true;
        this.createDebugPanel();
    }
    
    private createDebugPanel(): void {
        this.debugPanel = new eui.Group();
        this.debugPanel.width = 300;
        this.debugPanel.height = 400;
        this.debugPanel.backgroundColor = 0x000000;
        this.debugPanel.alpha = 0.8;
        this.debugPanel.x = 10;
        this.debugPanel.y = 10;
        
        // 调试信息显示
        let infoLabel = new eui.Label();
        infoLabel.text = "调试面板";
        infoLabel.textColor = 0xFFFFFF;
        infoLabel.y = 10;
        this.debugPanel.addChild(infoLabel);
        
        // 添加调试按钮
        this.addDebugButtons();
        
        // 添加到舞台
        egret.MainContext.instance.stage.addChild(this.debugPanel);
    }
    
    private addDebugButtons(): void {
        let buttons = [
            { label: "显示边界", action: () => this.toggleBoundaries() },
            { label: "显示FPS", action: () => this.toggleFPS() },
            { label: "清空缓存", action: () => this.clearCache() },
            { label: "重新加载", action: () => this.reloadUI() }
        ];
        
        buttons.forEach((btnData, index) => {
            let btn = new eui.Button();
            btn.label = btnData.label;
            btn.y = 40 + index * 35;
            btn.width = 280;
            btn.addEventListener(egret.TouchEvent.TOUCH_TAP, btnData.action, this);
            this.debugPanel.addChild(btn);
        });
    }
    
    private toggleBoundaries(): void {
        // 显示/隐藏所有组件的边界
        let stage = egret.MainContext.instance.stage;
        this.showBoundariesRecursive(stage);
    }
    
    private showBoundariesRecursive(displayObject: egret.DisplayObject): void {
        if (displayObject instanceof eui.Component) {
            let graphics = displayObject.graphics;
            graphics.clear();
            graphics.lineStyle(1, 0xFF0000);
            graphics.drawRect(0, 0, displayObject.width, displayObject.height);
        }
        
        // 递归处理子组件
        for (let i = 0; i < displayObject.numChildren; i++) {
            let child = displayObject.getChildAt(i);
            this.showBoundariesRecursive(child);
        }
    }
    
    private toggleFPS(): void {
        // 显示FPS计数器
        if (!this.debugPanel.getChildByName("fpsLabel")) {
            let fpsLabel = new eui.Label();
            fpsLabel.name = "fpsLabel";
            fpsLabel.y = 300;
            fpsLabel.textColor = 0x00FF00;
            this.debugPanel.addChild(fpsLabel);
            
            // 开始计数
            this.startFPSCount(fpsLabel);
        } else {
            // 移除FPS显示
            let fpsLabel = this.debugPanel.getChildByName("fpsLabel");
            if (fpsLabel) {
                this.debugPanel.removeChild(fpsLabel);
            }
        }
    }
    
    private startFPSCount(label: eui.Label): void {
        let lastTime = Date.now();
        let frameCount = 0;
        
        let updateFPS = () => {
            frameCount++;
            let currentTime = Date.now();
            
            if (currentTime - lastTime >= 1000) {
                let fps = Math.round(frameCount * 1000 / (currentTime - lastTime));
                label.text = `FPS: ${fps}`;
                frameCount = 0;
                lastTime = currentTime;
            }
            
            requestAnimationFrame(updateFPS);
        };
        
        updateFPS();
    }
    
    private clearCache(): void {
        // 清理资源缓存
        SmartResourceManager.getInstance().unloadUnusedAssets();
        console.log("缓存已清理");
    }
    
    private reloadUI(): void {
        // 重新加载当前UI
        let currentView = egret.MainContext.instance.stage;
        // 这里可以根据实际情况重新加载UI
        console.log("重新加载UI");
    }
}

9.2 自动化测试

// UI自动化测试框架
class UITestFramework {
    private testCases: TestCase[] = [];
    private isRunning: boolean = false;
    
    public addTestCase(name: string, testFunction: () => Promise<boolean>): void {
        this.testCases.push({ name, testFunction });
    }
    
    public async runTests(): Promise<void> {
        if (this.isRunning) {
            console.log("测试已在运行中");
            return;
        }
        
        this.isRunning = true;
        console.log("开始UI自动化测试...");
        
        let passed = 0;
        let failed = 0;
        
        for (let testCase of this.testCases) {
            try {
                console.log(`运行测试: ${testCase.name}`);
                let result = await testCase.testFunction();
                
                if (result) {
                    console.log(`✓ ${testCase.name} 通过`);
                    passed++;
                } else {
                    console.log(`✗ ${testCase.name} 失败`);
                    failed++;
                }
            } catch (error) {
                console.log(`✗ ${testCase.name} 异常: ${error.message}`);
                failed++;
            }
        }
        
        console.log(`测试完成: ${passed} 通过, ${failed} 失败`);
        this.isRunning = false;
    }
    
    // 示例测试用例
    public createExampleTests(): void {
        // 测试按钮点击
        this.addTestCase("按钮点击测试", async () => {
            let button = new eui.Button();
            button.label = "测试按钮";
            
            let clicked = false;
            button.addEventListener(egret.TouchEvent.TOUCH_TAP, () => {
                clicked = true;
            });
            
            // 模拟点击
            button.dispatchEvent(new egret.TouchEvent(egret.TouchEvent.TOUCH_TAP));
            
            return clicked;
        });
        
        // 测试数据绑定
        this.addTestCase("数据绑定测试", async () => {
            let model = { value: "初始值" };
            let label = new eui.Label();
            
            // 建立绑定
            eui.Binding.bindProperty(model, "value", label, "text");
            
            // 修改数据
            model.value = "新值";
            
            // 等待下一帧
            await new Promise(resolve => {
                egret.setTimeout(resolve, this, 100);
            });
            
            return label.text === "新值";
        });
        
        // 测试列表渲染
        this.addTestCase("列表渲染测试", async () => {
            let list = new eui.List();
            let data = [{ name: "项目1" }, { name: "项目2" }];
            list.dataProvider = new eui.ArrayCollection(data);
            
            // 等待渲染
            await new Promise(resolve => {
                egret.setTimeout(resolve, this, 100);
            });
            
            return list.numChildren > 0;
        });
    }
}

interface TestCase {
    name: string;
    testFunction: () => Promise<boolean>;
}

十、最佳实践与总结

10.1 代码组织最佳实践

  1. 组件化开发:每个UI组件应该独立、可复用
  2. 数据与视图分离:使用MVVM模式,避免在视图中处理业务逻辑
  3. 资源管理:合理使用对象池,避免频繁创建销毁对象
  4. 性能监控:定期检查渲染性能,优化重绘区域

10.2 跨平台注意事项

  1. 分辨率适配:使用相对单位,避免固定像素值
  2. 触摸事件:不同平台的触摸事件可能有差异
  3. 资源加载:考虑网络环境差异,实现渐进式加载
  4. 性能差异:移动端性能较弱,需要更严格的性能优化

10.3 持续优化建议

  1. 定期重构:随着项目增长,定期重构代码结构
  2. 性能分析:使用Egret Profiler分析性能瓶颈
  3. 用户反馈:收集用户反馈,持续改进UI体验
  4. 技术更新:关注Egret Engine的更新,及时升级

结语

通过本指南,你已经掌握了从零开始构建Egret EUI跨平台界面系统的完整流程。从环境搭建到高级优化,从基础组件到实战项目,我们覆盖了EUI开发的各个方面。

记住,优秀的UI系统不仅需要技术实现,更需要良好的设计思维和用户体验意识。持续学习、实践和优化,你将能够构建出既高效又美观的跨平台界面系统。

开始你的Egret EUI之旅吧!