问题背景:

        使用esp32读取SD卡中的文本文件显示在lvgl界面中。当我们读取一个较大的文本文件时,如果一次性加载几百KB甚至MB级别的文字,由于嵌入式设备内存资源紧张,会造成显示卡顿。

        抛开硬件问题不谈,直接将大量文本内容显示在lvgl的label标签中,也会造成lvgl的刷新缓慢,屏幕刷新出现重影。所以即使像esp32这种大容量的板子依然会由于lvgl的显示而造成刷新卡顿问题。

解决思路(基于esp32的硬件资源)

1. 针对中小型文件

        大小在100KB以内的文本(这里可以视情况而定,只要内存足够大),esp32的内存足以一次性全部读取到内存中,但是显示在lvgl标签中,一次加载超过1k文本数据还是太大,仍会造成lvgl刷新卡顿。所以我们将文本数据一次性读取到堆空间中,对lvgl显示的文本数据进行管理,每次仅显示指定数量的文字,再根据滚轮的位置判断是否需要向上或者向下换页,换页则从堆空间中取出指定大小的文本数据进行显示即可。

2. 针对大型文件:

        大小在几MB甚至更大的文本,我们无法一次性读取全部数据,那么我们只能每次读取指定大小的数据。这里需要根据界面滚动条的位置计算出文件指针的偏移量,在偏移量的基础上读取指定大小数据进行显示。

代码实现 (基于ESP-IDF 5.2.3)

1. 分页加载(针对中小型文件)

        text_parse.c

#include <stdio.h>
#include "string.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "text_parse.h"

static const char* TAG = "text_parse";

long textfile_read_All(const char* fname, text_block_data_t* text_data)
{
    char textfile_name[256] = {0};
    snprintf(textfile_name, sizeof(textfile_name), "/sdcard/%s", fname);
    FILE* fp = fopen(textfile_name, "r");
    if(!fp)
    {
        ESP_LOGE(TAG,"fopen failed!");
        return -1;
    }

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);
    // 文本大小限制
    if(fsize > 100*1024)
    {
        ESP_LOGE(TAG,"file is too big to read!");
        fclose(fp);
        return fsize;
    }
    fseek(fp, 0, SEEK_SET);     // 复位文件指针

    // 文件内容读取缓冲区
    uint8_t* read_buffer = (uint8_t*)malloc(fsize + 100);
    if(!read_buffer)
    {
        fclose(fp);
        return -1;
    }
        
    size_t read_bytes = 0;
    uint8_t* text_block_buffer = NULL;   // 读取到的数据指针
    read_bytes = fread(read_buffer, 1, fsize + 100, fp);
    if(read_bytes > 0)
    {
        text_block_buffer = (uint8_t*)malloc(read_bytes + 1);
        memcpy(text_block_buffer, read_buffer, read_bytes);
        text_block_buffer[read_bytes] = '\0';
        // 传递数据
        text_data->text_block = text_block_buffer;
        text_data->len = read_bytes;
        text_block_buffer = NULL;
    }
    else 
    {
        ESP_LOGE(TAG,"read_bytes is 0!");
        text_data->len = 0;
    }

    if(read_buffer)
    {
        free(read_buffer);
        read_buffer = NULL;
    }

    if(fp) fclose(fp);
    return fsize;
}

        text_parse.h

#ifndef _TEXT_PARSE_H_
#define _TEXT_PARSE_H_

#include "stdio.h"
#include <stdint.h>

// 读取的文本数据单元
typedef struct 
{
    uint8_t* text_block;         // 文本块
    size_t len;                  // 块大小
}text_block_data_t;

/// @brief 读取文本文件所有内容
/// @param fname 
/// @param text_data 
/// @return    返回文件大小
long textfile_read_All(const char* fname, text_block_data_t* text_data);

#endif

         lvgl显示页面 ui_text_reader.c

#include "lvgl.h"
#include "ui_text_reader.h"
#include "sdcard_file.h"
#include "text_parse.h"
#include "esp_log.h"

#define TAG     "ui_readfile"

// 页面1: 文件列表页面
static lv_obj_t* s_file_page = NULL;
static lv_obj_t* s_lv_file_title = NULL;    // 文件页面标题
static lv_obj_t* s_lv_file_list = NULL;     // 文件列表

// 页面2: Content_page
lv_obj_t* fileContent_scr;
lv_obj_t* scroll_container;
lv_obj_t* ui_nav;
lv_obj_t* file_name;
static lv_obj_t* content;

// 当前解析文件的总大小
static long fsize = 0;
// 解析出来的所有文件数据
static text_block_data_t file_data = {0};
// 当前显示页
static int current_page = 0; 

// 每页的字符数量 
#define PAGE_CHAR_COUNT 600
// 限制打开的最大文件大小
#define FILE_SIZE_MAX 100*1024 

void Content_page_init(void);

// 翻页显示
static void page_turning(void)
{
    printf("page turn...\r\n");
    static char page_text[PAGE_CHAR_COUNT + 1];
    int start_index = current_page * PAGE_CHAR_COUNT;      // 要显示数据的起始索引
    if (start_index >= file_data.len)
    {
        printf("error\r\n");
        return;
    }
        
    int end_index = start_index + PAGE_CHAR_COUNT;         // 要显示数据的结束索引
    if (end_index > file_data.len)
        end_index = file_data.len;
    memcpy(page_text, file_data.text_block + start_index, end_index - start_index);
    page_text[end_index - start_index] = '\0';
    lv_label_set_text(content, page_text);
    memset(page_text, 0, sizeof(page_text));
}

void scroll_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t* obj = lv_event_get_target(e);         // 获取触发事件的对象
    
    // 最大滚动长度 = 文本框label的高度 - 滚动容器的高度
    int scroll_y_max = lv_obj_get_height(lv_obj_get_child(obj, 0)) - lv_obj_get_height(obj);
    switch (code)
    {
    case LV_EVENT_SCROLL_END:
    {
        int y = lv_obj_get_scroll_y(obj);       // 获取当前滚轮y坐标
        // 根据滚动位置判断是否需要切换页
        if(y < 0)   
        {                   // 滚轮至顶上一页:当y为负数即向上滚动至顶时, 切换上一页
            if (current_page > 0) { 
                current_page--; 
                page_turning(); 
                lv_obj_scroll_to_y(obj, lv_obj_get_height(obj), LV_ANIM_OFF); 
            }
        }
        else if(scroll_y_max - y < 10)     // 滚轮至底下一页:最大滚动长度 - 当前滚轮y坐标 < 10 时表示滚轮已划动至页面底部
        {   
            // 计算总页数, 向上取整; 例: 200字不足一页, 仍使用一页显示
            int total_pages = (file_data.len + PAGE_CHAR_COUNT - 1) / PAGE_CHAR_COUNT;
            
            // 最后一页索引为total_pages -1
            if(current_page < total_pages -1)    
            {                   // 当未至最后一页时可翻页
                current_page++;
                page_turning();
                lv_obj_scroll_to_y(obj, 0, LV_ANIM_OFF); 
            }
        }
        break;
    }
    
    default:
        break;
    }
}

// 文件列表项事件回调
void lv_list_btn_event_cb(lv_event_t * e)
{   
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t* obj = lv_event_get_target(e);         // 获取触发事件的对象
    switch (code)
    {
    case LV_EVENT_CLICKED:
    {
        // 获取点击的列表项的文件名
        const char* fileName = lv_list_get_btn_text(s_lv_file_list, obj);
        if(fileName)
        {
            if(strstr(fileName, ".txt") || strstr(fileName, ".TXT"))
            {
                fsize = textfile_read_All(fileName, &file_data);
                if(fsize >= 0 && fsize <= FILE_SIZE_MAX)
                {
                    if(file_data.len)
                    {
                        lv_label_set_text(file_name, fileName);     // 显示文件名
                        page_turning();     // 打开文件后显示第一页内容
                    }
                    // 刷新屏幕
                    lv_obj_scroll_to(scroll_container, 0, 0, LV_ANIM_OFF);   // 重置滚动条
                    lv_scr_load_anim(fileContent_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, false);
                }
            }
        }
        break;
    }
       
    default:
        break;
    }
}

static void lv_ContentBackBtn_handle(lv_event_t* e)
{
    lv_event_code_t btn_ev = lv_event_get_code(e);         // 获取事件代码

    switch (btn_ev)
    {
    case LV_EVENT_CLICKED:
        // 释放当前解析文件所读取的所有内容
        if(file_data.text_block)
        {
            free(file_data.text_block);
            file_data.text_block = NULL;
        }
        file_data.len = 0;
        current_page = 0;      // 重置页码
        lv_scr_load_anim(s_file_page, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 300, 0, false);
        break;
      
    default:
        break;
    }
}

void ui_text_create(void)
{
    // 1.创建文件列表页面, 页面bg设置为白色
    s_file_page = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(s_file_page, lv_color_white(), 0);

    // 获取页面长宽
    lv_coord_t  hor_res = lv_disp_get_hor_res(NULL);
    lv_coord_t  ver_res = lv_disp_get_ver_res(NULL);

    // 创建文件页面标题标签
    s_lv_file_title = lv_label_create(s_file_page);
    lv_label_set_text(s_lv_file_title,"SDCard File");   // 设置标签文字
    lv_obj_align(s_lv_file_title,LV_ALIGN_TOP_MID,0,20);    // 设置标签位置: 顶端居中
    lv_obj_set_style_bg_color(s_lv_file_title, lv_color_white(), 0);        // 标签颜色为白色
    lv_obj_set_style_text_font(s_lv_file_title,&lv_font_montserrat_24,0);   // 设置标签文字大小
    lv_obj_set_style_text_color(s_lv_file_title,lv_color_black(),0);        // 设置标签文字颜色为黑色

    // 创建文件列表
    s_lv_file_list = lv_list_create(s_file_page);
    lv_obj_set_size(s_lv_file_list, hor_res-60, ver_res - 100);
    lv_obj_align(s_lv_file_list, LV_ALIGN_BOTTOM_MID, 0, -40);          // 底部居中,向上偏移40
    lv_obj_set_style_text_color(s_lv_file_list,lv_color_black(),0);     // 列表文字黑色
    lv_obj_set_style_bg_color(s_lv_file_list,lv_color_white(),0);       // 列表为白色

    // 添加文件列表
    const char (*filelist)[256] = NULL;
    int filename_cnt = sdcard_filelist(&filelist);
    // 遍历文件列表
    for (int i = 0; i < filename_cnt; i++)
    {
        lv_obj_t* btn = lv_list_add_btn(s_lv_file_list, NULL, *filelist);       // 作为按钮添加至文件列表中
        
        // 为文件列表项添加点击事件
        lv_obj_add_event_cb(btn, lv_list_btn_event_cb, LV_EVENT_CLICKED, NULL);
        filelist++;
    }
    
    // 2.创建一个文本内容浏览页面
    Content_page_init();

    lv_scr_load(s_file_page);       //加载文件页面
}

void Content_page_init(void) {

    fileContent_scr = lv_obj_create(NULL);

    // 创建可滚动的容器
    scroll_container = lv_obj_create(fileContent_scr);
    lv_obj_set_size(scroll_container, LV_HOR_RES, LV_VER_RES - 45);  // 滚动容器大小为屏幕分辨率减nav高度
    lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 45);
    lv_obj_set_style_pad_all(scroll_container, 0, 0);
    lv_obj_set_scrollbar_mode(scroll_container, LV_SCROLLBAR_MODE_AUTO);  // 启用滚动条
    lv_obj_set_scroll_dir(scroll_container, LV_DIR_VER);        // 滚动方向为垂直滚动
    lv_obj_set_style_radius(scroll_container, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_add_event_cb(scroll_container, scroll_event_cb, LV_EVENT_SCROLL_END, NULL);

    // 创建文本内容标签
    content = lv_label_create(scroll_container);
    lv_label_set_long_mode(content, LV_LABEL_LONG_WRAP);  // 设置长文本模式为自动换行
    lv_obj_set_width(content, LV_HOR_RES-10);  // 设置 label 宽度为屏幕宽度
    lv_obj_align(content, LV_ALIGN_TOP_LEFT, 5, 2);
    lv_obj_set_style_text_font(content, &lv_font_montserrat_16, 0);  

    // 创建nav顶部导航栏
    ui_nav = lv_obj_create(fileContent_scr);
    lv_obj_remove_style_all(ui_nav);
    lv_obj_set_width(ui_nav, 280);
    lv_obj_set_height(ui_nav, 45);
    lv_obj_set_align(ui_nav, LV_ALIGN_TOP_MID);
    lv_obj_set_style_bg_color(ui_nav, lv_color_hex(0xE0DFDF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_nav, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_color(ui_nav, lv_color_hex(0xffffff), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_opa(ui_nav, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_add_flag(ui_nav, LV_OBJ_FLAG_FLOATING);      // nav悬浮

    // 顶部文件名
    file_name = lv_label_create(ui_nav);
    lv_obj_align(file_name, LV_ALIGN_CENTER, 0, 2);
    lv_obj_set_style_text_font(file_name, &lv_font_montserrat_20, LV_PART_MAIN | LV_STATE_DEFAULT);

    // 顶部回退键 
    lv_obj_t* return_btn = lv_btn_create(ui_nav);
    lv_obj_set_style_shadow_opa(return_btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_size(return_btn, 40, 30);
    lv_obj_set_style_bg_color(return_btn, lv_color_hex(0xE0DFDF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_align(return_btn, LV_ALIGN_LEFT_MID, 18, 0);
    lv_obj_t* return_symbol = lv_label_create(return_btn);
    lv_obj_align(return_symbol, LV_ALIGN_CENTER, -5, 0);
    lv_obj_set_style_text_color(return_symbol, lv_color_hex(0x434343), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_label_set_text(return_symbol, LV_SYMBOL_LEFT);
    lv_obj_add_event_cb(return_btn, lv_ContentBackBtn_handle, LV_EVENT_CLICKED, NULL);
}

 

2. 动态加载 (针对大型文件)

        text_parse.c

#include <stdio.h>
#include "string.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "text_parse.h"

static const char* TAG = "text_parse";

static text_block_cfg_t block_cfg = {0};    // 解析单元配置
static uint8_t parseCfg_inited_flag = 0;    // 解析配置初始化标志
static uint8_t parse_started_flag = 0;      // 解析开始标志

static FILE* fp = NULL;    // 文件指针(指向当前正解析的文件)

void text_block_config(text_block_cfg_t* cfg)
{
    if(cfg->buff_size < 128)
        return;
    block_cfg.buff_size = cfg->buff_size;
    parseCfg_inited_flag = 1;
}

uint8_t* load_text_block(long offset)
{
    if(!parseCfg_inited_flag || !parse_started_flag 
        || fp == NULL) return NULL;
    
    // 移动文件指针位置
    fseek(fp, offset, SEEK_SET);
    
    size_t read_bytes = 0;          // 读取的字节数
    uint8_t* read_buffer = NULL;    // 读取缓冲区
    read_buffer = (uint8_t*)malloc(block_cfg.buff_size);    // 分配解析单元大小的堆内存
    if(!read_buffer)
    {
        ESP_LOGE(TAG, "malloc read_buffer failed!");
        goto clean_up;
    }
    
    uint8_t* text_block_buffer = NULL;   // 读取到的数据指针
    read_bytes = fread(read_buffer, 1, block_cfg.buff_size, fp);
    if(read_bytes > 0)
    {
        text_block_buffer = (uint8_t*)malloc(read_bytes + 1);
        if(!text_block_buffer)
        {
            ESP_LOGE(TAG, "malloc text_block_buffer failed!");
            goto clean_up;
        }
        memcpy(text_block_buffer, read_buffer, read_bytes);
        text_block_buffer[read_bytes] = '\0';  // 添加字符串终止符

        if(read_buffer)
        {
            free(read_buffer);
            read_buffer = NULL;
        }
        return text_block_buffer;
    }
    else
        ESP_LOGI(TAG, "no readbytes , maybe: file is null; read done; read fail");


clean_up:
    if(read_buffer)
    {
        free(read_buffer);
        read_buffer = NULL;
    }
    text_stop_farse();
    return NULL;
}

long start_parse_text(const char* fname)
{
    if(!parseCfg_inited_flag || parse_started_flag)
        return -1;
    
    char filename[256] = {0};
    snprintf(filename, sizeof(filename), "/sdcard/%s", fname);

    fp = fopen(filename, "r");
    if(!fp)
    {
        ESP_LOGE(TAG, "open file failed!");
        return -1;
    }

    // 计算当前解析文件大小
    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);

    parse_started_flag = 1;

    return fsize;
}

void text_stop_farse(void)
{
    if(fp)
    {
        fclose(fp);
        fp = NULL;
    }
    parse_started_flag = 0;
}

        text_parse.h 

#ifndef _TEXT_PARSE_H_
#define _TEXT_PARSE_H_

#include "stdio.h"
#include <stdint.h>

// 读取的文本数据单元
typedef struct 
{
    uint8_t* text_block;         // 文本块
    size_t len;                  // 块大小
}text_block_data_t;

// 文本单次解析大小配置
typedef struct 
{
    size_t buff_size;   
}text_block_cfg_t;

// 配置解析单元 (单次解析文本数据的大小)
void text_block_config(text_block_cfg_t* cfg);

/// @brief 开始解析文本文件
/// @param fname 文件名
/// @return     返回当前解析文件的总大小
long start_parse_text(const char* fname);

/// @brief 停止解析文件
/// @param  
void text_stop_farse(void);

/// @brief 加载文本块
/// @param offset  文件指针偏移量
/// @return uint8_t*  加载完成的文本内容指针
uint8_t* load_text_block(long offset);

#endif

         lvgl显示页面 ui_text_reader.c

#include "lvgl.h"
#include "ui_text_reader.h"
#include "sdcard_file.h"
#include "text_parse.h"
#include "esp_log.h"

#define TAG     "ui_readfile"

// 页面1: 文件列表页面
static lv_obj_t* s_file_page = NULL;
static lv_obj_t* s_lv_file_title = NULL;    // 文件页面标题
static lv_obj_t* s_lv_file_list = NULL;     // 文件列表

// 页面2: Content_page
lv_obj_t* fileContent_scr;
lv_obj_t* scroll_container;
lv_obj_t* ui_nav;
lv_obj_t* file_name;
static lv_obj_t* content;

// 分配的数据解析单元
static uint16_t parse_unit = 512;
// 当前解析文件的总大小
static long fsize = 0;     

void Content_page_init(void);

void scroll_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t* obj = lv_event_get_target(e);         // 获取触发事件的对象
    switch (code)
    {
    case LV_EVENT_SCROLL:       // 根据滑动坐标分块加载, 解决一次性加载过多数据导致内存不足或卡顿问题
    {   
        // 获取滚动位置
        lv_coord_t scroll_y = lv_obj_get_scroll_y(obj);
        // 计算当前可见区域的文本偏移量
        long offset = (scroll_y * fsize) / lv_obj_get_height(obj);
        // 确保偏移量在文件范围内
        if (offset < 0) offset = 0;
        if (offset >= fsize) offset = fsize - parse_unit;

        // 加载文本块
        const uint8_t *text = load_text_block(offset);
        if (text) {
            lv_label_set_text(content, (const char*)text);  // 更新 label 内容
            // lv_obj_scroll_to(scroll_container, 0, scroll_y, LV_ANIM_OFF);      // 滑动后保持当前滚轮坐标
        }
        // 释放堆内存
        if(text) free((uint8_t*)text);
        text = NULL;
        break;
    }

    case LV_EVENT_SCROLL_END:
    {
        // 获取滚动位置
        lv_coord_t scroll_y = lv_obj_get_scroll_y(obj);
        // 计算当前可见区域的文本偏移量
        long offset = (scroll_y * fsize) / lv_obj_get_height(obj);
        // 确保偏移量在文件范围内
        if (offset < 0) offset = 0;
        if (offset >= fsize) offset = fsize - parse_unit;

        // 加载文本块
        const uint8_t *text = load_text_block(offset);
        if (text) {
            lv_label_set_text(content, (const char*)text);  // 更新 label 内容
            lv_obj_scroll_to(scroll_container, 0, scroll_y, LV_ANIM_OFF);      // 滑动后保持当前滚轮坐标
        }
        // 释放堆内存
        if(text) free((uint8_t*)text);
        text = NULL;
        break;
    }
    
    default:
        break;
    }
}

// 文件列表项事件回调
void lv_list_btn_event_cb(lv_event_t * e)
{   
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t* obj = lv_event_get_target(e);         // 获取触发事件的对象
    static uint8_t* show_text = NULL;
    switch (code)
    {
    case LV_EVENT_CLICKED:
    {
        // 获取点击的列表项的文件名
        const char* fileName = lv_list_get_btn_text(s_lv_file_list, obj);
        if(fileName)
        {
            if(strstr(fileName, ".txt") || strstr(fileName, ".TXT"))
            {
                fsize = start_parse_text(fileName);
                if(fsize >= 0)      // 加载文本内容浏览页面
                { 
                    lv_label_set_text(file_name, fileName);
                    show_text = load_text_block(0);     // 初始加载一次文本块
                    // 显示至屏幕
                    if(show_text)
                        lv_label_set_text(content, (const char*)show_text);
                    else lv_label_set_text(content, "");
                    // 释放堆内存
                    if(show_text) free(show_text);
                    show_text = NULL;
                    // 刷新屏幕
                    lv_obj_scroll_to(scroll_container, 0, 0, LV_ANIM_OFF);   // 重置滚动条
                    lv_scr_load_anim(fileContent_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, false);
                }
            }
        }
        break;
    }
       
    default:
        break;
    }
}

static void lv_ContentBackBtn_handle(lv_event_t* e)
{
    lv_event_code_t btn_ev = lv_event_get_code(e);         // 获取事件代码

    switch (btn_ev)
    {
    case LV_EVENT_CLICKED:
        text_stop_farse();      // 返回文件列表页, 停止解析当前文件
        lv_scr_load_anim(s_file_page, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 300, 0, false);
        break;
      
    default:
        break;
    }
}

void ui_text_create(void)
{
    // 1.创建文件列表页面, 页面bg设置为白色
    s_file_page = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(s_file_page, lv_color_white(), 0);

    // 获取页面长宽
    lv_coord_t  hor_res = lv_disp_get_hor_res(NULL);
    lv_coord_t  ver_res = lv_disp_get_ver_res(NULL);

    // 创建文件页面标题标签
    s_lv_file_title = lv_label_create(s_file_page);
    lv_label_set_text(s_lv_file_title,"SDCard File");   // 设置标签文字
    lv_obj_align(s_lv_file_title,LV_ALIGN_TOP_MID,0,20);    // 设置标签位置: 顶端居中
    lv_obj_set_style_bg_color(s_lv_file_title, lv_color_white(), 0);        // 标签颜色为白色
    lv_obj_set_style_text_font(s_lv_file_title,&lv_font_montserrat_24,0);   // 设置标签文字大小
    lv_obj_set_style_text_color(s_lv_file_title,lv_color_black(),0);        // 设置标签文字颜色为黑色

    // 创建文件列表
    s_lv_file_list = lv_list_create(s_file_page);
    lv_obj_set_size(s_lv_file_list, hor_res-60, ver_res - 100);
    lv_obj_align(s_lv_file_list, LV_ALIGN_BOTTOM_MID, 0, -40);          // 底部居中,向上偏移40
    lv_obj_set_style_text_color(s_lv_file_list,lv_color_black(),0);     // 列表文字黑色
    lv_obj_set_style_bg_color(s_lv_file_list,lv_color_white(),0);       // 列表为白色

    // 添加文件列表
    const char (*filelist)[256] = NULL;
    int filename_cnt = sdcard_filelist(&filelist);
    // 遍历文件列表
    for (int i = 0; i < filename_cnt; i++)
    {
        lv_obj_t* btn = lv_list_add_btn(s_lv_file_list, NULL, *filelist);       // 作为按钮添加至文件列表中
        
        // 为文件列表项添加点击事件
        lv_obj_add_event_cb(btn, lv_list_btn_event_cb, LV_EVENT_CLICKED, NULL);
        filelist++;
    }
    
    // 2.创建一个文本内容浏览页面
    Content_page_init();

    // 初始化文本解析配置
    text_block_cfg_t block_cfg = 
    {
        .buff_size = parse_unit,
    };
    text_block_config(&block_cfg);

    lv_scr_load(s_file_page);       //加载文件页面
}

void Content_page_init(void) {

    fileContent_scr = lv_obj_create(NULL);

    // 创建可滚动的容器
    scroll_container = lv_obj_create(fileContent_scr);
    lv_obj_set_size(scroll_container, LV_HOR_RES, LV_VER_RES - 45);  // 滚动容器大小为屏幕分辨率减nav高度
    lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 45);
    lv_obj_set_style_pad_all(scroll_container, 0, 0);
    lv_obj_set_scrollbar_mode(scroll_container, LV_SCROLLBAR_MODE_AUTO);  // 启用滚动条
    lv_obj_set_scroll_dir(scroll_container, LV_DIR_VER);        // 滚动方向为垂直滚动
    lv_obj_set_style_radius(scroll_container, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_add_event_cb(scroll_container, scroll_event_cb, LV_EVENT_SCROLL | LV_EVENT_SCROLL_END, NULL);

    // 创建文本内容标签
    content = lv_label_create(scroll_container);
    lv_label_set_long_mode(content, LV_LABEL_LONG_WRAP);  // 设置长文本模式为自动换行
    lv_obj_set_width(content, LV_HOR_RES-10);  // 设置 label 宽度为屏幕宽度
    lv_obj_align(content, LV_ALIGN_TOP_LEFT, 5, 2);
    lv_obj_set_style_text_font(content, &lv_font_montserrat_16, 0);  

    // 创建nav顶部导航栏
    ui_nav = lv_obj_create(fileContent_scr);
    lv_obj_remove_style_all(ui_nav);
    lv_obj_set_width(ui_nav, 280);
    lv_obj_set_height(ui_nav, 45);
    lv_obj_set_align(ui_nav, LV_ALIGN_TOP_MID);
    lv_obj_set_style_bg_color(ui_nav, lv_color_hex(0xE0DFDF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_nav, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_color(ui_nav, lv_color_hex(0xffffff), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_opa(ui_nav, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_add_flag(ui_nav, LV_OBJ_FLAG_FLOATING);      // nav悬浮

    // 顶部文件名
    file_name = lv_label_create(ui_nav);
    lv_obj_align(file_name, LV_ALIGN_CENTER, 0, 2);
    lv_obj_set_style_text_font(file_name, &lv_font_montserrat_20, LV_PART_MAIN | LV_STATE_DEFAULT);

    // 顶部回退键 
    lv_obj_t* return_btn = lv_btn_create(ui_nav);
    lv_obj_set_style_shadow_opa(return_btn, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_size(return_btn, 40, 30);
    lv_obj_set_style_bg_color(return_btn, lv_color_hex(0xE0DFDF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_align(return_btn, LV_ALIGN_LEFT_MID, 18, 0);
    lv_obj_t* return_symbol = lv_label_create(return_btn);
    lv_obj_align(return_symbol, LV_ALIGN_CENTER, -5, 0);
    lv_obj_set_style_text_color(return_symbol, lv_color_hex(0x434343), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_label_set_text(return_symbol, LV_SYMBOL_LEFT);
    lv_obj_add_event_cb(return_btn, lv_ContentBackBtn_handle, LV_EVENT_CLICKED, NULL);
}

SD卡读取文件 sdcard_file.c

#include "esp_err.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#include "esp_log.h"
#include "driver/sdmmc_host.h"
#include "dirent.h"
#include "sdcard_file.h"

#define SD_SPI_DRIVER
#define MOUNT_POINT     "/sdcard"   //挂载点名称

static char* TAG = "sdcard";

#ifdef SD_SPI_DRIVER
#define PIN_NUM_MISO  GPIO_NUM_4
#define PIN_NUM_MOSI  GPIO_NUM_15
#define PIN_NUM_CLK   GPIO_NUM_14
#define PIN_NUM_CS    GPIO_NUM_13
#endif

esp_err_t sdcard_init(void)
{
    esp_err_t ret;
    esp_vfs_fat_mount_config_t mount_conf = 
    {
        .allocation_unit_size = 16*1024,
        .format_if_mount_failed = false,
        .max_files = 5,
    };
    sdmmc_card_t *card;

#ifndef SD_SPI_DRIVER
    ESP_LOGI(TAG, "Using SDMMC peripheral");
    // sdmmc主机配置
    sdmmc_host_t host_cfg = SDMMC_HOST_DEFAULT();
    host_cfg.max_freq_khz = SDMMC_FREQ_DEFAULT;
    
    // sd卡插槽配置
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    slot_config.width = 1;          // 数据总线宽度

    // 获得SD卡注册在VFS的FAT文件系统 (挂载文件系统)
    ret = esp_vfs_fat_sdmmc_mount(MOUNT_POINT, &host_cfg, &slot_config, &mount_conf, &card);
#else
    ESP_LOGI(TAG, "Using SPI peripheral");
    sdmmc_host_t host = SDSPI_HOST_DEFAULT();
    host.slot = SPI3_HOST;
    host.max_freq_khz = 20000;
    spi_bus_config_t bus_cfg = {
        .mosi_io_num = PIN_NUM_MOSI,
        .miso_io_num = PIN_NUM_MISO,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4000,
    };
    ret = spi_bus_initialize(host.slot, &bus_cfg, SPI_DMA_CH2);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to initialize bus. erro_num:%s", esp_err_to_name(ret));
        return ESP_FAIL;
    }
    sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
    slot_config.gpio_cs = PIN_NUM_CS;
    slot_config.host_id = host.slot;

    ESP_LOGI(TAG, "Mounting filesystem");
    ret = esp_vfs_fat_sdspi_mount(MOUNT_POINT, &host, &slot_config, &mount_conf, &card);
#endif

    if(ret != ESP_OK)
    {
        ESP_LOGI(TAG, "SDcard mount failed!");
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "SDcard mount success!");
    

    return ESP_OK;
}

int sdcard_filelist(const char (**file)[256])
{
    DIR* dir;
    struct dirent* entry;       // 定义文件入口

    // 定义20个文件名数组, 每个文件名最大长度256
    static char filename[20][256] = {0};
    dir = opendir(MOUNT_POINT);         // 打开挂载路径目录
    if(!dir)
        return ESP_FAIL;
    
    int file_cnt = 0;
    while((entry = readdir(dir)) != NULL)          // 遍历文件列表, readdir函数依次读取目录下所有文件后返回NULL
    {
        printf("%s\n", entry->d_name);
        snprintf(&filename[file_cnt++][0], 256, "%s", entry->d_name);
        if(file_cnt >= 20)
            break;
    }
    closedir(dir);
    *file = filename;

    return file_cnt;
}

sdcard_file.h

#ifndef __SDCARD_FILE_H
#define __SDCARD_FILE_H

#include "esp_err.h"

/** 初始化SD卡
 * @param 无
 * @return 成功或失败,只有成功初始化SD卡,才可以获取文件列表
*/
esp_err_t sdcard_init(void);

/** 获取SD卡的文件列表
 * @param file 返回的文件列表
 * @return 文件列表长度
*/
int sdcard_filelist(const char (**file)[256]);

#endif

 主函数main.c

 

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "ui_text_reader.h"
#include "lvgl.h"
#include "lv_port.h"
#include "st7789_driver.h"
#include "sdcard_file.h"

#define TAG     "main"

void lvgl_task_run(void * param)
{
    ui_text_create(); 
    while (1)
    {
        lv_task_handler();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void app_main(void)
{
    sdcard_init();      // 初始化sd卡
    lv_port_init();

    xTaskCreatePinnedToCore(lvgl_task_run, "lvgl_task", 8192, NULL, 3, NULL, 1);
}

总结

动态加载: 基于滚轮位置加载文本块方式读取文件
    --> 指定单次读取的数据块大小
    --> 根据页面滚轮位置计算出读取文件内容的偏移量,
    --> 根据偏移量移动文件指针, 读取当前文件指针后的固定大小数据块
    --> 采用动态分配内存方式, 显示一个数据块,释放一个数据块

    优点: 避免一次性加载大量的数据导致显示卡顿
           适用资源紧张的嵌入式设备,适用读取大文件(几百KB或几MB)
           实时加载内容,适合需要无缝浏览的场景

    缺点: 频繁读取文件导致滚轮不顺滑, 显示性能不稳定

 分页加载: 使用分页加载方式读取文件
    --> 将文本数据一次性读取出来存储在堆空间
    --> 确定每页需要显示的字节数
    --> 根据滚轮位置判断是否需要换页
    --> 将整个文本内容分页存储

    优点: 避免了频繁的文件读取操作,滚动时的性能更稳定

    缺点: 需要一次性加载整个文本内容到内存中,内存占用较大
           不适合大文件, 这里限制最大限制100KB的文件

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐