/**
* ESP32-S3 UI框架完整代码 - 使用U8g2库
* 针对Wokwi模拟器优化,支持触摸屏和按键双重输入
* 包含图文混排
*/
#include <U8g2lib.h> // U8g2显示库
#include <Arduino.h>
#include <algorithm>
using std::min;
using std::max;
// ==================== Wokwi模拟器配置 ====================
// 虚拟按键引脚(使用内部上拉电阻,按键接地)
#define BTN_UP_PIN 2
#define BTN_DOWN_PIN 3
#define BTN_LEFT_PIN 4
#define BTN_RIGHT_PIN 5
#define BTN_A_PIN 6
#define BTN_B_PIN 7
// 按键去抖参数(毫秒)
#define DEBOUNCE_DELAY 5
// 显示尺寸常量
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// 动画帧率控制
#define ANIMATION_FRAME_DELAY 200 // ms
// 最大栈深度
#define MAX_STACK_DEPTH 5
// ==================== 类型定义 ====================
typedef uint8_t PageID;
typedef void (*Callback)(void);
// 按键编码定义
#define KEY_UP 'U'
#define KEY_DOWN 'D'
#define KEY_LEFT 'L'
#define KEY_RIGHT 'R'
#define KEY_A 'A'
#define KEY_B 'B'
#define KEY_NONE '\0'
// ==================== 数据结构定义 ====================
/**
* 图片资源结构
*/
struct ImageResource {
const uint8_t* bitmap; // 位图数据指针
uint8_t width; // 图片宽度
uint8_t height; // 图片高度
};
/**
* 选项内容结构
*/
struct OptionContent {
const char* text; // 选项文本
const ImageResource* image; // 静态图片资源
bool hasImage; // 是否有图片
};
/**
* 菜单项结构
*/
struct MenuItem {
OptionContent content; // 选项内容
PageID targetPage; // 目标页面ID
Callback action; // 自定义动作回调
};
/**
* 页面渲染配置结构
*/
struct RenderConfig {
uint8_t layout; // 布局方式:0-竖排, 1-横排
uint8_t startX; // 起始X坐标
uint8_t startY; // 起始Y坐标
uint8_t itemSpacing; // 选项间距
uint8_t visibleItems; // 可见选项数量
};
/**
* 页面状态结构
*/
struct PageState {
PageID pageId; // 页面ID
uint8_t selectedIndex; // 选中项索引
int8_t scrollOffset; // 滚动偏移量
int16_t pixelOffset; // 像素偏移量
};
// ==================== 图片资源数据 ====================
// 游戏图标 (16x16)
static const unsigned char icon_game_bits[] = {
0x00, 0x00, 0xE0, 0x07, 0x10, 0x08, 0xC8, 0x13, 0x24, 0x24, 0x12, 0x48,
0x0A, 0x50, 0x06, 0x60, 0x06, 0x60, 0x0A, 0x50, 0x12, 0x48, 0x24, 0x24,
0xC8, 0x13, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00
};
// 设置图标 (16x16)
static const unsigned char icon_settings_bits[] = {
0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0x8C, 0x31, 0x9C, 0x39, 0xF8, 0x1F,
0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0x9C, 0x39,
0x8C, 0x31, 0x80, 0x01, 0x80, 0x01, 0x00, 0x00
};
// 信息图标 (16x16)
static const unsigned char icon_info_bits[] = {
0x00, 0x00, 0xF0, 0x0F, 0x08, 0x10, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20,
0x04, 0x20, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20,
0x08, 0x10, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00
};
// WiFi图标 (16x16)
static const unsigned char icon_wifi_bits[] = {
0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0x38, 0x1C,
0x8C, 0x31, 0xC6, 0x63, 0xE0, 0x07, 0xF0, 0x0F, 0x38, 0x1C, 0x0C, 0x30,
0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x00
};
// GIF动画帧数据 - 闪烁效果
static const unsigned char icon_gif_frame[] = {
0x00, 0x00, 0xF0, 0x0F, 0x08, 0x10, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20,
0x04, 0x20, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20, 0x04, 0x20,
0x08, 0x10, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char menu_background[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0xFC,0x0F,0x00,0x00,0xE0,0x3F,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0xFC,0x0F,0x00,0x00,0xE0,0x3F,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0xFC,0x07,0x00,0x00,0xE0,0x3F,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0xFC,0x07,0x00,0x00,0xE0,0x3F,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0x3E,
0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0xBE,0xEF,0xFB,0x3E,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0xF0,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,
0x00,0xF0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,
0x40,0x30,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x02,
0x60,0x30,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x06,
0x70,0xF0,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x0E,
0xF8,0xF3,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0xC0,0x1F,
0x70,0x30,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x0E,
0x60,0x30,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x06,
0x40,0x30,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x0F,0x02,
0x00,0x30,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x0F,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};
// ==================== 资源实例化 ====================
ImageResource ICON_GAME = { icon_game_bits, 16, 16 };
ImageResource ICON_SETTINGS = { icon_settings_bits, 16, 16 };
ImageResource ICON_INFO = { icon_info_bits, 16, 16 };
ImageResource ICON_WIFI = { icon_wifi_bits, 16, 16 };
// ==================== 类前向声明 ====================
class Page;
class NavigationManager;
// ==================== 全局变量 ====================
NavigationManager* globalNavManager = nullptr;
// ==================== 核心类实现 ====================
/**
* 抽象页面类
* 所有页面的基类,定义页面生命周期和接口
*/
class Page {
public:
PageID id;
MenuItem* items;
uint8_t itemCount;
RenderConfig renderConfig;
uint8_t currentSelection;
uint8_t scrollOffset;
int16_t pixelOffset; // 新增:像素级别的平滑偏移
bool smoothScrolling; // 新增:是否启用平滑滚动
unsigned long lastScrollTime; // 新增:上次滚动时间
virtual ~Page() {}
virtual void onEnter(void* data) = 0;
virtual void onExit() = 0;
virtual void onUpdate() = 0;
virtual void onKeyPress(char key) = 0;
virtual void render(U8G2& display) = 0;
virtual void updateAnimations()=0;
MenuItem* getSelectedItem() {
if (itemCount > 0 && currentSelection < itemCount) {
return &items[currentSelection];
}
return nullptr;
}
/**
* 处理导航按键 - 通用方法
*/
virtual void handleNavigationKey(char key) {
if (renderConfig.layout == 0) { // 竖排布局
switch (key) {
case KEY_UP:
if (currentSelection > 0) {
if(currentSelection - scrollOffset == 0)
scrollOffset--;
currentSelection--;
// 设置像素偏移以启动平滑滚动
if (smoothScrolling) {
pixelOffset = renderConfig.itemSpacing;
}
}
break;
case KEY_DOWN:
if (currentSelection < itemCount - 1) {
if(currentSelection - scrollOffset == renderConfig.visibleItems - 1)
scrollOffset++;
currentSelection++;
// 设置像素偏移以启动平滑滚动
if (smoothScrolling) {
pixelOffset = -renderConfig.itemSpacing;
}
}
break;
}
} else { // 横排布局
switch (key) {
case KEY_LEFT:
if (currentSelection > 0) {
if(currentSelection - scrollOffset == 0)
scrollOffset--;
currentSelection--;
// 设置像素偏移以启动平滑滚动
if (smoothScrolling) {
pixelOffset = renderConfig.itemSpacing;
}
}
break;
case KEY_RIGHT:
if (currentSelection < itemCount - 1) {
if(currentSelection - scrollOffset == renderConfig.visibleItems - 1)
scrollOffset++;
currentSelection++;
// 设置像素偏移以启动平滑滚动
if (smoothScrolling) {
pixelOffset = renderConfig.itemSpacing;
}
}
break;
}
}
}
void updateSmoothScroll() {
if (pixelOffset != 0) {
unsigned long currentTime = millis();
unsigned long deltaTime = currentTime - lastScrollTime;
if (deltaTime > 16) { // 约60FPS的更新率
// 使用固定步长而不是动态步长
int16_t scrollStep = 20; // 固定每次移动20像素
if (pixelOffset > 0) {
pixelOffset = max(0, pixelOffset - scrollStep);
} else if (pixelOffset < 0) {
pixelOffset = min(0, pixelOffset + scrollStep);
}
lastScrollTime = currentTime;
}
}
}
};
/**
* 导航管理器类
*/
class NavigationManager {
private:
PageState stack[MAX_STACK_DEPTH];
int stackTop = -1;
Page* currentPage = nullptr;
Page* (*pageFactory)(PageID);
public:
NavigationManager() {
globalNavManager = this;
}
void setPageFactory(Page* (*factory)(PageID)) {
pageFactory = factory;
}
/**
* 检查页面是否存在
*/
bool pageExists(PageID pageId) {
Page* testPage = pageFactory(pageId);
bool exists = (testPage != nullptr);
if (testPage) delete testPage;
return exists;
}
void pushPage(PageID pageId, void* data = nullptr) {
if (!pageExists(pageId)) {
Serial.printf("Error: Page %d does not exist!\n", pageId);
return;
}
if (stackTop >= MAX_STACK_DEPTH - 1) {
Serial.println("Error: Navigation stack full!");
return;
}
if (currentPage) {
PageState state;
state.pageId = currentPage->id;
state.selectedIndex = currentPage->currentSelection;
state.scrollOffset = currentPage->scrollOffset;
state.pixelOffset = currentPage->pixelOffset; // 保存像素偏移
stack[++stackTop] = state;
currentPage->onExit();
delete currentPage;
}
currentPage = pageFactory(pageId);
if (currentPage) {
currentPage->onEnter(data);
}
}
void popPage() {
if (stackTop < 0) return;
if (currentPage) {
currentPage->onExit();
delete currentPage;
}
PageState prevState = stack[stackTop--];
currentPage = pageFactory(prevState.pageId);
if (currentPage) {
currentPage->currentSelection = prevState.selectedIndex;
currentPage->scrollOffset = prevState.scrollOffset;
currentPage->pixelOffset = prevState.pixelOffset; // 恢复像素偏移
currentPage->onEnter(nullptr);
}
}
Page* getCurrentPage() { return currentPage; }
void popToRoot() {
while (stackTop >= 0) {
popPage();
}
}
};
/**
* 输入管理器类 - ESP32-S3版本
* 支持物理按键
*/
class InputManager {
private:
NavigationManager* navManager;
struct Button {
uint8_t pin;
char keyCode;
uint8_t lastState;
unsigned long lastPressTime;
};
Button buttons[6] = {
{BTN_UP_PIN, KEY_UP, HIGH, 0},
{BTN_DOWN_PIN, KEY_DOWN, HIGH, 0},
{BTN_LEFT_PIN, KEY_LEFT, HIGH, 0},
{BTN_RIGHT_PIN, KEY_RIGHT, HIGH, 0},
{BTN_A_PIN, KEY_A, HIGH, 0},
{BTN_B_PIN, KEY_B, HIGH, 0}
};
public:
InputManager() {
for (int i = 0; i < 6; i++) {
pinMode(buttons[i].pin, INPUT_PULLUP);
// 确保引脚初始状态为高电平
digitalWrite(buttons[i].pin, HIGH);
}
Serial.println("InputManager: Buttons initialized");
}
void setNavigationManager(NavigationManager* nav) {
navManager = nav;
}
/**
* 读取物理按键(ESP32-S3兼容)
*/
char readKey() {
unsigned long currentTime = millis();
for (int i = 0; i < 6; i++) {
int reading = digitalRead(buttons[i].pin);
if (reading == LOW && buttons[i].lastState == HIGH) {
if (currentTime - buttons[i].lastPressTime > DEBOUNCE_DELAY) {
buttons[i].lastPressTime = currentTime;
buttons[i].lastState = reading;
Serial.printf("Button pressed: %c\n", buttons[i].keyCode);
return buttons[i].keyCode;
}
}
if (reading == HIGH) {
buttons[i].lastState = reading;
}
}
return KEY_NONE;
}
void update() {
// 先检查物理按键
char key = readKey();
if (key != KEY_NONE) {
Page* currentPage = navManager->getCurrentPage();
if (currentPage) {
currentPage->onKeyPress(key);
}
}
}
};
/**
* 渲染工具类
*/
class RenderUtils {
public:
/**
* 绘制带滚动效果的菜单
*/
static void drawScrollMenu(U8G2& display, Page* page) {
RenderConfig config = page->renderConfig;
uint8_t visibleCount = min(config.visibleItems, page->itemCount);
// 计算平滑滚动偏移
int16_t smoothOffset = page->pixelOffset;
// 使用页面维护的 scrollOffset
uint8_t x = config.startX;
uint8_t y = config.startY;
// 绘制所有可见选项
for (uint8_t i = 0; i < page->itemCount; i++) {
bool isSelected = (i == page->currentSelection);
MenuItem& item = page->items[i];
// 更新绘制位置
if (config.layout == 0) { // 竖排
drawOptionContent(display, item.content, x, y+(i-page->scrollOffset)*config.itemSpacing + smoothOffset, isSelected);
} else { // 横排
drawOptionContent(display, item.content, x+(i-page->scrollOffset)*config.itemSpacing + smoothOffset, y, isSelected);
}
}
}
/**
* 绘制单个选项内容(图文混排)
*/
static void drawOptionContent(U8G2& display, const OptionContent& content,
uint8_t x, uint8_t y, bool isSelected) {
uint8_t textX = x;
uint8_t textY = y;
// 绘制图片或动画
if (content.hasImage) {
uint8_t imgX = x;
uint8_t imgY = y;
if (content.image) {
// 绘制静态图片
display.drawXBMP(imgX, imgY, content.image->width, content.image->height, content.image->bitmap);
}
textX = x + 8 - display.getStrWidth(content.text)/2;
textY = y + 35;
}
if (content.text) {
display.setFont(u8g2_font_6x10_tf);
if (isSelected) {
// 选中状态:反色显示
uint8_t textWidth = display.getStrWidth(content.text);
display.setDrawColor(1);
display.drawBox(textX - 1, textY - 1, textWidth + 2, 12);
display.setDrawColor(0);
display.drawStr(textX, textY, content.text);
display.setDrawColor(1);
} else{
if(!content.hasImage)
// 普通状态
display.drawStr(textX, textY, content.text);
}
}
}
};
// ==================== 具体页面实现 ====================
/**
* 主页面类 - ESP32-S3版本
*/
class MainPage : public Page {
public:
MainPage() {
id = 1;
itemCount = 4;
items = new MenuItem[4];
// 初始化菜单项
items[0] = {"Play", &ICON_GAME, true, 4, nullptr};
items[1] = {"Settings", &ICON_SETTINGS, true, 3, nullptr};
items[2] = {"WiFi1", &ICON_WIFI, true, 5, nullptr};
items[3] = {"WiFi22", &ICON_WIFI, true, 5, nullptr};
// 横向布局配置
renderConfig = {1, 56, 20, 45, 1};
currentSelection = 0;
scrollOffset = 0;
pixelOffset = 0;
smoothScrolling = true; // 启用平滑滚动
lastScrollTime = millis();
}
~MainPage() {
delete[] items;
}
void onEnter(void* data) override {
Serial.println("MainPage: Entered");
// 可以在这里添加进入页面时的初始化代码
}
void onExit() override {
Serial.println("MainPage: Exited");
// 可以在这里添加退出页面时的清理代码
}
void onUpdate() override {
// 主页面不需要特殊的更新逻辑
}
void updateAnimations() override {
// 主页面没有动画需要更新
}
void onKeyPress(char key) override {
Serial.printf("MainPage: Key %c pressed\n", key);
// 先处理导航按键
handleNavigationKey(key);
switch (key) {
case KEY_A:
if (MenuItem* selected = getSelectedItem()) {
if (globalNavManager->pageExists(selected->targetPage)) {
globalNavManager->pushPage(selected->targetPage);
} else {
Serial.printf("Error: Target page %d does not exist!\n", selected->targetPage);
// 可以在这里添加错误提示,比如显示错误消息或播放错误音效
}
}
break;
case KEY_B:
// 可以添加退出逻辑
break;
}
}
void render(U8G2& display) override {
display.clearBuffer();
// 绘制菜单
RenderUtils::drawScrollMenu(display, this);
//绘制背景
display.setDrawColor(1);
display.setBitmapMode(1);
display.drawXBM(0,0,128,64,menu_background);
display.sendBuffer();
}
};
/**
* 设置页面类
*/
class SettingsPage : public Page {
public:
SettingsPage() {
id = 3;
itemCount = 8;
items = new MenuItem[8];
items[0] = {"Brightness", nullptr, false, 0, nullptr};
items[1] = {"Sound", nullptr, false, 0, nullptr};
items[2] = {"Back1", nullptr, false, 0, nullptr};
items[3] = {"Back2", nullptr, false, 0, nullptr};
items[4] = {"Backl22", nullptr, false, 0, nullptr};
items[5] = {"Backli21t", nullptr, false, 0, nullptr};
items[6] = {"Backli2222", nullptr, false, 0, nullptr};
items[7] = {"Backli112ght", nullptr, false, 0, nullptr};
renderConfig = {0, 2, 2, 15, 4};
currentSelection = 0;
scrollOffset = 0;
pixelOffset = 0;
smoothScrolling = true; // 启用平滑滚动
lastScrollTime = millis();
}
void onEnter(void* data) override {
Serial.println("SettingsPage: Entered");
// 可以在这里添加进入页面时的初始化代码
}
void onExit() override {
Serial.println("SettingsPage: Exited");
// 可以在这里添加退出页面时的清理代码
}
void onUpdate() override {
// 设置页面不需要特殊的更新逻辑
}
void updateAnimations() override {
// 设置页面没有动画需要更新
}
void onKeyPress(char key) override {
// 先处理导航按键
handleNavigationKey(key);
switch (key) {
case KEY_A:
Serial.printf("Settings option %d selected\n", currentSelection);
// 可以添加设置项的具体操作
break;
case KEY_B:
Serial.println("Returning to main page");
globalNavManager->popPage();
break;
}
}
void render(U8G2& display) override {
display.clearBuffer();
RenderUtils::drawScrollMenu(display, this);
display.sendBuffer();
}
};
/**
* 声音波形展示页面类
*/
class SoundWavePage : public Page {
private:
static const int WAVE_DATA_SIZE = 128; // 波形数据点数(与屏幕宽度匹配)
int8_t waveData[WAVE_DATA_SIZE]; // 波形数据数组
unsigned long lastUpdateTime; // 上次更新时间
int currentPosition; // 当前播放位置
bool isPlaying; // 播放状态
public:
SoundWavePage() {
id = 4;
itemCount = 0;
items = nullptr;
// 页面配置(这个页面没有菜单项)
renderConfig = {0, 0, 0, 0, 0};
currentSelection = 0;
scrollOffset = 0;
pixelOffset = 0;
smoothScrolling = false; // 这个页面不需要平滑滚动
lastScrollTime = millis();
// 初始化波形数据
generateWaveform();
currentPosition = 0;
isPlaying = true;
lastUpdateTime = millis();
}
~SoundWavePage() {
// 无需删除items,因为itemCount为0
}
/**
* 生成示例波形数据(正弦波叠加一些谐波)
*/
void generateWaveform() {
for (int i = 0; i < WAVE_DATA_SIZE; i++) {
// 基础正弦波(主频率)
float base = sin(2.0 * PI * i / WAVE_DATA_SIZE * 2.0);
// 添加二次谐波
float harmonic1 = 0.6 * sin(2.0 * PI * i / WAVE_DATA_SIZE * 4.0 + 0.5);
// 添加三次谐波
float harmonic2 = 0.3 * sin(2.0 * PI * i / WAVE_DATA_SIZE * 6.0 + 1.2);
// 组合波形并缩放到合适的范围
float combined = (base + harmonic1 + harmonic2) * 0.8;
// 转换为8位有符号整数(-31到31,适应屏幕高度)
waveData[i] = static_cast<int8_t>(combined * 31);
}
}
void onEnter(void* data) override {
Serial.println("SoundWavePage: Entered - Audio waveform display");
isPlaying = true;
currentPosition = 0;
lastUpdateTime = millis();
}
void onExit() override {
Serial.println("SoundWavePage: Exited");
isPlaying = false;
}
void onUpdate() override {
// 更新播放位置(模拟音频播放)
unsigned long currentTime = millis();
if (isPlaying && currentTime - lastUpdateTime > 30) { // 约33Hz更新率
currentPosition = (currentPosition + 1) % WAVE_DATA_SIZE;
lastUpdateTime = currentTime;
}
}
void updateAnimations() override {
// 波形动画在onUpdate中处理
}
void onKeyPress(char key) override {
switch (key) {
case KEY_B:
Serial.println("Returning from sound wave page");
globalNavManager->popPage();
break;
case KEY_A:
// 切换播放/暂停
isPlaying = !isPlaying;
Serial.println(isPlaying ? "Waveform: Playing" : "Waveform: Paused");
break;
case KEY_LEFT:
// 后退
currentPosition = (currentPosition - 10 + WAVE_DATA_SIZE) % WAVE_DATA_SIZE;
Serial.println("Waveform: Rewind");
break;
case KEY_RIGHT:
// 前进
currentPosition = (currentPosition + 10) % WAVE_DATA_SIZE;
Serial.println("Waveform: Forward");
break;
}
}
void render(U8G2& display) override {
display.clearBuffer();
// 绘制标题
display.setFont(u8g2_font_6x10_tf);
display.drawStr(40, 2, "Sound Wave");
// 绘制X轴(屏幕中线)
uint8_t centerY = SCREEN_HEIGHT / 2;
display.drawHLine(0, centerY, SCREEN_WIDTH);
// 绘制波形
for (int x = 0; x < SCREEN_WIDTH; x++) {
// 计算数据索引(循环播放)
int dataIndex = (currentPosition + x) % WAVE_DATA_SIZE;
int8_t amplitude = waveData[dataIndex];
// 计算Y坐标(将振幅映射到屏幕坐标)
uint8_t y = centerY - amplitude;
// 绘制数据点
display.drawPixel(x, y);
// 可选:绘制垂直线段增强视觉效果
if (x % 4 == 0) {
display.drawVLine(x, min(y, centerY), abs(amplitude));
}
}
// 绘制播放状态指示器
if (isPlaying) {
display.drawStr(2, SCREEN_HEIGHT - 10, "Playing");
} else {
display.drawStr(2, SCREEN_HEIGHT - 10, "Paused");
}
// 绘制操作提示
display.drawStr(SCREEN_WIDTH - 60, SCREEN_HEIGHT - 10, "A:Play/Pause B:Back");
display.sendBuffer();
}
};
// ==================== 全局对象 ====================
// 使用适合ESP32-S3的显示驱动
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
NavigationManager navManager;
InputManager inputManager;
/**
* 页面工厂函数
*/
Page* createPage(PageID id) {
switch (id) {
case 1: return new MainPage();
case 3: return new SettingsPage();
case 4: return new SoundWavePage(); // 添加声音波形页面
default:
Serial.printf("Error: Unknown page ID: %d\n", id);
return nullptr;
}
}
// ==================== Arduino主程序 ====================
void setup() {
Serial.begin(115200); // ESP32-S3使用更高的波特率
delay(1000); // 等待串口初始化
Serial.println("ESP32-S3 UI Framework Initializing...");
Serial.println("Wokwi Simulation Ready");
// 初始化显示
u8g2.begin();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setDrawColor(1);
u8g2.setFontPosTop();
Serial.println("Display initialized");
// 设置页面工厂和导航
navManager.setPageFactory(createPage);
inputManager.setNavigationManager(&navManager);
// 启动主页面
navManager.pushPage(1);
Serial.println("UI Framework Started Successfully");
Serial.println("Use buttons for navigation:");
Serial.println("UP/DOWN/LEFT/RIGHT: Navigation");
Serial.println("A: Select, B: Back");
}
void loop() {
// 处理输入
inputManager.update();
// 更新和渲染当前页面
Page* currentPage = navManager.getCurrentPage();
if (currentPage) {
currentPage->onUpdate();
currentPage->updateAnimations();
// 更新平滑滚动动画
if (currentPage->smoothScrolling) {
currentPage->updateSmoothScroll();
}
currentPage->render(u8g2);
}
// ESP32-S3可以处理更高的帧率
delay(16); // 约60FPS,为了平滑动画
}