解决lvgl界面显示超长文本卡顿问题(基于lvgl8.3)
大小在100KB以内的文本(这里可以视情况而定,只要内存足够大),esp32的内存足以一次性全部读取到内存中,但是显示在lvgl标签中,一次加载超过1k文本数据还是太大,仍会造成lvgl刷新卡顿。所以我们将文本数据一次性读取到堆空间中,对lvgl显示的文本数据进行管理,每次仅显示指定数量的文字,再根据滚轮的位置判断是否需要向上或者向下换页,换页则从堆空间中取出指定大小的文本数据进行显示即可。大小在
问题背景:
使用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的文件
更多推荐



所有评论(0)