192 lines
6.1 KiB
C++
192 lines
6.1 KiB
C++
#ifndef UI_H
|
||
#define UI_H
|
||
|
||
#include <Arduino.h>
|
||
#include <Adafruit_ST7735.h>
|
||
#include <Adafruit_GFX.h>
|
||
#include <LittleFS.h>
|
||
#include <U8g2_for_Adafruit_GFX.h>
|
||
#include "utils.h"
|
||
|
||
namespace esp32 {
|
||
namespace ui {
|
||
|
||
// 简易图标:RGB565 像素数组,内存持有
|
||
struct Icon {
|
||
uint16_t width = 0;
|
||
uint16_t height = 0;
|
||
uint16_t* pixels = nullptr; // width*height 大小的 RGB565 数组
|
||
};
|
||
|
||
// 显示操作类型
|
||
enum class OpType : uint8_t { FillRect = 0, DrawText = 1, DrawIcon = 2, Line = 3, Circle = 4 };
|
||
|
||
// 显示项(Display List 条目)
|
||
struct DisplayItem {
|
||
OpType op;
|
||
uint16_t key; // 用于跨帧识别(简化版,当前全量重放)
|
||
int16_t x;
|
||
int16_t y;
|
||
int16_t w; // FillRect 使用 或 Line/Circle 半径/终点 x
|
||
int16_t h; // FillRect 使用 或 Line 终点 y
|
||
uint16_t color; // FillRect/DrawText 使用
|
||
const char* text; // 指向 textBuf 或字符串字面量
|
||
const Icon* icon; // DrawIcon 使用
|
||
char textBuf[64]; // 小型文本缓冲,存放动态生成的字符串(例如时间、lap)
|
||
};
|
||
|
||
// 固定容量的显示列表
|
||
class DisplayList {
|
||
public:
|
||
static const int MAX_ITEMS = 256;
|
||
DisplayList() : _count(0) {}
|
||
|
||
void clear() { _count = 0; }
|
||
int size() const { return _count; }
|
||
const DisplayItem& at(int i) const { return _items[i]; }
|
||
|
||
bool addFillRect(uint16_t key, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
|
||
if (_count >= MAX_ITEMS) return false;
|
||
auto& it = _items[_count++];
|
||
it.op = OpType::FillRect;
|
||
it.key = key;
|
||
it.x = x; it.y = y; it.w = w; it.h = h; it.color = color;
|
||
it.text = nullptr; it.icon = nullptr;
|
||
return true;
|
||
}
|
||
|
||
bool addText(uint16_t key, int16_t x, int16_t y, uint16_t color, const char* text, bool centered = false) {
|
||
if (_count >= MAX_ITEMS) return false;
|
||
auto& it = _items[_count++];
|
||
it.op = OpType::DrawText;
|
||
it.key = key;
|
||
it.x = x; it.y = y;
|
||
it.w = centered ? 1 : 0; // w=1 表示需要居中
|
||
it.h = 0;
|
||
it.color = color;
|
||
// 复制文本到缓冲,保证作用域结束后仍可用
|
||
if (text) {
|
||
strncpy(it.textBuf, text, sizeof(it.textBuf) - 1);
|
||
it.textBuf[sizeof(it.textBuf) - 1] = '\0';
|
||
it.text = it.textBuf;
|
||
} else {
|
||
it.textBuf[0] = '\0';
|
||
it.text = it.textBuf;
|
||
}
|
||
it.icon = nullptr;
|
||
return true;
|
||
}
|
||
|
||
bool addIcon(uint16_t key, int16_t x, int16_t y, const Icon* icon) {
|
||
if (_count >= MAX_ITEMS) return false;
|
||
auto& it = _items[_count++];
|
||
it.op = OpType::DrawIcon;
|
||
it.key = key;
|
||
it.x = x; it.y = y; it.w = 0; it.h = 0; it.color = 0;
|
||
it.text = nullptr; it.icon = icon;
|
||
return true;
|
||
}
|
||
|
||
bool addLine(uint16_t key, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
|
||
if (_count >= MAX_ITEMS) return false;
|
||
auto& it = _items[_count++];
|
||
it.op = OpType::Line;
|
||
it.key = key;
|
||
it.x = x1; it.y = y1; it.w = x2; it.h = y2; it.color = color;
|
||
it.text = nullptr; it.icon = nullptr;
|
||
return true;
|
||
}
|
||
|
||
bool addCircle(uint16_t key, int16_t cx, int16_t cy, int16_t radius, uint16_t color) {
|
||
if (_count >= MAX_ITEMS) return false;
|
||
auto& it = _items[_count++];
|
||
it.op = OpType::Circle;
|
||
it.key = key;
|
||
it.x = cx; it.y = cy; it.w = radius; it.h = 0; it.color = color;
|
||
it.text = nullptr; it.icon = nullptr;
|
||
return true;
|
||
}
|
||
|
||
private:
|
||
int _count;
|
||
DisplayItem _items[MAX_ITEMS];
|
||
};
|
||
|
||
// Tween(补间动画)
|
||
struct Tween {
|
||
float* target = nullptr;
|
||
float from = 0.0f;
|
||
float to = 0.0f;
|
||
uint32_t startMs = 0;
|
||
uint32_t durationMs = 0;
|
||
float x1 = 0.42f, y1 = 0.0f, x2 = 0.58f, y2 = 1.0f; // 贝塞尔参数
|
||
bool active = false;
|
||
};
|
||
|
||
// UI 框架主体
|
||
class UI {
|
||
public:
|
||
UI() : _tft(nullptr), _u8g2(nullptr), _builder(nullptr), _builderCtx(nullptr) {}
|
||
|
||
void begin(Adafruit_ST7735* tft, U8G2_FOR_ADAFRUIT_GFX* u8g2 = nullptr) {
|
||
_tft = tft;
|
||
_u8g2 = u8g2;
|
||
}
|
||
|
||
// builder: 负责把状态转为显示列表
|
||
using BuilderFn = void (*)(void* ctx, DisplayList& out);
|
||
void setScene(BuilderFn builder, void* ctx) {
|
||
_builder = builder;
|
||
_builderCtx = ctx;
|
||
}
|
||
|
||
// 每帧调用:更新动画 + 生成显示列表 + 渲染
|
||
void tick(uint32_t nowMs);
|
||
|
||
// 添加一个 float 补间(从当前值到目标值)
|
||
int addTweenFloat(float* target, float to, uint32_t durationMs,
|
||
float x1 = 0.42f, float y1 = 0.0f, float x2 = 0.58f, float y2 = 1.0f);
|
||
|
||
bool hasActiveTweens() const;
|
||
bool isTweenActive(int id) const;
|
||
|
||
// 图标加载:文件格式 (little-endian): [2字节宽][2字节高][像素: width*height*2]
|
||
static bool loadIconFromFS(const char* path, Icon& out);
|
||
static void freeIcon(Icon& icon);
|
||
|
||
// 启用内存帧缓冲(双缓冲),第一次调用会分配两块 16 位色的画布
|
||
bool enableBackBuffer(uint16_t width, uint16_t height);
|
||
bool isBackBufferEnabled() const { return _useBackBuffer; }
|
||
|
||
private:
|
||
void _updateTweens(uint32_t nowMs);
|
||
void _render(const DisplayList& list);
|
||
void _drawIcon(int16_t x, int16_t y, const Icon& icon);
|
||
void _renderToCanvas(const DisplayList& list); // 使用内存画布绘制
|
||
void _flushCanvas(); // 将当前画布推送到屏幕
|
||
void _swapCanvas(); // 交换前后缓冲
|
||
|
||
Adafruit_ST7735* _tft;
|
||
U8G2_FOR_ADAFRUIT_GFX* _u8g2;
|
||
BuilderFn _builder;
|
||
void* _builderCtx;
|
||
DisplayList _list;
|
||
|
||
static const int MAX_TWEENS = 8;
|
||
Tween _tweens[MAX_TWEENS];
|
||
|
||
// 双缓冲相关
|
||
bool _useBackBuffer = false;
|
||
GFXcanvas16* _canvasA = nullptr; // 当前绘制目标
|
||
GFXcanvas16* _canvasB = nullptr; // 上一帧
|
||
GFXcanvas16* _canvasCurrent = nullptr;
|
||
GFXcanvas16* _canvasPrev = nullptr;
|
||
uint16_t _bufW = 0;
|
||
uint16_t _bufH = 0;
|
||
};
|
||
|
||
} // namespace ui
|
||
} // namespace esp32
|
||
|
||
#endif // UI_H
|