【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();
}
相关推荐
雨师@1 小时前
go语言项目--实例化(图书管理)--005
开发语言·后端·golang
Aspiresky1 小时前
探索Rust语言之引用
开发语言·后端·rust
天空'之城2 小时前
Linux 系统编程 10:线程同步
linux·开发语言·系统编程·线程同步
Vect__2 小时前
Go 数据结构 slice 深度剖析
开发语言·数据结构·golang
想你依然心痛2 小时前
AtomCode在Python数据科学项目中的使用体验:从数据分析到可视化
开发语言·python·数据分析
满天星83035772 小时前
【Qt】控件(二) (geometry及与frameGeometry的区别)
开发语言·qt
威武的花瓣2 小时前
调用Page.RegisterAsyncTask()的异步页
ios·iphone
Esaka_Forever2 小时前
Python 与 JS (V8) 垃圾回收核心区别 + 底层根源分析
开发语言·javascript·jvm
pp起床2 小时前
黑马点评 - 短信验证码登录实现
java·开发语言·tomcat