【LVGL】滑动切换页面的界面优化实践

这个 Demo 基于 LVGL 实现滑动切换主页界面,完成了背景美化、图标高亮展示、动态标题说明和按钮按压反馈等交互效果。

cpp 复制代码
#include "lvgl/lvgl.h"

#define NUM_ICONS 5
#define CENTER_ZOOM 420
#define SIDE_ZOOM   150
#define FAR_ZOOM     96
#define CENTER_OPA  255
#define SIDE_OPA    102
#define FAR_OPA      48
#define ICON_WIDTH  120
#define ICON_HEIGHT 120
#define CENTER_SIZE 196
#define SIDE_SIZE    84
#define FAR_SIZE     58
#define DRAG_STEP_X 160
#define ITEM_SPACING 220
#define VISIBLE_COUNT 3
#define HOME_BG_COLOR_HEX 0x123B22
#define INFO_TEXT_COLOR_HEX 0xF6D32D
#define ICON_BG_COLOR_HEX 0x2A1240

static lv_obj_t * container;
static lv_obj_t * icons[NUM_ICONS];
static lv_obj_t * icon_symbols_obj[NUM_ICONS];
static lv_obj_t * icon_info_label;
static lv_style_t style_icon;
static lv_style_t style_icon_pressed;
static lv_style_t style_icon_release_transition;

static float scroll_offset = 0.0f;
static int pressed_x = 0;
static float pressed_offset = 0;
static bool is_dragging = false;
static int current_center_idx = -1;

static const lv_color_t icon_colors[NUM_ICONS] = {
    LV_COLOR_MAKE(0x87, 0xF5, 0xC2),
    LV_COLOR_MAKE(0xFF, 0xDE, 0x73),
    LV_COLOR_MAKE(0xFA, 0x4F, 0x0A),
    LV_COLOR_MAKE(0xF7, 0x22, 0x22),
    LV_COLOR_MAKE(0xB7, 0x9D, 0xFF),
};

static const char * icon_symbols[NUM_ICONS] = {
    LV_SYMBOL_WIFI,
    LV_SYMBOL_BELL,
    LV_SYMBOL_BATTERY_FULL,
    LV_SYMBOL_AUDIO,
    LV_SYMBOL_BLUETOOTH,
};

static const char * icon_names[NUM_ICONS] = {
    "WiFi",
    "Bell",
    "Battery",
    "Audio",
    "Bluetooth",
};

static float clampf(float value, float min_value, float max_value)
{
    if (value < min_value) return min_value;
    if (value > max_value) return max_value;
    return value;
}

static float absf(float value)
{
    return value < 0.0f ? -value : value;
}

static float lerpf(float start, float end, float t)
{
    return start + (end - start) * t;
}

static float smoothstepf(float t)
{
    t = clampf(t, 0.0f, 1.0f);
    return t * t * (3.0f - 2.0f * t);
}

static lv_color_t darken_icon_filter(const lv_color_filter_dsc_t * dsc, lv_color_t color, lv_opa_t opa)
{
    LV_UNUSED(dsc);
    return lv_color_darken(color, opa);
}

static int get_center_icon_index(void)
{
    int idx = (int)(scroll_offset + 0.5f);

    if (idx < 0) idx = 0;
    if (idx >= NUM_ICONS) idx = NUM_ICONS - 1;

    return idx;
}

static void update_icon_info_label(void)
{
    int idx;

    if (icon_info_label == NULL) return;

    idx = get_center_icon_index();
    if (idx == current_center_idx) return;

    current_center_idx = idx;
    lv_label_set_text(icon_info_label, icon_names[idx]);
}

static void update_stack_order(const float * scores)
{
    bool used[NUM_ICONS] = { false };

    for (int order = 0; order < NUM_ICONS; order++) {
        int pick = -1;
        float best_score = -1000000.0f;

        for (int i = 0; i < NUM_ICONS; i++) {
            if (used[i]) continue;

            if (pick < 0 || scores[i] < best_score) {
                pick = i;
                best_score = scores[i];
            }
        }

        used[pick] = true;
        lv_obj_move_foreground(icons[pick]);
    }
}

static void recalc_layout(void)
{
    lv_disp_t * disp = lv_disp_get_default();
    lv_coord_t cx = disp ? lv_disp_get_hor_res(disp) / 2 : 400;
    lv_coord_t cy = disp ? lv_disp_get_ver_res(disp) / 2 : 240;
    float stack_scores[NUM_ICONS];
    bool visible[NUM_ICONS] = { false };

    for (int i = 0; i < NUM_ICONS; i++) {
        stack_scores[i] = -1000000.0f;
    }

    for (int shown = 0; shown < VISIBLE_COUNT; shown++) {
        int pick = -1;
        float best_dist = 1000000.0f;

        for (int i = 0; i < NUM_ICONS; i++) {
            if (visible[i]) continue;

            float dist = absf((float)i - scroll_offset);
            if (pick < 0 || dist < best_dist) {
                pick = i;
                best_dist = dist;
            }
        }

        if (pick >= 0) visible[pick] = true;
    }

    for (int i = 0; i < NUM_ICONS; i++) {
        if (!visible[i]) {
            lv_obj_add_flag(icons[i], LV_OBJ_FLAG_HIDDEN);
            continue;
        }

        float dist = (float)i - scroll_offset;
        float abs_dist = absf(dist);
        float near_t = clampf(abs_dist, 0.0f, 1.0f);
        float far_t = clampf(abs_dist - 1.0f, 0.0f, 1.0f);

        float size = lerpf((float)CENTER_SIZE, (float)SIDE_SIZE, near_t);
        float zoom = lerpf((float)CENTER_ZOOM, (float)SIDE_ZOOM, near_t);
        float opa = lerpf((float)CENTER_OPA, (float)SIDE_OPA, near_t);

        if (abs_dist > 1.0f) {
            size = lerpf((float)SIDE_SIZE, (float)FAR_SIZE, far_t);
            zoom = lerpf((float)SIDE_ZOOM, (float)FAR_ZOOM, far_t);
            opa = lerpf((float)SIDE_OPA, (float)FAR_OPA, far_t);
        }

        lv_coord_t icon_size = (lv_coord_t)size;
        lv_coord_t x = (lv_coord_t)(cx + dist * ITEM_SPACING - icon_size / 2);
        lv_coord_t y = cy - icon_size / 2;
        lv_opa_t obj_opa = (lv_opa_t)opa;
        lv_opa_t shadow_opa = obj_opa > 60 ? (lv_opa_t)(obj_opa - 60) : 0;

        lv_obj_clear_flag(icons[i], LV_OBJ_FLAG_HIDDEN);
        lv_obj_set_size(icons[i], icon_size, icon_size);
        lv_obj_set_pos(icons[i], x, y);
        lv_obj_set_style_bg_opa(icons[i], obj_opa, 0);
        lv_obj_set_style_shadow_opa(icons[i], shadow_opa, 0);
        lv_obj_set_style_transform_zoom(icon_symbols_obj[i], (lv_coord_t)zoom, 0);
        lv_obj_set_style_text_opa(icon_symbols_obj[i], obj_opa, 0);
        lv_obj_update_layout(icon_symbols_obj[i]);
        lv_obj_set_style_transform_pivot_x(icon_symbols_obj[i], lv_obj_get_width(icon_symbols_obj[i]) / 2, 0);
        lv_obj_set_style_transform_pivot_y(icon_symbols_obj[i], lv_obj_get_height(icon_symbols_obj[i]) / 2, 0);
        lv_obj_center(icon_symbols_obj[i]);
        stack_scores[i] = size;
    }

    update_stack_order(stack_scores);
    if (icon_info_label != NULL) lv_obj_move_foreground(icon_info_label);
    update_icon_info_label();
}

static void scroll_anim_cb(void * var, int32_t value)
{
    scroll_offset = (float)value / 100.0f;
    recalc_layout();
}

static void start_drag(lv_event_t * e)
{
    lv_indev_t * indev = lv_event_get_indev(e);
    lv_point_t point;

    if (indev == NULL) indev = lv_indev_get_act();
    if (indev == NULL) return;

    lv_anim_del(&scroll_offset, scroll_anim_cb);

    lv_indev_get_point(indev, &point);
    pressed_x = point.x;
    pressed_offset = scroll_offset;
    is_dragging = true;
}

static void update_drag(lv_event_t * e)
{
    lv_indev_t * indev = lv_event_get_indev(e);
    lv_point_t point;

    if (indev == NULL) indev = lv_indev_get_act();
    if (indev == NULL) return;

    if (!is_dragging) {
        start_drag(e);
    }

    lv_indev_get_point(indev, &point);

    int dx = point.x - pressed_x;
    float delta = dx / (float)DRAG_STEP_X;
    scroll_offset = pressed_offset - delta;

    if (scroll_offset < 0) scroll_offset = 0;
    if (scroll_offset > NUM_ICONS - 1) scroll_offset = NUM_ICONS - 1;

    recalc_layout();
}

static void snap_to_center(int target_idx)
{
    if (target_idx < 0) target_idx = 0;
    if (target_idx >= NUM_ICONS) target_idx = NUM_ICONS - 1;

    lv_anim_t anim;
    lv_anim_init(&anim);
    lv_anim_set_var(&anim, &scroll_offset);
    lv_anim_set_values(&anim, (int)(scroll_offset * 100), target_idx * 100);
    lv_anim_set_time(&anim, 300);
    lv_anim_set_exec_cb(&anim, scroll_anim_cb);
    lv_anim_set_path_cb(&anim, lv_anim_path_ease_out);
    lv_anim_start(&anim);
}

static void event_handler(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);

    if (code == LV_EVENT_PRESSED) {
        start_drag(e);
    } else if (code == LV_EVENT_PRESSING) {
        update_drag(e);
    } else if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
        if (!is_dragging) return;
        is_dragging = false;
        snap_to_center((int)(scroll_offset + 0.5f));
    }
}

void demo_carousel(void)
{
    static lv_style_prop_t press_transition_props[] = {
        LV_STYLE_TRANSLATE_Y,
        LV_STYLE_SHADOW_WIDTH,
        LV_STYLE_SHADOW_OFS_Y,
        LV_STYLE_SHADOW_OPA,
        0
    };
    static lv_style_transition_dsc_t transition_dsc_def;
    static lv_style_transition_dsc_t transition_dsc_pr;
    static lv_color_filter_dsc_t color_filter;

    lv_style_init(&style_icon);
    lv_style_set_bg_color(&style_icon, lv_color_hex(ICON_BG_COLOR_HEX));
    lv_style_set_bg_opa(&style_icon, 255);
    lv_style_set_radius(&style_icon, LV_RADIUS_CIRCLE);
    lv_style_set_border_width(&style_icon, 0);
    lv_style_set_shadow_width(&style_icon, 4);
    lv_style_set_shadow_color(&style_icon, lv_color_black());
    lv_style_set_shadow_opa(&style_icon, 30);
    lv_style_set_shadow_ofs_y(&style_icon, 2);
    lv_style_set_pad_all(&style_icon, 10);

    lv_style_transition_dsc_init(&transition_dsc_def, press_transition_props, lv_anim_path_ease_out, 140, 40, NULL);
    lv_style_transition_dsc_init(&transition_dsc_pr, press_transition_props, lv_anim_path_ease_in_out, 90, 0, NULL);

    lv_color_filter_dsc_init(&color_filter, darken_icon_filter);

    lv_style_init(&style_icon_release_transition);
    lv_style_set_transition(&style_icon_release_transition, &transition_dsc_def);

    lv_style_init(&style_icon_pressed);
    lv_style_set_translate_y(&style_icon_pressed, 4);
    lv_style_set_shadow_width(&style_icon_pressed, 1);
    lv_style_set_shadow_ofs_y(&style_icon_pressed, 0);
    lv_style_set_shadow_opa(&style_icon_pressed, 12);
    lv_style_set_color_filter_dsc(&style_icon_pressed, &color_filter);
    lv_style_set_color_filter_opa(&style_icon_pressed, LV_OPA_20);
    lv_style_set_transition(&style_icon_pressed, &transition_dsc_pr);

    lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(HOME_BG_COLOR_HEX), 0);
    lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0);

    container = lv_obj_create(lv_scr_act());
    lv_obj_set_size(container, lv_obj_get_width(lv_scr_act()), lv_obj_get_height(lv_scr_act()));
    lv_obj_center(container);
    lv_obj_clear_flag(container, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_add_flag(container, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
    lv_obj_set_style_bg_color(container, lv_color_hex(HOME_BG_COLOR_HEX), 0);
    lv_obj_set_style_bg_opa(container, LV_OPA_COVER, 0);
    lv_obj_set_style_border_width(container, 0, 0);
    lv_obj_set_style_radius(container, 0, 0);
    lv_obj_set_style_pad_all(container, 0, 0);
    lv_obj_add_event_cb(container, event_handler, LV_EVENT_ALL, NULL);

    icon_info_label = lv_label_create(container);
    lv_obj_set_style_text_color(icon_info_label, lv_color_hex(INFO_TEXT_COLOR_HEX), 0);
    lv_obj_set_style_text_font(icon_info_label, &lv_font_montserrat_34, 0);
    lv_label_set_text(icon_info_label, "");
    lv_obj_align(icon_info_label, LV_ALIGN_TOP_RIGHT, -24, 20);

    for (int i = 0; i < NUM_ICONS; i++) {
        icons[i] = lv_obj_create(container);
        lv_obj_set_size(icons[i], ICON_WIDTH, ICON_HEIGHT);
        lv_obj_add_style(icons[i], &style_icon, 0);
        lv_obj_add_style(icons[i], &style_icon_release_transition, 0);
        lv_obj_add_style(icons[i], &style_icon_pressed, LV_STATE_PRESSED);
        lv_obj_clear_flag(icons[i], LV_OBJ_FLAG_SCROLLABLE);
        lv_obj_add_flag(icons[i], LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_EVENT_BUBBLE | LV_OBJ_FLAG_PRESS_LOCK | LV_OBJ_FLAG_OVERFLOW_VISIBLE);

        icon_symbols_obj[i] = lv_label_create(icons[i]);
        lv_label_set_text(icon_symbols_obj[i], icon_symbols[i]);
        lv_obj_set_style_text_color(icon_symbols_obj[i], icon_colors[i], 0);
        lv_obj_set_style_text_font(icon_symbols_obj[i], &lv_font_montserrat_48, 0);
        lv_obj_update_layout(icon_symbols_obj[i]);
        lv_obj_center(icon_symbols_obj[i]);
    }

    scroll_offset = 2.0f;
    recalc_layout();
}
相关推荐
Scott9999HH1 天前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社1 天前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海1 天前
python 列表、元组、集合和字典
开发语言·python
2501_916008891 天前
iOS应用开发工具全面解析:如何选择与优化开发效率
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
a4493153621 天前
iPhone Face ID 故障的分层诊断——原深感系统与主板加密配对机制
macos·ios·电脑·cocoa·iphone
萧瑟余晖1 天前
JDK 26 新特性详解
java·开发语言
白玉cfc1 天前
【iOS】内存五大分区
macos·ios·cocoa
马优晨1 天前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区1 天前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大1 天前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf