引言
在移动互联网时代,跨平台开发已成为主流趋势。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 核心优势
- 高性能渲染:基于Canvas/WebGL的渲染机制
- 声明式UI:类似React的组件化思想
- 响应式布局:自动适配不同屏幕尺寸
- 热更新支持:无需重新打包即可更新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 代码组织最佳实践
- 组件化开发:每个UI组件应该独立、可复用
- 数据与视图分离:使用MVVM模式,避免在视图中处理业务逻辑
- 资源管理:合理使用对象池,避免频繁创建销毁对象
- 性能监控:定期检查渲染性能,优化重绘区域
10.2 跨平台注意事项
- 分辨率适配:使用相对单位,避免固定像素值
- 触摸事件:不同平台的触摸事件可能有差异
- 资源加载:考虑网络环境差异,实现渐进式加载
- 性能差异:移动端性能较弱,需要更严格的性能优化
10.3 持续优化建议
- 定期重构:随着项目增长,定期重构代码结构
- 性能分析:使用Egret Profiler分析性能瓶颈
- 用户反馈:收集用户反馈,持续改进UI体验
- 技术更新:关注Egret Engine的更新,及时升级
结语
通过本指南,你已经掌握了从零开始构建Egret EUI跨平台界面系统的完整流程。从环境搭建到高级优化,从基础组件到实战项目,我们覆盖了EUI开发的各个方面。
记住,优秀的UI系统不仅需要技术实现,更需要良好的设计思维和用户体验意识。持续学习、实践和优化,你将能够构建出既高效又美观的跨平台界面系统。
开始你的Egret EUI之旅吧!
