绘图¶
在 LVGL 中,您无需手动绘制任何内容。只需创建对象(如按钮、标签、弧等),移动和更改它们,LVGL 将刷新并重绘所需的内容。
然而,了解 LVGL 中绘图的基本原理可能会有所帮助,例如添加自定义功能、更容易发现问题或仅仅出于好奇。
基本概念是不要直接在显示器上绘制,而是首先在内部绘图缓冲区上绘制。当绘图(渲染)完成后,该缓冲区会被复制到显示器上。
绘图缓冲区可以小于显示器的尺寸。LVGL 将以适合给定绘图缓冲区的“块”方式进行渲染。
与直接绘制到显示器相比,这种方法有两个主要优点:
避免了在绘制 UI 层时的闪烁。例如,如果 LVGL 直接在显示器上绘制,当绘制 背景 + 按钮 + 文本 时,每个“阶段”都会短暂可见。
修改内部 RAM 中的缓冲区并最终只写入一次像素比在每次像素访问时直接读取/写入显示器更快。 (例如,通过带 SPI 接口的显示控制器)。
请注意,这种概念不同于“传统”的双缓冲,其中有两个显示器大小的帧缓冲区: 一个保存当前显示器上显示的图像,渲染发生在另一个(非活动)帧缓冲区中,并在渲染完成后交换它们。 主要区别在于,使用 LVGL,您不需要存储两个帧缓冲区(通常需要外部 RAM),而只需要较小的绘图缓冲区,这些缓冲区可以轻松地适应内部 RAM。
屏幕刷新的机制¶
首先,请确保熟悉 LVGL 的缓冲模式。
LVGL 按以下步骤刷新屏幕:
UI 中发生某些需要重绘的事情。例如,按钮被按下,图表发生变化,动画发生等。
LVGL 将更改对象的旧区域和新区域保存到一个称为 无效区域缓冲区 的缓冲区中。为了优化,在某些情况下,对象不会被添加到缓冲区中:
隐藏的对象不会被添加。
完全超出其父对象的对象不会被添加。
部分超出父对象的区域会被裁剪到父对象的区域。
其他屏幕上的对象不会被添加。
在每个
LV_DISP_DEF_REFR_PERIOD(在lv_conf.h中设置)中,发生以下情况:LVGL 检查无效区域并合并相邻或相交的区域。
获取第一个合并区域,如果它小于 绘图缓冲区,则简单地将该区域的内容渲染到 绘图缓冲区 中。 如果该区域不适合缓冲区,则尽可能多地绘制行到 绘图缓冲区。
当区域被渲染时,从显示驱动程序调用
flush_cb刷新显示器。如果区域大于缓冲区,则也渲染剩余部分。
对剩余的合并区域重复相同的操作。
当一个区域被重绘时,库会搜索覆盖该区域的最顶层对象,并从该对象开始绘制。 例如,如果按钮的标签发生了变化,库会看到只需绘制文本下方的按钮,而不需要重绘按钮其余部分下方的显示器。
关于绘图机制,不同缓冲模式的区别如下:
单缓冲 - 在开始重绘下一部分之前,LVGL 需要等待
lv_disp_flush_ready()(从flush_cb调用)。双缓冲 - 当第一个缓冲区发送到
flush_cb时,LVGL 可以立即绘制到第二个缓冲区,因为刷新应该由 DMA(或类似硬件)在后台完成。双缓冲 -
flush_cb只需交换帧缓冲区的地址。
遮罩¶
遮罩 是 LVGL 绘图引擎的基本概念。 使用 LVGL 不需要了解此处描述的机制,但您可能会发现了解绘图的工作原理很有趣。 了解遮罩在自定义绘图时非常有用。
要了解遮罩,让我们首先看看绘图的步骤。 LVGL 执行以下步骤来渲染任何形状、图像或文本。可以将其视为绘图管道。
准备绘图描述符 从对象的样式创建绘图描述符(例如
lv_draw_rect_dsc_t)。这为我们提供了绘图的参数,例如颜色、宽度、不透明度、字体、半径等。调用绘图函数 使用绘图描述符和其他一些参数调用绘图函数(例如
lv_draw_rect())。它会将基本形状渲染到当前绘图缓冲区。创建遮罩 如果形状非常简单且不需要遮罩,则转到 #5。否则,在绘图函数中创建所需的遮罩。(例如,圆角矩形遮罩)
计算所有添加的遮罩 它将不透明度值合成到具有创建遮罩“形状”的 遮罩缓冲区 中。 例如,在“线遮罩”的情况下,根据遮罩的参数,保持缓冲区的一侧不变(默认值为 255),并将其余部分设置为 0,以指示该侧应被移除。
混合颜色或图像 在混合过程中,处理遮罩(使某些像素透明或不透明)、混合模式(加法、减法等)和颜色/图像不透明度。
LVGL 具有以下内置遮罩类型,可以实时计算和应用:
LV_DRAW_MASK_TYPE_LINE从一条线(顶部、底部、左侧或右侧)移除一侧。lv_draw_line使用它的四个实例。 本质上,每条(倾斜)线都由四个线遮罩边界形成一个矩形。LV_DRAW_MASK_TYPE_RADIUS使用圆角过渡移除矩形的内角或外角。通过将半径设置为较大值(LV_RADIUS_CIRCLE),还可以用来创建圆。LV_DRAW_MASK_TYPE_ANGLE移除一个圆形扇形。它由lv_draw_arc使用以移除“空”扇形。LV_DRAW_MASK_TYPE_FADE创建垂直渐变(更改不透明度)。LV_DRAW_MASK_TYPE_MAP遮罩存储在位图数组中,并应用必要的部分。
遮罩用于创建几乎所有基本图元:
字母 从字母创建遮罩,并使用遮罩绘制具有字母颜色的矩形。
线 由四个“线遮罩”创建,以遮罩线的左、右、上和下部分,从而获得完美的垂直边界。
圆角矩形 实时创建遮罩以为角添加半径。
剪裁角 为了剪裁圆角上溢出的内容(通常是子对象),还应用了圆角矩形遮罩。
矩形边框 与圆角矩形相同,但内部部分也被遮罩。
弧形绘图 绘制一个圆形边框,但也应用了弧形遮罩。
ARGB 图像 Alpha 通道分离为遮罩,图像作为普通 RGB 图像绘制。
使用遮罩¶
每种遮罩类型都有一个相关的参数结构来描述遮罩的数据。以下是现有的参数类型:
lv_draw_mask_line_param_tlv_draw_mask_radius_param_tlv_draw_mask_angle_param_tlv_draw_mask_fade_param_tlv_draw_mask_map_param_t
使用
lv_draw_mask_<type>_init初始化遮罩参数。有关完整的 API,请参见lv_draw_mask.h。使用
int16_t mask_id = lv_draw_mask_add(¶m, ptr)将遮罩参数添加到绘图引擎中。ptr可以是任何指针,用于标识遮罩(如果未使用,则为NULL)。调用绘图函数。
使用
lv_draw_mask_remove_id(mask_id)或lv_draw_mask_remove_custom(ptr)从绘图引擎中移除遮罩。使用
lv_draw_mask_free_param(¶m)释放参数。
参数可以被添加和移除任意次数,但在不再需要时需要释放。
lv_draw_mask_add 仅保存遮罩的指针,因此在使用时参数需要有效。
挂钩绘图¶
尽管可以通过样式轻松自定义小部件,但在某些情况下可能需要更自定义的内容。 为了确保极大的灵活性,LVGL 在绘图期间发送了许多事件,并附带了有关 LVGL 即将绘制内容的参数。 这些参数的某些字段可以被修改以绘制其他内容,或者可以手动添加任何自定义绘图操作。
一个很好的用例是 按钮矩阵 小部件。默认情况下,其按钮可以在不同状态下设置样式,但您无法逐个设置按钮的样式。 然而,会为每个按钮发送一个事件,您可以例如告诉 LVGL 在特定按钮上使用不同的颜色,或者在某些按钮上手动绘制图像。
以下详细描述了每个事件。
主绘图¶
这些事件与对象的实际绘图相关。例如,按钮、文本等的绘图发生在这里。
lv_event_get_clip_area(event) 可用于获取当前剪辑区域。剪辑区域在绘图函数中是必需的,以使它们仅在有限的区域内绘制。
LV_EVENT_DRAW_MAIN_BEGIN¶
在开始绘制对象之前发送。这是手动添加遮罩的好地方。例如,添加一个线遮罩以“移除”对象的右侧。
LV_EVENT_DRAW_MAIN¶
对象的实际绘图发生在此事件中。例如,按钮的矩形在此处绘制。首先调用小部件的内部事件以执行绘图,然后您可以在它们之上绘制任何内容。 例如,您可以添加自定义文本或图像。
LV_EVENT_DRAW_MAIN_END¶
主绘图完成时调用。您也可以在此处绘制任何内容,这也是移除在 LV_EVENT_DRAW_MAIN_BEGIN 中创建的遮罩的好地方。
后绘图¶
后绘图事件在对象的所有子对象绘制完成后调用。例如,LVGL 使用后绘图阶段绘制滚动条,因为它们应该位于所有子对象之上。
lv_event_get_clip_area(event) 可用于获取当前剪辑区域。
LV_EVENT_DRAW_POST_BEGIN¶
在开始后绘图阶段之前发送。遮罩也可以在此处添加以遮罩后绘制的内容。
LV_EVENT_DRAW_POST¶
实际绘图应在此处发生。
LV_EVENT_DRAW_POST_END¶
后绘图完成时调用。如果遮罩未在 LV_EVENT_DRAW_MAIN_END 中移除,则应在此处移除。
部分绘图¶
当 LVGL 绘制对象的一部分(例如滑块的指示器、表格的单元格或按钮矩阵的按钮)时,它会在绘制该部分之前和之后发送事件,并附带一些绘图上下文。 这允许通过遮罩、额外绘图或更改 LVGL 计划用于绘图的参数,在非常低的级别上更改部分。
在这些事件中,使用 lv_obj_draw_part_t 结构来描述绘图的上下文。并非所有字段都为每个部分和小部件设置。
要查看为小部件设置了哪些字段,请参阅小部件的文档。
lv_obj_draw_part_t 具有以下字段:
// 始终设置
const lv_area_t * clip_area; // 当前剪辑区域,如果您需要在事件中绘制某些内容,则需要
uint32_t part; // 当前发送事件的部分
uint32_t id; // 部分的索引。例如,按钮矩阵或表格单元格上的按钮索引。
// 绘图描述符,仅在相关时设置
lv_draw_rect_dsc_t * rect_dsc; // 可修改的绘图描述符以更改 LVGL 将绘制的内容。仅为矩形部分设置
lv_draw_label_dsc_t * label_dsc; // 可修改的绘图描述符以更改 LVGL 将绘制的内容。仅为文本部分设置
lv_draw_line_dsc_t * line_dsc; // 可修改的绘图描述符以更改 LVGL 将绘制的内容。仅为线条部分设置
lv_draw_img_dsc_t * img_dsc; // 可修改的绘图描述符以更改 LVGL 将绘制的内容。仅为图像部分设置
lv_draw_arc_dsc_t * arc_dsc; // 可修改的绘图描述符以更改 LVGL 将绘制的内容。仅为弧形部分设置
// 其他参数
lv_area_t * draw_area; // 正在绘制的部分的区域
const lv_point_t * p1; // 绘图过程中计算的点。例如,图表的点或弧的中心。
const lv_point_t * p2; // 绘图过程中计算的点。例如,图表的点。
char text[16]; // 绘图过程中计算的文本。可以修改。例如,图表轴上的刻度标签。
lv_coord_t radius; // 例如,弧的半径(不是角半径)。
int32_t value; // 绘图过程中计算的值。例如,图表的刻度线值。
const void * sub_part_ptr; // 指向标识部分中某些内容的指针。例如,图表系列。
lv_event_get_draw_part_dsc(event) 可用于获取指向 lv_obj_draw_part_t 的指针。
LV_EVENT_DRAW_PART_BEGIN¶
开始绘制部分。这是修改绘图描述符(例如 rect_dsc)或添加遮罩的好地方。
LV_EVENT_DRAW_PART_END¶
完成部分绘图。这是绘制部分上的额外内容或移除在 LV_EVENT_DRAW_PART_BEGIN 中添加的遮罩的好地方。
其他¶
LV_EVENT_COVER_CHECK¶
此事件用于检查对象是否完全覆盖区域。
lv_event_get_cover_area(event) 返回指向要检查的区域的指针,lv_event_set_cover_res(event, res) 可用于设置以下结果之一:
LV_COVER_RES_COVER对象完全覆盖该区域LV_COVER_RES_NOT_COVER对象未覆盖该区域LV_COVER_RES_MASKED对象上有遮罩,因此它未完全覆盖该区域
以下是对象无法完全覆盖区域的一些原因:
它根本不完全在区域内
它有一个半径
它没有 100% 的背景不透明度
它是 ARGB 或色度键控图像
它没有正常的混合模式。在这种情况下,LVGL 需要知道对象下的颜色以正确应用混合
它是文本等
简而言之,如果由于任何原因对象下的区域可见,则对象不会覆盖该区域。
在发送此事件之前,LVGL 会检查小部件的坐标是否至少完全覆盖该区域。如果没有,则不会调用该事件。
您只需要检查您添加的绘图。小部件已知的现有属性在其内部事件中处理。
例如,如果小部件的 radius > 0,则它可能不会覆盖区域,但您只需处理 radius,如果您将修改它并且小部件不会知道它。
LV_EVENT_REFR_EXT_DRAW_SIZE¶
如果您需要在小部件外绘图,LVGL 需要知道以提供额外的绘图空间。 假设您创建了一个事件,在滑块的旋钮上方写入当前值。在这种情况下,LVGL 需要知道滑块的绘图区域应该增加文本所需的大小。
您可以简单地使用 lv_event_set_ext_draw_size(e, size) 设置所需的绘图区域。