mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-27 19:12:40 +00:00
426 lines
12 KiB
C++
426 lines
12 KiB
C++
/*
|
|
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
#include <mooncake.h>
|
|
#include <mooncake_log.h>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <smooth_ui_toolkit.hpp>
|
|
#include <smooth_lvgl.hpp>
|
|
#include <assets/assets.h>
|
|
#include <fmt/chrono.h>
|
|
#include <hal/hal.h>
|
|
#include <memory>
|
|
#include <vector>
|
|
#include <lvgl.h>
|
|
|
|
using namespace smooth_ui_toolkit;
|
|
using namespace smooth_ui_toolkit::lvgl_cpp;
|
|
|
|
/**
|
|
* @brief
|
|
*
|
|
*/
|
|
class StatuBarGesture {
|
|
public:
|
|
std::function<void(void)> onGesture;
|
|
|
|
StatuBarGesture() : _is_tracking(false), _last_state(LV_INDEV_STATE_REL)
|
|
{
|
|
}
|
|
|
|
void init()
|
|
{
|
|
_is_tracking = false;
|
|
_last_state = LV_INDEV_STATE_REL;
|
|
_screen_height = 240;
|
|
_top_threshold = 20; // 距离顶部 20 像素内触发
|
|
_swipe_min_dist = 50; // 向下滑动至少 50 像素才触发
|
|
}
|
|
|
|
void update()
|
|
{
|
|
lv_indev_t* indev = GetHAL().lvTouchpad;
|
|
if (!indev) {
|
|
return;
|
|
}
|
|
|
|
lv_indev_state_t state = lv_indev_get_state(indev);
|
|
lv_point_t curr_point;
|
|
lv_indev_get_point(indev, &curr_point);
|
|
|
|
// 1. 按下瞬间 (Transition: Released -> Pressed)
|
|
if (state == LV_INDEV_STATE_PR && _last_state == LV_INDEV_STATE_REL) {
|
|
// 只有在按下那一刻就在顶部,才标记为追踪开始
|
|
if (curr_point.y <= _top_threshold && curr_point.y >= 0) {
|
|
_start_point = curr_point;
|
|
_is_tracking = true;
|
|
} else {
|
|
_is_tracking = false; // 按下位置不对,此次滑动全程忽略
|
|
}
|
|
}
|
|
// 2. 抬起瞬间 (Transition: Pressed -> Released)
|
|
else if (state == LV_INDEV_STATE_REL && _last_state == LV_INDEV_STATE_PR) {
|
|
if (_is_tracking) {
|
|
int delta_y = curr_point.y - _start_point.y; // 向下滑为正
|
|
int delta_x = abs(curr_point.x - _start_point.x);
|
|
|
|
// 判断标准:向下位移足够,且角度偏垂直
|
|
if (delta_y > _swipe_min_dist && delta_y > delta_x) {
|
|
if (onGesture) {
|
|
onGesture();
|
|
}
|
|
}
|
|
_is_tracking = false; // 重置追踪状态
|
|
}
|
|
}
|
|
|
|
_last_state = state; // 更新状态机
|
|
}
|
|
|
|
private:
|
|
bool _is_tracking;
|
|
lv_indev_state_t _last_state; // 记录上一帧的状态
|
|
lv_point_t _start_point;
|
|
int _screen_height;
|
|
int _top_threshold;
|
|
int _swipe_min_dist;
|
|
};
|
|
|
|
namespace status_bar_view {
|
|
|
|
class Widget {
|
|
public:
|
|
virtual ~Widget() = default;
|
|
virtual void update() = 0;
|
|
};
|
|
|
|
class TimeLabel : public Widget {
|
|
public:
|
|
TimeLabel(lv_obj_t* parent, uint32_t colorText)
|
|
{
|
|
_label = std::make_unique<Label>(parent);
|
|
_label->setText("");
|
|
_label->setTextColor(lv_color_hex(colorText));
|
|
_label->setTextFont(&lv_font_montserrat_16);
|
|
_label->align(LV_ALIGN_CENTER, 0, 0);
|
|
|
|
update();
|
|
}
|
|
|
|
void update() override
|
|
{
|
|
auto now = std::chrono::system_clock::now();
|
|
auto now_t = std::chrono::system_clock::to_time_t(now);
|
|
|
|
struct tm local_tm;
|
|
localtime_r(&now_t, &local_tm);
|
|
|
|
int hour12 = local_tm.tm_hour % 12;
|
|
if (hour12 == 0) {
|
|
hour12 = 12;
|
|
}
|
|
|
|
_label->setText(fmt::format("{}:{:02d} {}", hour12, local_tm.tm_min, local_tm.tm_hour >= 12 ? "PM" : "AM"));
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Label> _label;
|
|
};
|
|
|
|
class BatteryIcon {
|
|
public:
|
|
BatteryIcon(lv_obj_t* parent, uint32_t colorSecondary, uint32_t colorPrimary)
|
|
{
|
|
_color_primary = colorPrimary;
|
|
|
|
_bat_top = std::make_unique<uitk::lvgl_cpp::Container>(parent);
|
|
_bat_top->setBgColor(lv_color_hex(colorSecondary));
|
|
_bat_top->setScrollbarMode(LV_SCROLLBAR_MODE_OFF);
|
|
_bat_top->setBorderWidth(0);
|
|
_bat_top->setSize(4, 4);
|
|
_bat_top->setRadius(4);
|
|
_bat_top->setOutlineWidth(1);
|
|
_bat_top->setOutlineColor(lv_color_hex(colorPrimary));
|
|
|
|
_bar = std::make_unique<uitk::lvgl_cpp::Bar>(parent);
|
|
_bar->setSize(30, 12);
|
|
_bar->setRadius(4);
|
|
_bar->setRadius(0, LV_PART_INDICATOR);
|
|
_bar->setBgColor(lv_color_hex(colorSecondary));
|
|
_bar->setBgColor(lv_color_hex(colorPrimary), LV_PART_INDICATOR);
|
|
_bar->setBgOpa(LV_OPA_COVER);
|
|
_bar->setRange(0, 100);
|
|
_bar->setValue(0);
|
|
_bar->setPadding(1, 1, 1, 1);
|
|
_bar->setOutlineWidth(1);
|
|
_bar->setOutlineColor(lv_color_hex(colorPrimary));
|
|
_bar->setRadius(3, LV_PART_INDICATOR);
|
|
|
|
_lightning_icon = std::make_unique<uitk::lvgl_cpp::Image>(parent);
|
|
_lightning_icon->setSrc(&icon_bat_lightning);
|
|
_lightning_icon->setImageRecolor(lv_color_hex(colorPrimary));
|
|
_lightning_icon->setImageRecolorOpa(LV_OPA_COVER);
|
|
_lightning_icon->setHidden(true);
|
|
}
|
|
|
|
void align(lv_align_t align, int32_t x_ofs, int32_t y_ofs)
|
|
{
|
|
_bar->align(align, x_ofs, y_ofs);
|
|
lv_obj_align_to(_bat_top->get(), _bar->get(), LV_ALIGN_CENTER, 15, 0);
|
|
lv_obj_align_to(_lightning_icon->get(), _bar->get(), LV_ALIGN_CENTER, 0, 0);
|
|
}
|
|
|
|
void setLevel(uint8_t level)
|
|
{
|
|
_bar->setValue(level);
|
|
}
|
|
|
|
void setCharging(bool charging)
|
|
{
|
|
if (charging) {
|
|
_bar->setBgColor(lv_color_hex(0x19C25F), LV_PART_INDICATOR);
|
|
_lightning_icon->setHidden(false);
|
|
} else {
|
|
_bar->setBgColor(lv_color_hex(_color_primary), LV_PART_INDICATOR);
|
|
_lightning_icon->setHidden(true);
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Bar> _bar;
|
|
std::unique_ptr<Container> _bat_top;
|
|
std::unique_ptr<Image> _lightning_icon;
|
|
|
|
uint32_t _color_primary = 0;
|
|
};
|
|
|
|
class Battery : public Widget {
|
|
public:
|
|
Battery(lv_obj_t* parent, uint32_t colorSecondary, uint32_t colorPrimary)
|
|
{
|
|
_label_level = std::make_unique<Label>(parent);
|
|
_label_level->setText("");
|
|
_label_level->setTextColor(lv_color_hex(colorPrimary));
|
|
_label_level->setTextFont(&lv_font_montserrat_16);
|
|
_label_level->align(LV_ALIGN_RIGHT_MID, -41, 0);
|
|
|
|
_battery_icon = std::make_unique<BatteryIcon>(parent, colorSecondary, colorPrimary);
|
|
_battery_icon->align(LV_ALIGN_RIGHT_MID, -7, 0);
|
|
|
|
update();
|
|
}
|
|
|
|
void update() override
|
|
{
|
|
auto level = GetHAL().getBatteryLevel();
|
|
_label_level->setText(fmt::format("{}%", level));
|
|
_battery_icon->setLevel(level);
|
|
_battery_icon->setCharging(GetHAL().isBatteryCharging());
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Label> _label_level;
|
|
std::unique_ptr<BatteryIcon> _battery_icon;
|
|
};
|
|
|
|
class Wifi : public Widget {
|
|
public:
|
|
Wifi(lv_obj_t* parent, uint32_t colorPrimary)
|
|
{
|
|
_wifi_icon = std::make_unique<Image>(parent);
|
|
_wifi_icon->align(LV_ALIGN_LEFT_MID, 11, 0);
|
|
_wifi_icon->setImageRecolor(lv_color_hex(colorPrimary));
|
|
_wifi_icon->setImageRecolorOpa(LV_OPA_COVER);
|
|
|
|
update();
|
|
}
|
|
|
|
void update() override
|
|
{
|
|
auto status = GetHAL().getWifiStatus();
|
|
switch (status) {
|
|
case WifiStatus::None:
|
|
_wifi_icon->setSrc(NULL);
|
|
break;
|
|
case WifiStatus::Low:
|
|
_wifi_icon->setSrc(&icon_wifi_low);
|
|
break;
|
|
case WifiStatus::Medium:
|
|
_wifi_icon->setSrc(&icon_wifi_medium);
|
|
break;
|
|
case WifiStatus::High:
|
|
_wifi_icon->setSrc(&icon_wifi_high);
|
|
break;
|
|
default:
|
|
_wifi_icon->setSrc(&icon_wifi_slash);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Image> _wifi_icon;
|
|
};
|
|
|
|
class StatusBarView {
|
|
public:
|
|
StatusBarView(lv_obj_t* parent, uint32_t colorSecondary, uint32_t colorPrimary)
|
|
{
|
|
_panel = std::make_unique<uitk::lvgl_cpp::Container>(lv_screen_active());
|
|
_panel->setBgColor(lv_color_hex(colorSecondary));
|
|
_panel->setScrollbarMode(LV_SCROLLBAR_MODE_OFF);
|
|
_panel->align(LV_ALIGN_TOP_MID, 0, 0);
|
|
_panel->setBorderWidth(0);
|
|
_panel->setSize(320, 28);
|
|
_panel->setRadius(0);
|
|
_panel->setPadding(0, 0, 0, 0);
|
|
_panel->onClick().connect([this]() { hide(); });
|
|
|
|
_widgets.push_back(std::make_unique<TimeLabel>(_panel->get(), colorPrimary));
|
|
_widgets.push_back(std::make_unique<Battery>(_panel->get(), colorSecondary, colorPrimary));
|
|
_widgets.push_back(std::make_unique<Wifi>(_panel->get(), colorPrimary));
|
|
|
|
_panel->setPos(0, _pos_y_hide);
|
|
_pos_y_anim.springOptions().bounce = 0.1;
|
|
_pos_y_anim.springOptions().visualDuration = 0.3;
|
|
// _pos_y_anim.easingOptions().duration = 0.3;
|
|
_pos_y_anim.teleport(_pos_y_hide);
|
|
}
|
|
|
|
void update()
|
|
{
|
|
_pos_y_anim.update();
|
|
if (!_pos_y_anim.done()) {
|
|
_panel->setPos(0, _pos_y_anim.directValue());
|
|
} else {
|
|
if (_hide_panel_flag) {
|
|
_hide_panel_flag = false;
|
|
_panel->setHidden(true);
|
|
}
|
|
}
|
|
|
|
if (GetHAL().millis() - _last_update_tick > 1000) {
|
|
_last_update_tick = GetHAL().millis();
|
|
for (auto& widget : _widgets) {
|
|
widget->update();
|
|
}
|
|
}
|
|
}
|
|
|
|
void show()
|
|
{
|
|
_panel->moveForeground();
|
|
_panel->setHidden(false);
|
|
_pos_y_anim = _pos_y_show;
|
|
_is_hidden = false;
|
|
}
|
|
|
|
void hide()
|
|
{
|
|
_pos_y_anim = _pos_y_hide;
|
|
_is_hidden = true;
|
|
_hide_panel_flag = true;
|
|
}
|
|
|
|
bool isHidden() const
|
|
{
|
|
return _is_hidden;
|
|
}
|
|
|
|
private:
|
|
const int _pos_y_show = 0;
|
|
const int _pos_y_hide = -28;
|
|
|
|
std::unique_ptr<Container> _panel;
|
|
std::vector<std::unique_ptr<Widget>> _widgets;
|
|
AnimateValue _pos_y_anim;
|
|
bool _is_hidden = false;
|
|
bool _hide_panel_flag = false;
|
|
uint32_t _last_update_tick = 0;
|
|
};
|
|
|
|
} // namespace status_bar_view
|
|
|
|
/**
|
|
* @brief
|
|
*
|
|
*/
|
|
class StatusBar {
|
|
public:
|
|
void init(lv_obj_t* parent, uint32_t colorSecondary, uint32_t colorPrimary)
|
|
{
|
|
_status_bar_gesture = std::make_unique<StatuBarGesture>();
|
|
_status_bar_gesture->onGesture = [&]() { handle_gesture(); };
|
|
_status_bar_gesture->init();
|
|
|
|
_status_bar_view = std::make_unique<status_bar_view::StatusBarView>(parent, colorSecondary, colorPrimary);
|
|
_status_bar_view->show();
|
|
_status_bar_show_tick = GetHAL().millis();
|
|
_is_first_show = true;
|
|
}
|
|
|
|
void update()
|
|
{
|
|
_status_bar_gesture->update();
|
|
_status_bar_view->update();
|
|
update_visibility();
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<StatuBarGesture> _status_bar_gesture;
|
|
std::unique_ptr<status_bar_view::StatusBarView> _status_bar_view;
|
|
uint32_t _status_bar_show_tick = 0;
|
|
bool _is_first_show = true;
|
|
|
|
void handle_gesture()
|
|
{
|
|
_status_bar_view->show();
|
|
_status_bar_show_tick = GetHAL().millis();
|
|
}
|
|
|
|
void update_visibility()
|
|
{
|
|
if (!_status_bar_view->isHidden()) {
|
|
if (GetHAL().millis() - _status_bar_show_tick > (_is_first_show ? 1800 : 6000)) {
|
|
_is_first_show = false;
|
|
_status_bar_view->hide();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief
|
|
*
|
|
*/
|
|
namespace view {
|
|
|
|
static std::unique_ptr<StatusBar> _status_bar;
|
|
|
|
void create_status_bar(uint32_t colorSecondary, uint32_t colorPrimary, lv_obj_t* parent)
|
|
{
|
|
_status_bar = std::make_unique<StatusBar>();
|
|
_status_bar->init(parent, colorSecondary, colorPrimary);
|
|
}
|
|
|
|
void update_status_bar()
|
|
{
|
|
if (_status_bar) {
|
|
_status_bar->update();
|
|
}
|
|
}
|
|
|
|
bool is_status_bar_created()
|
|
{
|
|
return _status_bar != nullptr;
|
|
}
|
|
|
|
void destroy_status_bar()
|
|
{
|
|
_status_bar.reset();
|
|
}
|
|
|
|
} // namespace view
|