侧边栏壁纸
博主头像
潦草地博主等级

行动起来,活在当下

  • 累计撰写 8 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

LVGL网格列表实现

Storen
2025-02-19 / 0 评论 / 0 点赞 / 15 阅读 / 17648 字

网格列表

1、定义图标结构体

图标大小+中心点坐标

// 保存网格中图标大小、位置XY的结构体
typedef struct {
    float size;
    float center_x;
    float center_y;
} grid_item_info;

2、构建网格数据

使用二维数组保存数据

/**
 * 构建指定行数的网格数据(rows*3)(正常大小、无缩放/偏移)
 */
grid_item_info **build_gird_items(int rows)
{
    grid_item_info **items = (grid_item_info **)malloc(rows * sizeof(grid_item_info *));

    for (int i = 0; i < rows; i++)
    {
        items[i] = (grid_item_info *)malloc(3 * sizeof(grid_item_info));
        init_row_data(items[i], i);
    }

    return items;
}

3、排列图标

两重For循环创建图标控件,并设置到指定位置

    // 创建网格项
    for (int i = 0; i < panel_data->row_count; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            lv_obj_t *child = create_item(
                panel,
                panel_data->grid_items[i][j].size,
                panel_data->grid_items[i][j].center_x,
                panel_data->grid_items[i][j].center_y + panel_data->total_scroll_distance);

            // 创建并初始化拖拽用户数据
            drag_user_data_t *drag_data = (drag_user_data_t *)malloc(sizeof(drag_user_data_t));
            memset(drag_data, 0, sizeof(drag_user_data_t));
            drag_data->pressed_item_index = -1;
            drag_data->last_pressed_point_x = -1;
            drag_data->last_pressed_point_y = -1;
            lv_obj_set_user_data(child, drag_data);

            // 添加事件回调
            lv_obj_add_flag(child, LV_OBJ_FLAG_CLICKABLE);
            lv_obj_add_event_cb(child, child_event_cb, LV_EVENT_LONG_PRESSED, NULL);
            lv_obj_add_event_cb(child, child_event_cb, LV_EVENT_PRESSING, NULL);
            lv_obj_add_event_cb(child, child_event_cb, LV_EVENT_RELEASED, NULL);
        }
    }

4、滑动处理

获取滑动距离,然后更新所有子控件

    // 获取点击坐标
    lv_event_code_t code = lv_event_get_code(e);
    lv_indev_t *indev = lv_event_get_indev(e);
    lv_point_t point;
    lv_indev_get_point(indev, &point);

...
    panel_data->slide_diff_y = point.y - panel_data->last_point_y;
    panel_data->last_point_y = point.y;
    // 累加总的滑动距离
    add_scroll_distance(panel_data->slide_diff_y, panel_data);

...
    // For循环遍历所有子控件,更新滑动后的位置
    for (size_t i = 0; i < lv_obj_get_child_cnt(panel); i++)
    {
        lv_obj_t *child = lv_obj_get_child(panel, i);
        lv_coord_t child_y = lv_obj_get_y(child);
        lv_coord_t child_h = lv_obj_get_height(child);
        grid_item_info info = panel_data->grid_items[i / 3][i % 3];
        update_item(child, info.size, info.center_x, info.center_y + panel_data->total_scroll_distance);
    }

惯性滚动

1、计算速度

使用上一次的滑动距离作为开始速度

    // 计算滑动距离
    panel_data->slide_diff_y = point.y - panel_data->last_point_y;
    panel_data->last_point_y = point.y;

        // 使用上一次滑动距离作为惯性滚动的开始速度
        panel_data->fling_start_diff_y = panel_data->slide_diff_y;

2、惯性滚动动画

重点:动画时长、滚动减速、动画结束后自动对齐图标

    if (code == LV_EVENT_RELEASED)
    {
        // 使用上一次滑动距离作为惯性滚动的开始速度
        panel_data->fling_start_diff_y = panel_data->slide_diff_y;
        printf("SPEED = %d\n", panel_data->fling_start_diff_y);
        panel_data->last_point_y = -1;
        panel_data->slide_diff_y = 0;
        panel_data->no_slide_flag = true;

        // 构建惯性滚动动画
        lv_anim_init(&panel_data->fling_anim);
        lv_anim_set_var(&panel_data->fling_anim, panel);
        lv_anim_set_values(&panel_data->fling_anim, 0, 1000);
        lv_anim_set_exec_cb(&panel_data->fling_anim, fling_animation_cb);
        lv_anim_set_path_cb(&panel_data->fling_anim, lv_anim_path_ease_out);

        // 依据开始速度,计算动画结束时间
        int fling_time = abs(panel_data->fling_start_diff_y) * 10; // lv_anim_speed_to_time(10, 0, abs(panel_data->fling_start_diff_y));
        lv_anim_set_time(&panel_data->fling_anim, fling_time);
        lv_anim_start(&panel_data->fling_anim);
        return;
    }
/**
 * 惯性滚动动画的回调
 */
static void fling_animation_cb(void *var, int v)
{
    lv_obj_t *panel = (lv_obj_t *)var;
    panel_user_data_t *panel_data = (panel_user_data_t *)lv_obj_get_user_data(panel);

    // 按动画进度计算出当前滚动速度,然后更新滑动总距离
    float anim_value = v * 1.0f / 1000;
    add_scroll_distance(panel_data->fling_start_diff_y * (1 - anim_value), panel_data);

    float scroll_factor = panel_data->total_scroll_distance * 1.0f / app_item_size;
    transform_grid(panel_data->grid_items, panel_data->row_count, -scroll_factor);

    // 遍历子控件,更新子控件滑动后的位置
    for (size_t i = 0; i < lv_obj_get_child_cnt(panel); i++)
    {
        lv_obj_t *child = lv_obj_get_child(panel, i);
        grid_item_info info = panel_data->grid_items[i / 3][i % 3];
        update_item(child, info.size, info.center_x,
                    info.center_y + panel_data->total_scroll_distance);
    }

    // 动画结束后执行自动对齐动画
    if (anim_value == 1)
       ....
    }
}

自动对齐

1、计算对齐图标的最短滚动距离

总的滑动距离 对 图标的高度 求余,然后和 图标高度/2 比较大小。

        panel_data->snap_start_distance = panel_data->total_scroll_distance;
        panel_data->snap_distance_consumed = 0;
        // 计算出图标对齐需要的最短滚动距离
        panel_data->snap_distance = (distance > app_item_size / 2) ? distance - app_item_size : distance;

2、自动对齐动画

        // 构建动画并执行
        lv_anim_init(&panel_data->snap_anim);
        lv_anim_set_var(&panel_data->snap_anim, panel);
        lv_anim_set_values(&panel_data->snap_anim, 0, 1000);
        lv_anim_set_exec_cb(&panel_data->snap_anim, snap_animation_cb);
        lv_anim_set_path_cb(&panel_data->snap_anim, lv_anim_path_ease_in);

        int snap_time = abs(panel_data->snap_distance) * 10; // lv_anim_speed_to_time(50, 0, panel_data->snap_distance);
        lv_anim_set_time(&panel_data->snap_anim, snap_time);
        lv_anim_start(&panel_data->snap_anim);
/**
 * 图标自动对其动画的回调
 */
static void snap_animation_cb(void *var, int v)
{
    lv_obj_t *panel = (lv_obj_t *)var;
    panel_user_data_t *panel_data = (panel_user_data_t *)lv_obj_get_user_data(panel);

    // 按动画进度计算出当前滚动速度,然后更新滑动总距离
    float anim_value = v * 1.0f / 1000;
    panel_data->snap_distance_consumed = panel_data->snap_distance * anim_value;

    add_scroll_distance(panel_data->snap_start_distance +
                            panel_data->snap_distance_consumed - panel_data->total_scroll_distance,
                        panel_data);

    float scroll_factor = panel_data->total_scroll_distance * 1.0f / app_item_size;
    transform_grid(panel_data->grid_items, panel_data->row_count, -scroll_factor);

    // 遍历子控件,更新子控件滑动后的位置
    for (size_t i = 0; i < lv_obj_get_child_cnt(panel); i++)
    {
        lv_obj_t *child = lv_obj_get_child(panel, i);
        grid_item_info info = panel_data->grid_items[i / 3][i % 3];
        update_item(child, info.size, info.center_x,
                    info.center_y + panel_data->total_scroll_distance);
    }
}

拖动排序

1、开始拖动

记录拖动控件的信息(下标、拖动前的位置等信息)

    if (code == LV_EVENT_LONG_PRESSED && !drag_data->child_slide_flag)
    {
        // 拖动控件的下标
        drag_data->pressed_item_index = lv_obj_get_index(item);
        drag_data->pressed_item = item;
        drag_data->first_pressed_point_x = point.x;
        drag_data->first_pressed_point_y = point.y;
        // 拖动前的位置信息等
        lv_coord_t size = lv_obj_get_width(drag_data->pressed_item);
        lv_coord_t center_x = lv_obj_get_x(drag_data->pressed_item) + size / 2;
        lv_coord_t center_y = lv_obj_get_y(drag_data->pressed_item) + size / 2;
        drag_data->default_center_x = center_x;
        drag_data->default_center_y = center_y;
        printf("child_event_cb skip. 5\n");
        return;
    }

2、图标跟手

计算拖动距离,更新拖动图标位置

    lv_coord_t size = lv_obj_get_width(drag_data->pressed_item);
    lv_coord_t center_x = lv_obj_get_x(drag_data->pressed_item) + size / 2;
    lv_coord_t center_y = lv_obj_get_y(drag_data->pressed_item) + size / 2;
    lv_obj_move_foreground(drag_data->pressed_item);
    lv_obj_set_style_border_color(drag_data->pressed_item, lv_color_hex(0xff0000), LV_PART_MAIN);
    // 计算图标拖动距离,更新图标位置
    lv_coord_t cx = drag_data->default_center_x + (point.x - drag_data->first_pressed_point_x);
    lv_coord_t cy = drag_data->default_center_y + (point.y - drag_data->first_pressed_point_y);
    update_item(drag_data->pressed_item, size, cx, cy);

3、拖动结束

找到拖动的终点/目标位置,执行图标交换动画

        if (drag_data->pressed_item != NULL && drag_data->pressed_item_index >= 0)
        {
            // 找到拖动的目标位置(下标)
            int target = find_item_index_under(point.x, point.y, panel_data);
            if (target < 0)
            {
                lv_obj_move_to_index(drag_data->pressed_item, drag_data->pressed_item_index);
            }
            else
            {
                // 重置图标的属性
                lv_obj_set_style_opa(drag_data->pressed_item, 0, LV_PART_MAIN);
                lv_obj_move_to_index(drag_data->pressed_item, drag_data->pressed_item_index);
                lv_obj_set_style_border_color(drag_data->pressed_item, lv_color_hex(0xffffff), LV_PART_MAIN);
                // 开始交换位置
                change_order(drag_data->pressed_item->parent, drag_data->pressed_item_index, target);
            }
        }

4、计算交换动画的开始数据和结束数据

froms、tos

    // 计算动画过程中的关键数据:图标动画前的位置信息(froms),图标动画后的位置信息(tos)
    for (int i = 0; i < size; i++)
    {
        int index = source < target ? source + i : source - i;
        lv_obj_t *child = lv_obj_get_child(panel, index);

        // 图标动画前的位置信息(froms)
        panel_data->order_data->froms[i] = (grid_item_info){
            .size = lv_obj_get_width(child),
            .center_x = lv_obj_get_x(child) + lv_obj_get_width(child) / 2,
            .center_y = lv_obj_get_y(child) + lv_obj_get_height(child) / 2};

        // 图标动画后的位置信息(tos)
        if (i == 0)
        {   
            // source位置的图标需要拖动到target位置,不是回到前一个位置
            init_grid_item(&panel_data->order_data->tos[i], target);
            panel_data->order_data->tos[i].center_y += panel_data->total_scroll_distance;
        }
        else
        {
            // 非source位置图标,统统往前移动
            int pre_index = index + (source < target ? -1 : 1);
            init_grid_item(&panel_data->order_data->tos[i], pre_index);
            panel_data->order_data->tos[i].center_y += panel_data->total_scroll_distance;
        }
    }

5、开启排序动画

    // 启动动画
    lv_anim_init(&panel_data->range_anim);
    lv_anim_set_var(&panel_data->range_anim, panel);
    lv_anim_set_values(&panel_data->range_anim, 0, 1000);
    lv_anim_set_exec_cb(&panel_data->range_anim, range_animation_cb);
    lv_anim_set_path_cb(&panel_data->range_anim, lv_anim_path_linear);
    lv_anim_set_time(&panel_data->range_anim, 450);
    lv_anim_start(&panel_data->range_anim);
/**
 * 图标拖动后排序动画的回调
 */
static void range_animation_cb(void *var, int v)
{
    lv_obj_t *panel = (lv_obj_t *)var;
    panel_user_data_t *panel_data = (panel_user_data_t *)lv_obj_get_user_data(panel);
    order_animation_data_t *order_data = panel_data->order_data;

    float anim_value = v * 1.0f / 1000;
    int size = abs(order_data->target - order_data->source) + 1;
    lv_obj_t *source_item = NULL;

    // 遍历位置改变的图标
    for (int i = 0; i < size; i++)
    {
        int index = order_data->source + (order_data->source < order_data->target ? i : -i);
        lv_obj_t *child = lv_obj_get_child(panel, index);
        // 拿到该图标动画前和动画后的位置信息
        grid_item_info from = order_data->froms[i];
        grid_item_info to = order_data->tos[i];

        // 按动画进度更新图标位置
        update_item(
            child,
            from.size + (to.size - from.size) * anim_value,
            from.center_x + (to.center_x - from.center_x) * anim_value,
            from.center_y + (to.center_y - from.center_y) * anim_value);

        if (i == 0 && anim_value > 0.1f)
        {
            lv_obj_set_style_opa(child, 255, LV_PART_MAIN);
        }

        // 动画结束,更新图标的Index(current->previous)
        if (anim_value == 1.0f)
        {
            if (i != 0)
            {
                int pre_index = index + (order_data->source < order_data->target ? -1 : 1);
                lv_obj_move_to_index(child, pre_index);
            }
            else
            {
                source_item = child;
            }
        }
    }

    // 动画结束,更新图标的Index(source->target)
    if (anim_value == 1.0f && source_item != NULL)
    {
        lv_obj_move_to_index(source_item, order_data->target);
    }
}

图标缩放

1、定义图标转换/变化参数表

// 保存图标缩放值、位置数据的结构体
typedef struct {
    float scale;
    float offset_x;
    float offset_y;
} transform_info;
/**
 * 5*3的图标变换信息表
 */
const transform_info transforms[5][3] = {
    // transform_info{scale, offset_x, offset_y}
    {{+0.32, +0.08, +0.10}, {+0.34, +0.00, +0.05}, {+0.32, -0.08, +0.10}},
    {{+0.75, +0.01, +0.03}, {+1.00, +0.00, -0.02}, {+0.75, -0.01, +0.03}},
    {{+1.00, -0.04, +0.00}, {+1.20, +0.00, +0.00}, {+1.00, +0.04, +0.00}},
    {{+0.75, +0.01, -0.03}, {+1.00, +0.00, +0.02}, {+0.75, -0.01, -0.03}},
    {{+0.32, +0.08, -0.10}, {+0.34, +0.00, -0.05}, {+0.32, -0.08, -0.10}},
};

2、将参数表应用到网格列表

/**
 * 依据transforms设置的缩放等数据,对网格列表进行缩放、偏移
 * 
 * @param rows 列表总行数
 * @param scroll_row_number 当前滚动的行数,可以为为小数
 */
void transform_grid(grid_item_info **items, int rows, float scroll_row_number)
{
    float start_pos = scroll_row_number;
    int start_index = (int)start_pos;
    float fraction = start_pos - start_index;

    if (fraction < 0.0f)
    {
        start_index--;
        fraction += 1.0f;
    }

    // 重置图标数据为无缩放状态
    for (int i = 0; i < rows; i++)
    {
        init_row_data(items[i], i);
    }

    // 按行遍历数据,从start_index往后遍历5行
    for (int i = 0; i < 5 + 1; i++)
    {
        int base_index = i + start_index;
        // 行数边界判断
        if (base_index >= 0 && base_index < rows)
        {
            transform_row(items[base_index], i + fraction);
        }
    }
}
/**
 * 依据transforms设置的缩放等数据,对网格中的某一行进行变换(图标大小缩放、位置XY偏移)
 * 
 * @param transform_factor 表示当前这一行Items,相对于相对于第一行的位置,往下方 移动/滑动/偏移 了多少行(小数)
 */
static void transform_row(grid_item_info *row, float transform_factor)
{
    int transform_index = (int)transform_factor;
    float fraction = transform_factor - transform_index;

    for (int i = 0; i < 3; i++)
    {
        // 从transforms表中拿到当前整数行对应的缩放值、偏移值
        float scale = transforms[transform_index][i].scale;
        float offset_x = transforms[transform_index][i].offset_x;
        float offset_y = transforms[transform_index][i].offset_y;

        // 如果滚动的行数不是整数,还有部分小数,则使用线性插值的方式计算出合适的中间值
        if (fraction != 0.0f)
        {
            float offset_x_outer = (i == 0) ? 0.08f : (i == 2 ? -0.08f : 0.0f);
            // 当前行的“变换”数据
            transform_info current = (transform_index < 5) ? transforms[transform_index][i] : (transform_info){0, offset_x_outer, -0.21f};
            // 上一行的“变换”数据
            transform_info previous = (transform_index > 0) ? transforms[transform_index - 1][i] : (transform_info){0, offset_x_outer, +0.21f};

            // 线性插值
            scale = previous.scale * fraction + current.scale * (1.0f - fraction);
            offset_x = previous.offset_x * fraction + current.offset_x * (1.0f - fraction);
            offset_y = previous.offset_y * fraction + current.offset_y * (1.0f - fraction);
        }

        // 保存原始数据 缩放、偏移后 的数据
        row[i].size = row[i].size * (1.0f - (1.0f - scale) * global_transform_factor);
        row[i].center_x = row[i].center_x + (offset_x * global_transform_factor) * screen_size;
        row[i].center_y = row[i].center_y + (offset_y * global_transform_factor) * screen_size;
    }
}

0

评论区