LVGL版本:8.1
概述
? ? ? ?在LVGL中,可以導入多種不同類型的圖像:
- 經轉換器生成的C語言數組,適用于頁面中不常改變的固定圖像。
- 存儲系統中的外部圖像,比較靈活,可以通過插卡或從網絡中獲取,但需要配置相應的驅動和解碼器,解碼過程也需要占用時間和內存。
- SYMBOL類型的文本,和label組件類似。
? ? ? ?其中第三種比較容易實現,第一種僅需經過官方工具生成二進制數據,再在代碼中引用即可。本文重點說明第二種情況即外部圖像的使用,并以PNG類型圖片為例介紹LVGL圖像的解碼和顯示。
原理框圖
? ? ? ?圖像的解析和使用可以分以下幾層:
? ? ? ? 圖中的虛線框可有可無,有的僅代表多一層函數,為了更好的理解,在圖上加入這些虛線框。
? ? ? ?對于一個通過lv_img_create()函數創建的lv_img_t圖像,可以通過設置圖像源進行圖像導入,LVGL會先查看緩存,如果有命中則直接使用,否則進入解碼環節。對于內置圖像如SYMBOL和VARIABLE(即C數組)類型,可以直接使用內置的解碼器,在編譯時會直接合并入程序。對于外部圖像格式如jpg和png,則需要添加額外的解碼器,并且,由于這些外部圖像通常是以文件的形式存放在文件系統中,還需要提供基本的文件操作來讀取文件,如Linux可以簡單使用POSIX標準的系統調用函數(open、read、write等),事實上,如果沒有提供文件系統,用戶也可以自定義圖像的打開和讀取方式,只要符合接口標準即可。
開啟配置
? ? ? ?以PNG類型的圖像為例,需要在lv_conf.h配置文件中,打開LV_USE_PNG開關,同時要開啟文件系統(根據實際所在系統進行選擇),比如在這里我選擇的是LV_USE_FS_POSIX,即使用POSIX標準的文件操作函數(read、write等),也是Linux的原生操作。注意這里要求填入一個前綴來區分不同系統,通常使用類似“盤符”的字母(如這里使用了'S'),因為LVGL底層打開源路徑時也是比對的路徑首字母,配對成功后才會使用對應的驅動來打開。
//lv_conf.h
/* 文件系統,如開啟,請設置相應的驅動字母(或前綴) */
#define LV_USE_FS_STDIO '\0' /*Uses fopen, fread, etc*/
//#define LV_FS_STDIO_PATH "/home/john/" /*Set the working directory. If commented it will be "./" */#define LV_USE_FS_POSIX 'S' /*Uses open, read, etc*/
//#define LV_FS_POSIX_PATH "/home/john/" /*Set the working directory. If commented it will be "./" */#define LV_USE_FS_WIN32 '\0' /*Uses CreateFile, ReadFile, etc*/
//#define LV_FS_WIN32_PATH "C:\\Users\\john\\" /*Set the working directory. If commented it will be ".\\" */#define LV_USE_FS_FATFS '\0' /*Uses f_open, f_read, etc*//*PNG decoder library*/
#define LV_USE_PNG 1
? ? ? ?此外,還可以增加系統的圖像緩存數量LV_IMG_CACHE_DEF_SIZE,來減少不必要的解碼,提高渲染效率。
//lv_conf.h
#define LV_IMG_CACHE_DEF_SIZE 1 //圖像緩存數量,只有使用外部解碼器的圖像,該配置才有意義
?
圖像解碼
? ? ? ?通過lv_img_set_src()函數設置圖像源來開啟LVGL解碼和顯示的過程,其中圖像源src可以是以下類型:
- 圖像路徑,通常以一個盤符為首(如'S'或'S:'),后面跟絕對路徑,比如是'S/img/abc.png',注意圖像要預先導入到設備對應路徑上才能讀取;
- LVGL規定的SYMBOL類型,也可以通過官方工具生成自定義SYMBOL;
- C語言數組,包含圖像的位置和色彩信息,可以通過官方工具將其它類型的圖像轉換為LVGL可識別的數據文件,并在使用處進行聲明和導入。
? ? ? ?在這里,該函數僅用于設置圖像源,并獲取圖像基本信息,還沒有到解碼部分。
//lv_img.c
void lv_img_set_src(lv_obj_t * obj, const void * src)
{LV_ASSERT_OBJ(obj, MY_CLASS);lv_obj_invalidate(obj);lv_img_src_t src_type = lv_img_src_get_type(src); //獲取來源類型lv_img_t * img = (lv_img_t *)obj;/*If the new source type is unknown free the memories of the old source*/if(src_type == LV_IMG_SRC_UNKNOWN) {LV_LOG_WARN("lv_img_set_src: unknown image type");if(img->src_type == LV_IMG_SRC_SYMBOL || img->src_type == LV_IMG_SRC_FILE) {lv_mem_free((void *)img->src);}img->src = NULL;img->src_type = LV_IMG_SRC_UNKNOWN;return;}lv_img_header_t header;lv_img_decoder_get_info(src, &header); //通過圖片來源獲取對應的解碼器/*Save the source*/if(src_type == LV_IMG_SRC_VARIABLE) {/*If memory was allocated because of the previous `src_type` then free it*/if(img->src_type == LV_IMG_SRC_FILE || img->src_type == LV_IMG_SRC_SYMBOL) {lv_mem_free((void *)img->src);}img->src = src;}else if(src_type == LV_IMG_SRC_FILE || src_type == LV_IMG_SRC_SYMBOL) {/*If the new and the old src are the same then it was only a refresh.*/if(img->src != src) {const void * old_src = NULL;/*If memory was allocated because of the previous `src_type` then save its pointer and free after allocation.*It's important to allocate first to be sure the new data will be on a new address.*Else `img_cache` wouldn't see the change in source.*/if(img->src_type == LV_IMG_SRC_FILE || img->src_type == LV_IMG_SRC_SYMBOL) {old_src = img->src;}char * new_str = lv_mem_alloc(strlen(src) + 1);LV_ASSERT_MALLOC(new_str);if(new_str == NULL) return;strcpy(new_str, src);img->src = new_str;if(old_src) lv_mem_free((void *)old_src);}}if(src_type == LV_IMG_SRC_SYMBOL) {/*`lv_img_dsc_get_info` couldn't set the with and height of a font so set it here*/const lv_font_t * font = lv_obj_get_style_text_font(obj, LV_PART_MAIN);lv_coord_t letter_space = lv_obj_get_style_text_letter_space(obj, LV_PART_MAIN);lv_coord_t line_space = lv_obj_get_style_text_line_space(obj, LV_PART_MAIN);lv_point_t size;lv_txt_get_size(&size, src, font, letter_space, line_space, LV_COORD_MAX, LV_TEXT_FLAG_NONE);header.w = size.x;header.h = size.y;}img->src_type = src_type; //圖像類型(變量/符號/文件路徑)img->w = header.w; //圖像寬度img->h = header.h; //圖像高度img->cf = header.cf; //圖像色彩信息img->pivot.x = header.w / 2;img->pivot.y = header.h / 2;lv_obj_refresh_self_size(obj);/*Provide enough room for the rotated corners*/if(img->angle || img->zoom != LV_IMG_ZOOM_NONE) lv_obj_refresh_ext_draw_size(obj);lv_obj_invalidate(obj); //標記無效區域,下次更新才會正式解碼圖片
}
? ? ? ?其中lv_img_decoder_get_info()用于獲取圖像源的基本信息,該函數會首先遍歷解碼器鏈表,匹配合適的解碼器,然后將圖像的寬、高、色彩信息記錄下來。
//lv_img_decoder.c
lv_res_t lv_img_decoder_get_info(const void * src, lv_img_header_t * header)
{lv_memset_00(header, sizeof(lv_img_header_t));if(src == NULL) return LV_RES_INV;lv_img_src_t src_type = lv_img_src_get_type(src);if(src_type == LV_IMG_SRC_VARIABLE) {const lv_img_dsc_t * img_dsc = src;if(img_dsc->data == NULL) return LV_RES_INV;}lv_res_t res = LV_RES_INV;lv_img_decoder_t * d;_LV_LL_READ(&LV_GC_ROOT(_lv_img_decoder_ll), d) { //遍歷解碼器鏈表,為圖像源尋找合適的解碼器if(d->info_cb) {res = d->info_cb(d, src, header); //調用info回調,獲取圖像信息,header用于保存這些信息if(res == LV_RES_OK) break; //返回OK,說明解碼器配對成功,退出并返回}}return res;
}
? ? ? ?如果定義了LV_USE_PNG,則默認注冊LVGL自帶的PNG解碼器,其中包含獲取基本信息的回調函數decoder_info()。
//lv_png.c
static lv_res_t decoder_info(struct _lv_img_decoder_t * decoder, const void * src, lv_img_header_t * header)
{(void) decoder; /*Unused*/lv_img_src_t src_type = lv_img_src_get_type(src); /* 獲取來源類型 *//* 檢查是否是文件路徑類型 */if(src_type == LV_IMG_SRC_FILE) {const char * fn = src;if(!strcmp(&fn[strlen(fn) - 3], "png")) { /* 檢查后綴是否為png *//* Read the width and height from the file. They have a constant location:* [16..23]: width* [24..27]: height*/uint32_t size[2];lv_fs_file_t f;lv_fs_res_t res = lv_fs_open(&f, fn, LV_FS_MODE_RD); //使用對應的驅動打開文件if(res != LV_FS_RES_OK) return LV_RES_INV;lv_fs_seek(&f, 16, LV_FS_SEEK_SET);uint32_t rn;lv_fs_read(&f, &size, 8, &rn);if(rn != 8) return LV_RES_INV;lv_fs_close(&f);/*Save the data in the header*/header->always_zero = 0;header->cf = LV_IMG_CF_RAW_ALPHA;/*The width and height are stored in Big endian format so convert them to little endian*/header->w = (lv_coord_t) ((size[0] & 0xff000000) >> 24) + ((size[0] & 0x00ff0000) >> 8);header->h = (lv_coord_t) ((size[1] & 0xff000000) >> 24) + ((size[1] & 0x00ff0000) >> 8);return LV_RES_OK;}}/*If it's a PNG file in a C array...*/else if(src_type == LV_IMG_SRC_VARIABLE) {const lv_img_dsc_t * img_dsc = src;header->always_zero = 0;header->cf = img_dsc->header.cf; /*Save the color format*/header->w = img_dsc->header.w; /*Save the color width*/header->h = img_dsc->header.h; /*Save the color height*/return LV_RES_OK;}return LV_RES_INV; /*If didn't succeeded earlier then it's an error*/
}
? ? ? ?其中lv_fs_open()函數是尋找合適的驅動來打開這個文件,我們在前面定義了LV_USE_FS_POSIX,LVGL在初始化階段就會自動注冊POSIX標準驅動,使用open、read、write等函數打開對應文件。
//lv_fs_posix.c
void lv_fs_posix_init(void)
{static lv_fs_drv_t fs_drv; /* 驅動描述子 */lv_fs_drv_init(&fs_drv);fs_drv.letter = LV_USE_FS_POSIX; //文件系統前綴,可以簡單設置為'S'fs_drv.open_cb = fs_open;fs_drv.close_cb = fs_close;fs_drv.read_cb = fs_read;fs_drv.write_cb = fs_write;fs_drv.seek_cb = fs_seek;fs_drv.tell_cb = fs_tell;fs_drv.dir_close_cb = fs_dir_close;fs_drv.dir_open_cb = fs_dir_open;fs_drv.dir_read_cb = fs_dir_read;lv_fs_drv_register(&fs_drv);
}
? ? ? ?到這里為止僅僅只是把圖像源記錄下來,直到下次更新周期的繪制階段,才會正式解析圖像。下面直接跳轉到圖像的繪制函數lv_draw_img_core(),這里僅截取解碼圖像的代碼片段。
//lv_draw_img.c
LV_ATTRIBUTE_FAST_MEM static lv_res_t lv_img_draw_core(const lv_area_t * coords, const lv_area_t * clip_area,const void * src,const lv_draw_img_dsc_t * draw_dsc)
{if(draw_dsc->opa <= LV_OPA_MIN) return LV_RES_OK;_lv_img_cache_entry_t * cdsc = _lv_img_cache_open(src, draw_dsc->recolor, draw_dsc->frame_id);if(cdsc == NULL) return LV_RES_INV;/*......*/
}
? ? ? ?可以看到,LVGL在繪制圖像前先詢問緩存,即_lv_img_cache_open()函數,該函數會先遍歷緩存鏈表,查找是否存在匹配的緩存,如匹配直接返回該緩存,否則從鏈表中找一個存活時間最短的緩存,用新的圖像替換掉該舊緩存。
//lv_img_cache.c
_lv_img_cache_entry_t * _lv_img_cache_open(const void * src, lv_color_t color, int32_t frame_id)
{_lv_img_cache_entry_t * cached_src = NULL;#if LV_IMG_CACHE_DEF_SIZEif(entry_cnt == 0) {LV_LOG_WARN("lv_img_cache_open: the cache size is 0");return NULL;}_lv_img_cache_entry_t * cache = LV_GC_ROOT(_lv_img_cache_array);/*Decrement all lifes. Make the entries older*/uint16_t i;for(i = 0; i < entry_cnt; i++) {if(cache[i].life > INT32_MIN + LV_IMG_CACHE_AGING) {cache[i].life -= LV_IMG_CACHE_AGING;}}for(i = 0; i < entry_cnt; i++) {if(color.full == cache[i].dec_dsc.color.full &&frame_id == cache[i].dec_dsc.frame_id &&lv_img_cache_match(src, cache[i].dec_dsc.src)) { //是否命中緩存/* 打開一個圖像也會增加圖像的存活時間,因此在這里對緩存增加一個time_to_open的時間 */cached_src = &cache[i];cached_src->life += cached_src->dec_dsc.time_to_open * LV_IMG_CACHE_LIFE_GAIN;if(cached_src->life > LV_IMG_CACHE_LIFE_LIMIT) cached_src->life = LV_IMG_CACHE_LIFE_LIMIT;LV_LOG_TRACE("image source found in the cache");break;}}/* 命中緩存后直接返回該緩存 */if(cached_src) return cached_src;/* 如果沒有命中,則在緩存中尋找一個合適的位置存放新圖像 */cached_src = &cache[0];for(i = 1; i < entry_cnt; i++) {if(cache[i].life < cached_src->life) { //尋找生存時間最短的圖像cached_src = &cache[i];}}/* 找到被替代的緩存后,要先關閉它的解碼器 */if(cached_src->dec_dsc.src) {lv_img_decoder_close(&cached_src->dec_dsc);LV_LOG_INFO("image draw: cache miss, close and reuse an entry");}else {LV_LOG_INFO("image draw: cache miss, cached to an empty entry");}
#elsecached_src = &LV_GC_ROOT(_lv_img_cache_single);
#endif/* 打開一個新的解碼器 */uint32_t t_start = lv_tick_get();lv_res_t open_res = lv_img_decoder_open(&cached_src->dec_dsc, src, color, frame_id);if(open_res == LV_RES_INV) {LV_LOG_WARN("Image draw cannot open the image resource");lv_memset_00(cached_src, sizeof(_lv_img_cache_entry_t));cached_src->life = INT32_MIN; /*Make the empty entry very "weak" to force its us*/return NULL;}cached_src->life = 0; //記錄緩存后,設置存活時間從0開始/* 記錄解碼它的時間 */if(cached_src->dec_dsc.time_to_open == 0) {cached_src->dec_dsc.time_to_open = lv_tick_elaps(t_start);}if(cached_src->dec_dsc.time_to_open == 0) cached_src->dec_dsc.time_to_open = 1;return cached_src;
}
? ? ? ?當沒有命中緩存時,會調用lv_img_decoder_open()函數嘗試使用解碼器打開該圖像,打開成功后,會將圖像數據提取出來。
//lv_img_decoder.c
lv_res_t lv_img_decoder_open(lv_img_decoder_dsc_t * dsc, const void * src, lv_color_t color, int32_t frame_id)
{lv_memset_00(dsc, sizeof(lv_img_decoder_dsc_t));if(src == NULL) return LV_RES_INV;lv_img_src_t src_type = lv_img_src_get_type(src);if(src_type == LV_IMG_SRC_VARIABLE) {const lv_img_dsc_t * img_dsc = src;if(img_dsc->data == NULL) return LV_RES_INV;}dsc->color = color;dsc->src_type = src_type;dsc->frame_id = frame_id;if(dsc->src_type == LV_IMG_SRC_FILE) {size_t fnlen = strlen(src);dsc->src = lv_mem_alloc(fnlen + 1);LV_ASSERT_MALLOC(dsc->src);if(dsc->src == NULL) {LV_LOG_WARN("lv_img_decoder_open: out of memory");return LV_RES_INV;}strcpy((char *)dsc->src, src);}else {dsc->src = src;}lv_res_t res = LV_RES_INV;lv_img_decoder_t * decoder;_LV_LL_READ(&LV_GC_ROOT(_lv_img_decoder_ll), decoder) { //遍歷解碼器鏈表/* info和open函數是必要的 */if(decoder->info_cb == NULL || decoder->open_cb == NULL) continue;res = decoder->info_cb(decoder, src, &dsc->header);if(res != LV_RES_OK) continue; //返回OK,說明命中解碼器dsc->decoder = decoder;res = decoder->open_cb(decoder, dsc); //使用解碼器打開圖像文件/* 如果返回OK,表示成功打開圖像文件,則返回 */if(res == LV_RES_OK) return res;/* 準備遍歷下一個解碼器節點 */lv_memset_00(&dsc->header, sizeof(lv_img_header_t));dsc->error_msg = NULL;dsc->img_data = NULL;dsc->user_data = NULL;dsc->time_to_open = 0;}if(dsc->src_type == LV_IMG_SRC_FILE)lv_mem_free((void *)dsc->src);return res;
}
? ? ? ?接下來就是使用對應解碼器的open函數來獲取圖像,這里就不進一步說明了。可以參考官方對于不同格式圖像的解碼器例程代碼。