LVGL v9官方文档完美翻译(第一部分)

前言

前段时间学习LVGL,一开始跟着韦东山翻译的文档学习,但读起来感觉很不顺畅,于是决定跟着英文官方文档学习,并且进行更加流程的翻译,于是就有了该文章系列。读完该系列完整,将对LVGL有个整体的理解和应用,并且我刨除了一些无关的部分,加上了一些总结部分,可以快速了解widget的用法。
如果大家觉得不错,请收藏点赞转发,这是对我最大的鼓励,谢谢~

LVGL介绍

配置要求

基本上,每个能够驱动显示器的现代控制器都适合运行 LVGL。 最低要求是:

  • 16、32 或 64 位微控制器或处理器
  • 建议使用 >16 MHz 时钟速度
  • 闪存/ROM: > 64 kB 用于非常重要的组件 (> 建议使用 180 kB)
    • RAM:静态 RAM 使用量:~2 kB,取决于使用的功能和对象类型堆: > 2kB (> 建议使用 8 kB)动态数据(堆): > 2 KB (> 如果使用多个对象,建议使用 16 kB). 在 lv_conf.h 文件中配置 LV_MEM_SIZE 生效。显示缓冲区:> “水平分辨率”像素(推荐 >10 × 10ד 水平分辨率”)MCU或外部显示控制器中的一个帧缓冲区
  • C99 或更新的编译器

快速概览

基础知识

Widgets(控件)

按钮、标签、滑块、图表等图形元素称为对象或小控件。
控件有如下:

// 创建一个slider对象/控件
lv_obj_t * slider1 = lv_slider_create(lv_screen_active());
// 设置属性
lv_obj_set_x(slider1, 30);
lv_obj_set_y(slider1, 10);
lv_obj_set_size(slider1, 200, 50);

除了基本属性外,小部件还可以具有特定于类型的小部件由 lv_<widget_type>_set_<parameter_name>(obj, <value>) 函数设置的参数。例如:

lv_slider_set_value(slider1, 70, LV_ANIM_ON);

Events(事件)

事件用于通知用户某个对象发生了某些事情。 您可以将一个或多个回调分配给一个对象,如果该对象被单击、释放、拖动、删除等将被调用。

// 为按钮对象添加一个点击事件回调函数
lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); /*Assign a callback to the button*/
void btn_event_cb(lv_event_t * e)
{
    printf("Clicked\n");
}

lv_event_t * e 中,可以使用以下命令检索当前事件代码:

lv_event_code_t code = lv_event_get_code(e);

可以使用以下命令检索触发事件的对象:

lv_obj_t * obj = lv_event_get_target(e);

Parts(部分)

控件Widgets可能由一个或多个部分组成。
例如,一个按钮只有一个名为 LV_PART_MAIN 的部分。但是, lv_slider(滑块) 具有 LV_PART_MAINLV_PART_INDICATORLV_PART_KNOB

States(状态)

LVGL 对象可以处于以下状态的组合:

  • LV_STATE_DEFAULT: 正常,释放状态
  • LV_STATE_CHECKED: 切换或选中状态
  • LV_STATE_FOCUSED: 通过键盘或编码器聚焦或通过触摸板/鼠标点击
  • LV_STATE_FOCUS_KEY: 通过键盘或编码器聚焦,但不通过触摸板/鼠标聚焦
  • LV_STATE_EDITED: 由编码器编辑
  • LV_STATE_HOVERED: 鼠标悬停(现在不支持)
  • LV_STATE_PRESSED: 被按下
  • LV_STATE_SCROLLED: 正在滚动
  • LV_STATE_DISABLED: 禁用
    例如,如果你按下一个对象,它会自动进入 LV_STATE_FOCUSEDLV_STATE_PRESSED 状态,当你释放它时,LV_STATE_PRESSED 状态将被移除,保持活动状态。
    要检查对象是否处于给定状态,请使用 lv_obj_has_state(obj, LV_STATE_...)。如果对象当时处于该状态,它将返回 true
    要手动添加或删除状态,请使用下面的函数:
lv_obj_add_state(obj, LV_STATE_...);
lv_obj_remove_state(obj, LV_STATE_...);

Styles(样式)

一个样式实例包含诸如背景颜色、边框宽度、字体等属性,用来描述对象的外观。
样式用 lv_style_t 变量表示。在对象中只保存它们的指针,因此需要将其定义为静态或全局变量。在使用样式之前,需要使用 lv_style_init(&style1) 进行初始化。然后可以添加属性来配置样式。

static lv_style_t style1;
lv_style_init(&style1);
lv_style_set_bg_color(&style1, lv_color_hex(0xa03080))
lv_style_set_border_width(&style1, 2))

使用对象的部分Part和状态State的ORed组合来分配样式。例如,当滑块被按下时,在滑块指示器上使用这种样式:

// 在按下状态和INDICATOR部分时应用该样式
lv_obj_add_style(slider1, &style1, LV_PART_INDICATOR | LV_STATE_PRESSED);

如果部分Part是LV_PART_MAIN,则可以省略它。

lv_obj_add_style(btn1, &style1, LV_STATE_PRESSED); /*Equal to LV_PART_MAIN | LV_STATE_PRESSED*/

如果状态State是LV_STATE_DEFAULT,也可以省略。

lv_obj_add_style(btn1, &style1, 0); /*Equal to LV_PART_MAIN | LV_STATE_DEFAULT*/

样式可以级联(类似于CSS)。这意味着您可以向对象的一部分添加更多样式。例如,style_btn 可以设置默认按钮外观,而 style_btn_red 可以覆盖背景颜色,使按钮变为红色:

lv_obj_add_style(btn1, &style_btn, 0);
lv_obj_add_style(btn1, &style1_btn_red, 0);

如果当前状态下没有设置属性,则将使用带有LV_STATE_DEFAULT的样式。如果在默认状态中未定义属性,则会使用默认值。
某些属性(通常是与文本相关的属性)可以被继承。这意味着如果对象中未设置某个属性,系统会在其父级中查找该属性。例如,您可以在屏幕样式中设置一次字体,然后该屏幕上的所有文本都将默认继承它。
还可以向对象添加局部样式属性。这将创建一个仅由对象内部使用的样式:

// 为slider控件设置背景颜色
lv_obj_set_style_bg_color(slider1, lv_color_hex(0x2080bb), LV_PART_INDICATOR | LV_STATE_PRESSED);

Themes(主题)

主题是对象的默认样式。创建对象时,将自动应用来自主题的样式。
应用程序的主题是在 lv_conf.h 中设置的编译时配置。

概述

Objects(对象)

在LVGL中,用户界面的 基本组成部分 是对象(控件),也称为 Widgets。例如,一个 按钮标签图像列表图表 或者 文本区域
所有的对象都使用 lv_obj_t 指针作为句柄进行引用。之后可以使用该指针来设置或获取对象的属性。

属性

基本属性

所有的对象类型都有一些通用的基本属性:

  • 位置
  • 大小
  • 父级
  • 样式
  • 事件处理程序
  • 等等
    可以使用 lv_obj_set_...lv_obj_get_... 函数设置或者获取这些属性。
/*设置基本对象属性*/
lv_obj_set_size(btn1, 100, 50);   /*设置按钮的大小*/
lv_obj_set_pos(btn1, 20,30);      /*设置按钮的位置*/
特殊属性

对象类型也有特殊的属性。例如,滑块有:

  • 最小值和最大值
  • 当前值
    针对这些特殊属性,每个对象类型可能有独特的API函数。例如,对于滑块:
/*设置滑块特定属性*/
lv_slider_set_range(slider1, 0, 100);                   /*设置最小值和最大值*/
lv_slider_set_value(slider1, 40, LV_ANIM_ON);       /*设置当前值(位置)*/

所有控件的API在它们各自的 文档 中有描述,也可以查看头文件。

工作机制

父子结构

一个父对象可以被视为其子对象的容器。每个对象都必须会有且仅有一个父对象(屏幕除外),但一个父对象可以有任意数量的子对象。父对象的类型没有限制,但是有些对象一般是父对象(例如按钮)或者是子对象(例如标签)。

一起移动

子对象的位置随父对象的位置改变。所有子对象的位置都是相对于父对象。

// 父子结构
lv_obj_t *parent = lv_obj_create(lv_screen_active());
lv_obj_set_size(parent, 300, 240);
lv_obj_t *obj1 = lv_obj_create(parent);
lv_obj_set_pos(obj1, 10, 10);
lv_obj_set_size(obj1, 100, 100);
lv_obj_set_pos(parent, 40, 40);

../_images/par_child2.png

仅在父对象上可见
lv_obj_set_x(obj1, -30);    /*将子对象移出父对象一点点*/

../_images/par_child3.png
可以通过这个方法覆盖此行为 lv_obj_add_flag(obj, LV_OBJ_FLAG_OVERFLOW_VISIBLE),这会允许子对象在父对象之外进行绘制。

创建和删除对象

在LVGL中,可以在运行时动态创建或删除对象。这也就是说,只有当对象被创建之后才会消耗内存资源。
因此,您可以在点击按钮准备打开新界面(屏幕)时再创建新界面(屏幕),并在加载新界面(屏幕)时删除旧界面(屏幕)。
UI可根据设备的当前环境进行创建。例如,可以根据当前连接的传感器创建仪表、图表、条形图和滑块。
每个控件都有自己的 create 函数,函数原型如下:

lv_obj_t * lv_<widget>_create(lv_obj_t * parent, <如果有其他参数>);

通常,创建函数只有一个 parent 参数,指示在哪个对象上创建该控件。
返回值是指向创建出来的控件的指针,类型为 lv_obj_t *
删掉对象适用 lv_obj_delete 函数:

void lv_obj_delete(lv_obj_t *obj);

lv_obj_del() 会立即删除对象。如果出于任何原因无法立即删除对象,可以使用 lv_obj_delete_async ,它会在下一次调用 lv_timer_handler() 时执行删除操作。
可以使用 lv_obj_clean(obj) 删除对象的所有子对象(但不包括对象本身)。
可以使用 lv_obj_delete_delayed(obj, 1000) 在经过一段时间后再删除对象,以毫秒为单位。

屏幕

创建屏幕

屏幕是一种特殊的对象,它们没有父对象。因此可以像这样创建屏幕:

lv_obj_t * scr1 = lv_obj_create(NULL);
获取活动屏幕

每个显示器上都会存在一个活动屏幕。默认情况下,库会为每个显示器创建和加载一个名为“Base object”的屏幕。
要获得当前活动的屏幕,请使用 lv_screen_active() 函数。

加载屏幕

使用 lv_screen_load(lv_obj_t *scr) 来加载新的屏幕。

Layers(层)

使用 lv_screen_load(lv_obj_t *scr) 来加载新的屏幕。 自动生成两个图层:

  • 顶层(top layer)
  • 系统层(system layer)
    它们与屏幕独立,将显示在每个屏幕上。 顶层位于屏幕上每个对象之上, 系统层 位于 顶层 之上。您可以自由地向 顶层 添加任何弹窗。但是, 系统层 受到系统级别的限制(例如,鼠标光标将与 lv_indev_set_cursor() 一起放置在那里)。
    层级: 活动屏幕(screen_active) < 顶层(top layer) < 系统层(system layer)
    lv_layer_top()lv_layer_sys() 函数返回指向顶层和系统层的指针。
用动画加载屏幕

可以使用 lv_screen_load_anim(scr, transition_type, time, delay, auto_del) 来加载一个带动画效果的新屏幕。可以设置以下动画过渡类型:

处理多个显示器

屏幕是在当前选定的 默认显示器 上创建的。 默认显示器 是最后使用 lv_display_create() 注册的显示器。还可以使用 lv_display_set_default(disp) 显式地选择新的默认显示器。
lv_screen_active()lv_screen_load()lv_screen_load_anim() 操作默认显示器。

Parts(部分)

小部件Widget由多个控件组成。例如,一个 Base object 有主要部分和滚动条部分,而一个 Slider 有主要部分、指示器部分和旋钮部分。部件类似于CSS中的 伪元素
LVGL中存在以下预定义的部分:

  • LV_PART_MAIN:类似矩形的背景
  • LV_PART_SCROLLBAR:滚动条(一个或多个)
  • LV_PART_INDICATOR:指示器,例如滑块、条形图、开关或复选框的勾选框
  • LV_PART_KNOB:类似于把手(旋钮),用于调整值
  • LV_PART_SELECTED:指示当前选定的选项或部分
  • LV_PART_ITEMS:如果部件有多个类似的元素(例如表格单元格)则可用
  • LV_PART_CURSOR:标记特定位置,例如文本区域或图表的光标
  • LV_PART_CUSTOM_FIRST:可以从这里添加自定义部分。
    小部件Widgets的主要目的是允许对控件的 “组件(组成部分)” 进行样式设置。

States(状态)

控件可以处于以下状态的组合:

Snapshot(快照)

一个对象和其子对象的快照图像可以一起生成。

Positions,sizes,and layouts(位置、大小和布局)

LVGL的坐标设置的概念受到CSS的启发,LVGL并不完全实现了CSS,但实现了一个类似的子集(有的地方进行了微小调整)。

  • 显式设置的坐标存储在样式中(大小、位置、布局等)
  • 支持最小宽度、最大宽度、最小高度、最大高度
  • 有像素、百分比和“内容(content)”单位
  • x=0;y=0坐标表示父对象的 左上角 + 左或上内边距 + 边框的宽度
  • 宽/高(width/height)表示完整的尺寸,“内容区域”较小,带有填充和边框宽度
  • 支持flexbox和grid布局的部分功能(子集)

单位

  • 像素(pixel):简单地说就是一个以像素为单位的位置。整数总是指像素。
    例如 lv_obj_set_x(btn, 10) (设置按钮的横(x)坐标为10个像素)
  • 百分比(percentage):对象或其父对象大小的百分比。 lv_pct(value) 将一个值转换为百分比。
    例如 lv_obj_set_width(btn, lv_pct(50)) (将按钮的宽度设置为父级宽度的50%)
  • LV_SIZE_CONTENT:设置对象宽度/高度的特殊值,将会根据子对象所需的大小自动调整自身大小。类似于CSS中的 auto
    例如 : lv_obj_set_width(btn, LV_SIZE_CONTENT)(将按钮的宽度设置为自适应内容宽度)

盒子模型

LVGL遵循CSS的 border-box 模型。一个对象的“盒子”由以下部分构成:

  • 边界框:元素的宽度/高度。
  • 边框宽度:边框的宽度。
  • 填充:对象与其子元素之间的间距。
  • 外边距:对象外部的间距(仅由某些布局考虑)
  • 内容:内容区域,即边界框减去边框宽度和内边距的大小。
    The box models of LVGL: The content area is smaller than the bounding box with the padding and border width
    Border属于边界框内,Outline属于边界框外。

重要笔记

本节描述LVGL的行为可能会出现出乎意料的特殊情况。

坐标会被延迟计算

LVGL不会立即重新计算所有坐标变化,这样做是为了提高性能。相反,对象会被标记为"脏(dirty)",在重新绘制屏幕之前,LVGL会检查是否有任何"dirty"对象。如果有,则会刷新它们的位置、大小和布局。
换句话说,如果您需要获取对象的坐标,并且坐标刚刚发生了变化,LVGL需要强制重新计算坐标。要做到这一点,请调用 lv_obj_update_layout()
大小和位置可能取决于父级或布局。因此,lv_obj_update_layout() 会重新计算 obj 屏幕上所有对象的坐标。

删除样式

坐标有两种设置方式,一种是通过style对象设置,它会存储在style对象内部(RAM)中,另外一种是使用lv_obj_set_xxx,它会将数据保存在该对象的本地样式中。
这是一个内部机制,在你使用LVGL时并不太需要重点关注。 然而,有一个情况下你需要注意实现方式。 如果通过以下方式移除对象的样式:

lv_obj_remove_style_all(obj)

或者

lv_obj_remove_style(obj, NULL, LV_PART_MAIN);

这会导致之前设置的坐标也将被移除。
例如:

/* obj1 的大小将在最后被设置回默认值 */
lv_obj_set_size(obj1, 200, 100);  /* 现在 obj1 的大小为 200;100 */
lv_obj_remove_style_all(obj1);    /* 它会移除设置的大小 */
/* obj2 最后将会有 200;100 的大小 */
lv_obj_remove_style_all(obj2);
lv_obj_set_size(obj2, 200, 100);

Position(位置)

最简单的方法

如果想最简单地设置对象的x和y坐标,可以这样操作:

lv_obj_set_x(obj, 10);        //单独设置...
lv_obj_set_y(obj, 20);
lv_obj_set_pos(obj, 10, 20);    //或者使用一个函数

默认情况下,x和y坐标是从父元素的内容区域的左上角开始计算的。例如,如果父元素的每一边都有五个像素的填充(padding),那么上面的代码会把 obj 放置在(15, 25),因为内容区域在填充之后开始计算。
百分比值是通过父元素的内容(content)区域的大小来计算的。

lv_obj_set_x(btn, lv_pct(10)); //x = 父元素内容区域宽度的10%
对齐

在某些情况下,可以方便地从对象默认的左上角更改其定位原点。如果改变了原点,比如改成底部-右侧,那么(0,0)位置的意思是:与底部-右侧对齐。要改变原点,使用如下代码:

lv_obj_set_align(obj, align);

改变对齐方式并设置新的坐标:

lv_obj_align(obj, align, x, y);

有以下对齐选项可用:

lv_obj_center(obj);
//有相同的效果
lv_obj_align(obj, LV_ALIGN_CENTER, 0, 0);

如果父对象的大小改变,则子对象的设置对齐和位置会根据父对象的变化自动调整更新。
上述介绍的功能使对象对齐到其父对象。然而,也可以将对象对齐到任意参考对象。

lv_obj_align_to(obj_to_align, reference_obj, align, x, y);

除了上述的对齐选项外,还可以使用以下选项将对象对齐到参考对象外部:

lv_obj_align_to(label, btn, LV_ALIGN_OUT_TOP_MID, 0, -10);

注意,lv_obj_align_to()不能在对象的坐标或参考对象的坐标发生变化时重新对齐对象。

Size(大小)

最简单的方法

一个对象的宽度和高度也可以很容易地进行设置:

lv_obj_set_width(obj, 200);       //分别设置...
lv_obj_set_height(obj, 100);
lv_obj_set_size(obj, 200, 100);     //或者使用一个函数

百分比值是基于父对象的内容区域大小进行计算的。例如,要将对象的高度设置为屏幕高度:

lv_obj_set_height(obj, lv_pct(100));

大小设置支持特殊值:LV_SIZE_CONTENT。这意味着对象在相应方向上的大小将被设置为其子对象恰好所需的大小。请注意,只有右侧和底部的子对象才会被考虑,而顶部和左侧的子对象仍会被裁剪。这种限制使行为更可预测。
具有 LV_OBJ_FLAG_HIDDENLV_OBJ_FLAG_FLOATING 的对象将被 LV_SIZE_CONTENT 计算忽略。
上述函数设置对象边界框的大小,但内容区域的大小也可以设置。这也就是说,对象的边界框会根据填充的大小进行调整。

lv_obj_set_content_width(obj, 50); //实际宽度:左填充 + 50 + 右填充
lv_obj_set_content_height(obj, 30); //实际高度:顶部填充 + 30 + 底部填充

可以使用以下函数获取边界框和内容区域的大小:

int32_t w = lv_obj_get_width(obj);
int32_t h = lv_obj_get_height(obj);
int32_t content_w = lv_obj_get_content_width(obj);
int32_t content_h = lv_obj_get_content_height(obj);

使用样式

在底层实现中,位置、大小和对齐属性是样式属性。上述描述的“简单函数”隐藏了与样式相关的代码来简化操作,实质上已经在对象的本地样式中设置位置、大小和对齐等属性。
然而,使用样式来设置坐标具有一些重要的优点:

  • 使得同时设置多个对象的宽度/高度等变得简单。比如,使所有滑块的尺寸为 100x10 像素。
  • 还可以在一个位置修改值。
  • 这些数值可以部分地被其他样式覆盖。例如, style_btn 默认将对象的宽度设置为 100x50,但添加 style_full_width 仅覆盖对象的宽度。
  • 对象的位置或大小可以根据状态而有所不同。例如,在 LV_STATE_DEFAULT 状态下宽度为100像素,在 LV_STATE_PRESSED 状态下为120像素。
  • 可以使用样式转换使坐标变化更加平滑。
    以下是使用样式设置对象大小的一些示例代码:
static lv_style_t style;
lv_style_init(&style);
lv_style_set_width(&style, 100);
lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_add_style(btn, &style, LV_PART_MAIN);

为了保持 LVGL API 的精简,只有最常见的坐标设置功能有一个“简单”版本,更复杂的功能可以通过样式来实现。

Translation(位置转换)

现在假设有3个相邻的按钮。它们的位置如上所述。现在你想要在按下按钮时将按钮上移一点。
实现这一目标的一种方法是为其按下状态设置一个新的Y坐标:

// 这种方法有效,但不够灵活,因为按下时的坐标是硬编码的。如果按钮不在y=100处, style_pressed 就不会如预期般工作。
static lv_style_t style_normal;
lv_style_init(&style_normal);
lv_style_set_y(&style_normal, 100);
static lv_style_t style_pressed;
lv_style_init(&style_pressed);
lv_style_set_y(&style_pressed, 80);
lv_obj_add_style(btn1, &style_normal, LV_STATE_DEFAULT);
lv_obj_add_style(btn1, &style_pressed, LV_STATE_PRESSED);
lv_obj_add_style(btn2, &style_normal, LV_STATE_DEFAULT);
lv_obj_add_style(btn2, &style_pressed, LV_STATE_PRESSED);
lv_obj_add_style(btn3, &style_normal, LV_STATE_DEFAULT);
lv_obj_add_style(btn3, &style_pressed, LV_STATE_PRESSED);
// 可以使用平移来解决这个问题
static lv_style_t style_normal;
lv_style_init(&style_normal);
lv_style_set_y(&style_normal, 100);
static lv_style_t style_pressed;
lv_style_init(&style_pressed);
// 平移
lv_style_set_translate_y(&style_pressed, -20);
lv_obj_add_style(btn1, &style_normal, LV_STATE_DEFAULT);
lv_obj_add_style(btn1, &style_pressed, LV_STATE_PRESSED);
lv_obj_add_style(btn2, &style_normal, LV_STATE_DEFAULT);
lv_obj_add_style(btn2, &style_pressed, LV_STATE_PRESSED);
lv_obj_add_style(btn3, &style_normal, LV_STATE_DEFAULT);
lv_obj_add_style(btn3, &style_pressed, LV_STATE_PRESSED);

平移是相对于对象当前位置进行应用的。
在平移中也可以使用百分比值。百分比是相对于对象的大小(而不是相对于父对象的大小)。例如 lv_pct(50) 将使物体移动一半的宽度/高度。
在布局计算之后应用平移。因此,即使对象的位置总是被布局计算,也可以进行平移。
平移实际上移动了对象。这意味着它会使滚动条和 LV_SIZE_CONTENT 大小的对象对位置变化做出反应。

Transformation(大小转换)

对象的大小可以相对于当前大小进行改变。变换后的宽度和高度会分别加在对象的两侧。这意味着,一个变换后的宽度为10px会使对象的宽度增加20个像素。
与位置转换不同,大小转换并不会使对象“实际”变大。换句话说,滚动条、布局和 LV_SIZE_CONTENT 不会对变换后的大小做出反应。因此,大小转换 “只是” 一种视觉效果。
下面的代码会在按钮被按下时放大:

static lv_style_t style_pressed;
lv_style_init(&style_pressed);
lv_style_set_transform_width(&style_pressed, 10);
lv_style_set_transform_height(&style_pressed, 10);
lv_obj_add_style(btn, &style_pressed, LV_STATE_PRESSED);
Min and Max size(最小和最大尺寸)

与CSS类似,LVGL也支持 min-widthmax-widthmin-heightmax-height。这些限制了对象的大小,防止其变得比这些值更小/更大。如果通过百分比或 LV_SIZE_CONTENT 来设置大小,它们尤其有用。

static lv_style_t style_max_height;
lv_style_init(&style_max_height);
lv_style_set_y(&style_max_height, 200);
lv_obj_set_height(obj, lv_pct(100));
lv_obj_add_style(obj, &style_max_height, LV_STATE_DEFAULT); //将高度限制为200像素

也可以使用百分比值,相对于父容器的内容区域的大小。

static lv_style_t style_max_height;
lv_style_init(&style_max_height);
lv_style_set_y(&style_max_height, lv_pct(50));
lv_obj_set_height(obj, lv_pct(100));
lv_obj_add_style(obj, &style_max_height, LV_STATE_DEFAULT); //将高度限制为父容器高度的一半

Layout(布局)

概述

布局可以更新对象子元素的位置和大小。它们可以用于自动排列子元素成一行或一列,或者以更复杂的形式排列。
布局设置的位置和大小会覆盖“正常”的x、y、宽度和高度设置。
每个布局都有一个相同的函数: lv_obj_set_layout(obj, <布局名称>) ,用于在对象上设置布局。

内置布局

LVGL带有两种非常强大的布局:

  • Flexbox:将对象排列成行或列,支持换行和扩展项目。
  • Grid:在二维表中将对象排列成固定位置。
Flags(标志)

有一些标志可以用于对象,以影响它们与布局的行为:

添加新布局

LVGL可以通过自定义布局自由扩展,如下所示:

uint32_t MY_LAYOUT;
...
MY_LAYOUT = lv_layout_register(my_layout_update, &user_data);
...
void my_layout_update(lv_obj_t * obj, void * user_data)
{
    /*如果需要重新定位/调整“obj”的子对象,则会自动调用该函数*/
}

可以添加自定义样式属性,并在更新回调函数中检索和使用它们。例如:

uint32_t MY_PROP;
...
LV_STYLE_MY_PROP = lv_style_register_prop();
...
static inline void lv_style_set_my_prop(lv_style_t * style, uint32_t value)
{
    lv_style_value_t v = {
        .num = (int32_t)value
    };
    lv_style_set_prop(style, LV_STYLE_MY_PROP, v);
}

Styles(样式)

“Styles” 用于设置对象的外观。lvgl中的样式受到CSS的启发。概念如下:

  • 样式是一个 lv_style_t 变量,它可以保存诸如边框宽度、文字颜色等属性。类似于CSS中的 class
  • 样式可以分配给对象以改变它们的外观。在分配时,可以指定目标部分(CSS中的伪元素)和目标状态(伪类)。例如,当滑块处于按下状态时,可以为其添加 style_blue
  • 同样的样式可以被任意数量的对象使用。
  • 样式可以被级联,这意味着可以将多个样式分配给一个对象,每个样式可以具有不同的属性。因此,并非所有的属性都必须在一个样式中指定。 LVGL将在样式中搜索属性,直到找到定义该属性的样式,或者如果没有任何样式指定,则使用默认值。 例如, style_btn 可以生成默认的灰色按钮,而 style_btn_red 可以仅添加 background-color=red 覆盖背景颜色。
  • 最后的样式具有更高的优先级。这意味着,如果一个属性在两个样式中都指定了,对象中最新的样式将被使用。
  • 一些属性(例如文字颜色)可以从父级继承,如果没有在对象中指定的话。
  • 对象的本地样式比“正常”样式的优先级更高。
  • 与CSS不同(在CSS中,伪类描述不同的状态,例如 :focus ),在LVGL中,属性分配给了给定的状态。
  • 当对象更改状态时,可以应用过渡效果。

States(状态)

对象可以处于以下状态的组合中:

  • LV_STATE_DEFAULT:(0x0000) 正常、释放的状态
  • LV_STATE_CHECKED:(0x0001) 切换或选中的状态
  • LV_STATE_FOCUSED:(0x0002) 通过键盘或编码器聚焦,或通过触摸板/鼠标点击
  • LV_STATE_FOCUS_KEY:(0x0004) 仅通过键盘或编码器聚焦,而非通过触摸板/鼠标
  • LV_STATE_EDITED:(0x0008) 由编码器编辑
  • LV_STATE_HOVERED:(0x0010) 鼠标悬停(目前不支持)
  • LV_STATE_PRESSED:(0x0020) 被按下的状态
  • LV_STATE_SCROLLED:(0x0040) 正在滚动的状态
  • LV_STATE_DISABLED:(0x0080) 禁用的状态
  • LV_STATE_USER_1:(0x1000) 自定义状态
  • LV_STATE_USER_2:(0x2000) 自定义状态
  • LV_STATE_USER_3:(0x4000) 自定义状态
  • LV_STATE_USER_4:(0x8000) 自定义状态
    对象可以处于多个状态的组合中,例如同时处于聚焦和按下状态。这被表示为 LV_STATE_FOCUSED | LV_STATE_PRESSED
    样式可以应用于任何状态或状态组合。例如,为默认和按下状态设置不同的背景颜色。 如果某个状态中未定义属性,则会使用与之最匹配的状态的属性。通常情况下,这意味着将使用带有 LV_STATE_DEFAULT 的属性。 如果即使对于默认状态,某个属性也未设置,则将使用默认值。
    但是,“与之最匹配的状态的属性”到底是什么意思呢?状态有一个优先级,由它们的值表示(请参见上面的列表)。值越高,优先级越高。 为了确定使用哪个状态的属性,我们来看一个例子。假设背景颜色定义如下:
  • LV_STATE_DEFAULT:白色
  • LV_STATE_PRESSED:灰色
  • LV_STATE_FOCUSED:红色
  1. 初始情况下,对象处于默认状态,所以情况很简单:属性在对象当前状态中完全定义为白色。
  2. 当对象被按下时,有两个相关属性:白色的默认属性(默认与每个状态相关)和灰色的按下属性。按下状态的优先级为0x0020,高于默认状态的优先级0x0000,因此会使用灰色。
  3. 当对象处于聚焦状态时,与按下状态发生的情况相同,将使用红色。(聚焦状态的优先级高于默认状态)
  4. 当对象处于聚焦和按下状态时,灰色和红色都可以。但是按下状态的优先级高于聚焦状态,因此会使用灰色。
  5. 可以为 LV_STATE_PRESSED | LV_STATE_FOCUSED 设置玫瑰色,此组合状态的优先级为0x0020 + 0x0002 = 0x0022,高于按下状态的优先级,因此将使用玫瑰色。
  6. 当对象处于选中状态时,没有属性来设置此状态的背景颜色。由于缺乏更好的选择,对象仍然保持默认状态的白色属性。
    一些实用的注意事项:
  • 状态的优先级(值)非常直观,这是用户自然期望的结果。例如,如果对象处于聚焦状态,用户仍然希望看到它是否被按下,因此按下状态具有更高的优先级。如果聚焦状态具有更高的优先级,它将覆盖按下的颜色。
  • 如果要为所有状态(例如红色背景颜色)设置属性,只需为默认状态设置即可。如果对象在其当前状态下找不到属性,它将退回到默认状态的属性。
  • 使用 OR 运算符来描述复杂情况下的属性(例如,按下 + 选中 + 聚焦)。
  • 对于不同的状态,使用不同的样式元素可能是个好主意。例如,为释放、按下、选中 + 按下、聚焦、聚焦 + 按下、聚焦 + 按下 + 选中等状态找到背景颜色是相当困难的。相反,例如,使用按下和选中状态的背景颜色,并使用不同的边框颜色指示聚焦状态。

层叠样式

在一个样式中并不需要设置所有的属性。可以给一个对象添加更多的样式,并且后添加的样式可以修改或扩展外观。例如,创建一个通用的灰色按钮样式,然后再创建一个红色按钮的新样式,只设置新的背景颜色。
这就像在CSS中使用类列表一样,比如 <div class=".btn .btn-red">
后添加的样式具有优先权,优先于先前设置的样式。因此,在上述灰色/红色按钮的例子中,应该先添加普通按钮样式,然后再添加红色样式。但是,状态的优先级仍然会被考虑。因此,让我们来看一个例子:

  • 基本按钮样式为默认状态设置了暗灰色的颜色,为按下状态设置了浅灰色的颜色
  • 红色按钮样式只在默认状态下将背景颜色设置为红色
    在这种情况下,当按钮释放(处于默认状态)时,它将是红色的,因为最近添加的样式(红色样式)完全匹配。当按钮被按下时,浅灰色的颜色更匹配,因为它完美描述了当前的状态,所以按钮会是浅灰色的。

继承

一些属性(通常与文本相关的属性)可以从父对象的样式中继承。 只有在对象的样式中没有设置给定属性时(即使是默认状态下也是如此),继承才会应用。 在这种情况下,如果属性是可继承的,属性值将在父对象中搜索,直到某个对象为属性指定一个值。 父对象将使用自己的状态来确定值。因此,如果按钮被按下,并且文本颜色来自于这里,那么按下时文本颜色将会被使用。

Parts(部分)

物体可以由具有自己样式的“部分”组成。
在LVGL中,存在以下预定义部分:

初始化样式和设置/获取属性

样式存储在 lv_style_t 类型的变量中。样式变量应该是静态的、全局的或者动态分配的。 换句话说,它们不能是函数内部的局部变量,在函数结束时会被销毁。 在使用样式之前,应该使用 lv_style_init(&my_style) 进行初始化。样式初始化后,可以添加或更改属性。
属性设置函数的格式如下: lv_style_set_<property_name>(&style, <value>)。例如:

static lv_style_t style_btn;
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0x115588));
lv_style_set_bg_opa(&style_btn, LV_OPA_50);
lv_style_set_border_width(&style_btn, 2);
lv_style_set_border_color(&style_btn, lv_color_black());
static lv_style_t style_btn_red;
lv_style_init(&style_btn_red);
lv_style_set_bg_color(&style_btn_red, lv_plaette_main(LV_PALETTE_RED));
lv_style_set_bg_opa(&style_btn_red, LV_OPA_COVER);

要删除一个属性,可以使用:

lv_style_remove_prop(&style, LV_STYLE_BG_COLOR);

从样式中获取属性的值:

lv_style_value_t v;
lv_res_t res = lv_style_get_prop(&style, LV_STYLE_BG_COLOR, &v);
if(res == LV_RES_OK) {  /*找到了*/
    do_something(v.color);
}

lv_style_value_t 包含 3 个字段:

  • num:用于整数、布尔值和不透明度属性
  • color:用于颜色属性
  • ptr:用于指针属性
    要重置一个样式(释放其所有数据),可以使用:
lv_style_reset(&style);

样式也可以定义为 const,以节省 RAM:

const lv_style_const_prop_t style1_props[] = {
   LV_STYLE_CONST_WIDTH(50),
   LV_STYLE_CONST_HEIGHT(50),
   LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(style1, style1_props);

以后可以像其他样式一样使用 const 样式,但显然不能添加新属性。

向部件添加和删除样式

样式必须将其分配给一个对象才能生效。

添加样式

为了给对象添加样式,请使用 lv_obj_add_style(obj, &style, <selector>)<selector> 是一个按位或运算的值,用于指定要添加样式的部分和状态。下面是一些示例:

lv_obj_add_style(btn, &style_btn, 0);                     /*默认按钮样式*/
lv_obj_add_style(btn, &btn_red, LV_STATE_PRESSED);        /*仅在按下时将部分颜色更改为红色*/
替换样式

使用下列语句来替换对象的特定样式:lv_obj_replace_style(obj, old_style, new_style, selector)。 此函数仅在 selector 匹配 lv_obj_add_style 中使用的 selector 时,才会将 old_style 替换为 new_style。 两种样式,即 old_stylenew_style 都不能为 NULL (添加和删除分别存在不同的函数)。 如果 obj 的样式中存在多个 old_styleselector 的组合,所有出现的情况都将被替换。函数的返回值指示是否至少进行了一次成功替换。
使用 lv_obj_replace_style()

lv_obj_add_style(btn, &style_btn, 0);                      /* 添加按钮样式 */
lv_obj_replace_style(btn, &style_btn, &new_style_btn, 0);  /* 用不同的样式替换按钮样式 */
删除样式

从对象中删除所有样式,请使用 lv_obj_remove_style_all(obj)
要删除特定的样式,请使用 lv_obj_remove_style(obj, style, selector)。 此函数将仅在 selectorlv_obj_add_style() 中使用的 selector 匹配时删除 stylestyle 可以是 NULL,以仅检查 selector 并删除所有匹配的样式。 selector 可以使用 LV_STATE_ANYLV_PART_ANY 值,从任何状态或部件中删除样式。

通知样式更改

如果已经分配给一个对象的样式发生改变(例如添加或更改属性),那么使用该样式的对象应该收到通知。有三种选项可以做到这一点:

  1. 如果你知道改变的属性可以通过简单的重绘来应用(例如颜色或不透明度的变化),只需调用: lv_obj_invalidate(obj) 或者 lv_obj_invalidate(lv_screen_active())
  2. 如果更改或添加了更复杂的样式属性,并且你知道哪些对象受到该样式的影响,请调用: lv_obj_refresh_style(obj, part, property)。要刷新所有部件和属性,请使用: lv_obj_refresh_style(obj, LV_PART_ANY, LV_STYLE_PROP_ANY)
  3. 要让LVGL检查所有对象以查看它们是否使用了样式,并在需要时刷新它们,请调用: lv_obj_report_style_change(&style)。如果 style 是 NULL,则所有对象将收到有关样式更改的通知。
获取对象的属性值

获取对象的最终属性值,考虑级联、继承、本地样式和过渡效果,可以使用此类属性获取函数: lv_obj_get_style_<property_name>(obj, <part>)。 这些函数使用对象的当前状态,如果没有更好的候选项,则返回默认值。例如:

lv_color_t color = lv_obj_get_style_bg_color(btn, LV_PART_MAIN);

本地样式

除了“普通”样式外,对象还可以存储本地样式。 这个概念类似于CSS中的内联样式(例如 <div style="color:red">),但有一些修改。
本地样式与普通样式类似,但不能在其他对象之间共享。如果使用,本地样式会自动分配,并且在对象被删除时释放。它们很适合为对象添加本地定制。
与CSS不同,LVGL中的本地样式可以分配给状态( 伪类)和部分( 伪元素)。
要设置本地属性,请使用如下函数: lv_obj_set_style_<property_name>(obj, <value>, <selector>); 例如:

lv_obj_set_style_bg_color(slider, lv_color_red(), LV_PART_INDICATOR | LV_STATE_FOCUSED);

Properties(属性)

典型的背景属性

在Widgets部件的文档中,您会看到 "该部件使用典型的背景属性 "这样的句子。这些 "典型背景属性 "与以下内容有关:

  • Background
  • Border
  • Outline
  • Shadow
  • Padding
  • Width and height transformation
  • X and Y translation

Transitions(过渡动效)

默认情况下,当对象改变状态(例如,被按下)时,新状态的新属性立即设置。然而,使用过渡效果可以在状态改变时播放动画。 例如,按下按钮时,其背景颜色可以在300毫秒内过渡为按下的颜色。
过渡的参数存储在样式中。可以设置:

  • 过渡时间
  • 开始过渡前的延迟
  • 动画路径(也称为定时或缓动函数)
  • 要进行动画处理的属性
    过渡属性可以为每个状态定义。例如,在默认状态下设置500毫秒的过渡时间意味着当对象进入默认状态时,将应用500毫秒的过渡时间。 在按下状态下设置100毫秒的过渡时间,则在进入按下状态时会有100毫秒的过渡。这个例子的配置意味着迅速进入按下状态,然后慢慢返回默认状态。
    为了描述一个过渡,需要初始化一个 lv_transition_dsc_t 变量,并将其添加到一个样式中:
/*Only its pointer is saved so must static, global or dynamically allocated */
static const lv_style_prop_t trans_props[] = {
                                            LV_STYLE_BG_OPA, LV_STYLE_BG_COLOR,
                                            0, /*End marker*/
};
static lv_style_transition_dsc_t trans1;
lv_style_transition_dsc_init(&trans1, trans_props, lv_anim_path_ease_out, duration_ms, delay_ms);
lv_style_set_transition(&style1, &trans1);

Opacity, Blend modes and Transformations(不透明度,混合模式和变换)

如果 opablend_modetransform_angletransform_zoom 属性被设置为它们的非默认值,LVGL将为Widgets及其所有子级创建一个快照,以便将整个Widgets与设置的不透明度、混合模式和变换属性混合在一起。
这些属性仅在Widgets的 MAIN 部分上产生作用。
创建的快照称为“中间层”或简称为“层”。如果只有 opa 和/或 blend_mode 设置为非默认值,LVGL可以从较小的块构建图层。这些块的大小可以通过 lv_conf.h 中的以下属性配置:

  • LV_LAYER_SIMPLE_BUF_SIZE: [字节] 的理想目标缓冲区大小。LVGL将尝试分配此内存大小。
  • LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE: [字节] 如果无法分配 LV_LAYER_SIMPLE_BUF_SIZE,则使用该大小。
    如果还使用了变换属性,则图层无法以块的形式渲染,而需要分配更大的内存。所需的内存取决于角度、缩放和枢轴参数以及要重绘的区域大小,但永远不会大于Widgets的大小(包括用于阴影、轮廓等的额外绘制大小)。
    如果Widgets部件可以完全覆盖要重绘的区域,LVGL将创建一个RGB图层(渲染速度更快,占用的内存更少)。如果相反情况需要使用ARGB渲染。如果Widgets部件具有半径、 bg_opa != 255、有阴影、轮廓等,则小部件可能无法覆盖其区域。

主题

主题是一系列的样式。如果有一个激活的主题,LVGL会应用它到每个创建的部件上。这将为UI提供一个默认的外观,可以通过添加更多样式来进行修改。
每个显示器可以有不同的主题。例如,你可以在TFT上有一个多彩的主题,在第二个单色显示器上有一个单色的主题。
要为显示器设置一个主题,需要两个步骤:

  1. 初始化一个主题
  2. 将初始化的主题分配给一个显示器。
    主题初始化函数可以有不同的原型。下面的例子显示了如何设置“默认”主题:
lv_theme_t * th = lv_theme_default_init(display,  /*使用该显示器的DPI、大小等*/
                                        LV_COLOR_PALETTE_BLUE, LV_COLOR_PALETTE_CYAN,   /*主色和辅助色板*/
                                        false,    /*亮或暗模式*/
                                        &lv_font_montserrat_10, &lv_font_montserrat_14, &lv_font_montserrat_18); /*小、正常、大字体*/
lv_display_set_theme(display, th); /*将主题分配给显示器*/

包含的主题在 lv_conf.h 中已启用。如果默认主题由 LV_USE_THEME_DEFAULT 启用,LVGL在创建显示器时会自动初始化和设置它。

扩展主题

内置主题可以进行扩展。如果创建了自定义主题,则可以选择父主题。父主题的样式将在自定义主题的样式之前添加。可以通过这种方式链接任意数量的主题。例如,默认主题 -> 自定义主题 -> 深色主题。
lv_theme_set_parent(new_theme, base_theme) 用 new_theme 扩展了 base_theme

Style properties(样式属性)

尺寸和位置(Size and position)

属性名说明默认值是否继承布局影响扩展绘制
width设置对象的宽度。可以使用像素、百分比和 LV_SIZE_CONTENT 值。百分比值相对于父内容区的宽度。Widget dependent
min_width设置最小宽度。可以使用像素和百分比值。百分比值相对于父内容区的宽度。0
max_width设置最大宽度。可以使用像素和百分比值。百分比值相对于父内容区的宽度。LV_COORD_MAX
height设置对象的高度。可以使用像素、百分比和 LV_SIZE_CONTENT 值。百分比值相对于父内容区的高度。Widget dependent
min_height设置最小高度。可以使用像素和百分比值。百分比值相对于父内容区的高度。0
max_height设置最大高度。可以使用像素和百分比值。百分比值相对于父内容区的高度。LV_COORD_MAX
length根据小部件的类型而有所不同。例如,对于 lv_scale,它表示刻度的长度。0
x设置对象的 X 坐标,考虑设置的对齐方式。可以使用像素和百分比值。百分比值相对于父内容区的宽度。0
y设置对象的 Y 坐标,考虑设置的对齐方式。可以使用像素和百分比值。百分比值相对于父内容区的高度。0
align设置对齐方式,指定从父对象的哪个点解释 X 和 Y 坐标。可能的值为:LV_ALIGN_DEFAULTLV_ALIGN_TOP_LEFT/MID/RIGHTLV_ALIGN_BOTTOM_LEFT/MID/RIGHTLV_ALIGN_LEFT/RIGHT_MIDLV_ALIGN_CENTERLV_ALIGN_DEFAULT 表示 LV_ALIGN_TOP_LEFT 具有 LTR 基本方向,LV_ALIGN_TOP_RIGHT 具有 RTL 基本方向。LV_ALIGN_DEFAULT
transform_width使对象在两侧变宽。可以使用像素和百分比(使用 lv_pct(x))值。百分比值相对于对象的宽度。0
transform_height使对象在两侧变高。可以使用像素和百分比(使用 lv_pct(x))值。百分比值相对于对象的高度。0
translate_x在 X 方向上移动对象。应用于布局、对齐和其他定位之后。可以使用像素和百分比值。百分比值相对于对象的宽度。0
translate_y在 Y 方向上移动对象。应用于布局、对齐和其他定位之后。可以使用像素和百分比值。百分比值相对于对象的高度。0
transform_scale_x水平缩放对象。值 256(或 LV_SCALE_NONE)表示正常大小,128 表示半大小,512 表示双倍大小,等等0
transform_scale_y垂直缩放对象。值 256(或 LV_SCALE_NONE)表示正常大小,128 表示半大小,512 表示双倍大小,等等0
transform_rotation旋转对象。值以 0.1 度为单位解释。例如 450 表示 45 度。0
transform_pivot_x设置用于变换的旋转中心点的 X 坐标。相对于对象的左上角。0
transform_pivot_y设置用于变换的旋转中心点的 Y 坐标。相对于对象的左上角。0
transform_skew_x水平倾斜对象。值以 0.1 度为单位解释。例如 450 表示 45 度。0
transform_skew_y垂直倾斜对象。值以 0.1 度为单位解释。例如 450 表示 45 度。0

内边距(Padding)

属性名说明默认值是否继承布局影响扩展绘制
pad_top设置顶部内边距。使内容区域在此方向上变小。0
pad_bottom设置底部内边距。使内容区域在此方向上变小。0
pad_left设置左侧内边距。使内容区域在此方向上变小。0
pad_right设置右侧内边距。使内容区域在此方向上变小。0
pad_row设置行之间的内边距。用于布局。0
pad_column设置列之间的内边距。用于布局。0

外边距(Margin)

属性名说明默认值是否继承布局影响扩展绘制
margin_top设置顶部外边距。对象将与其兄弟节点保持此空间。0
margin_bottom设置底部外边距。对象将与其兄弟节点保持此空间。0
margin_left设置左侧外边距。对象将与其兄弟节点保持此空间。0
margin_right设置右侧外边距。对象将与其兄弟节点保持此空间。0

背景(Background)

属性名说明默认值是否继承布局影响扩展绘制
bg_color设置对象的背景颜色。0xffffff
bg_opa设置背景的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_TRANSP
bg_grad_color设置背景的渐变颜色。仅在 grad_dir 不是 LV_GRAD_DIR_NONE 时使用。0x000000
bg_grad_dir设置背景渐变的方向。可能的值有 LV_GRAD_DIR_NONE/HOR/VERLV_GRAD_DIR_NONE
bg_main_stop设置渐变背景颜色的起始点。0 表示顶部/左侧,255 表示底部/右侧,128 表示中心,等等。0
bg_grad_stop设置渐变背景颜色的终止点。0 表示顶部/左侧,255 表示底部/右侧,128 表示中心,等等。255
bg_main_opa设置第一个渐变颜色的不透明度。255
bg_grad_opa设置第二个渐变颜色的不透明度。255
bg_grad设置渐变定义。指向的实例必须在对象存活期间存在。NULL 表示禁用。它将 BG_GRAD_COLORBG_GRAD_DIRBG_MAIN_STOPBG_GRAD_STOP 包装成一个描述符,并允许使用更多颜色创建渐变。如果设置,则忽略其他与渐变相关的属性。NULL
bg_image_src设置背景图像。可以是指向 lv_image_dsc_t 的指针、文件路径或 LV_SYMBOL_…NULL
bg_image_opa设置背景图像的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
bg_image_recolor设置要混合到背景图像的颜色。0x000000
bg_image_recolor_opa设置背景图像重新着色的强度。值 0、LV_OPA_0LV_OPA_TRANSP 表示不混合,255、LV_OPA_100LV_OPA_COVER 表示完全重新着色,其他值或 LV_OPA_10LV_OPA_20 等按比例解释。LV_OPA_TRANSP
bg_image_tiled如果启用,背景图像将被平铺。可能的值为 true 或 false。0

边框(Border)

属性名说明默认值是否继承布局影响扩展绘制
border_color设置边框的颜色。0x000000
border_opa设置边框的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
border_width设置边框的宽度。只能使用像素值。0
border_side设置仅绘制哪一边的边框。可能的值有 LV_BORDER_SIDE_NONE/TOP/BOTTOM/LEFT/RIGHT/INTERNAL。可以使用 OR-ed 值,例如 LV_BORDER_SIDE_TOPLV_BORDER_SIDE_NONE
border_post设置边框应在子项绘制之前还是之后绘制。true:在子项之后,false:在子项之前绘制0

轮廓(Outline)

属性名说明默认值是否继承布局影响扩展绘制
outline_width设置轮廓的宽度(以像素为单位)。0
outline_color设置轮廓的颜色。0x000000
outline_opa设置轮廓的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
outline_pad设置轮廓的填充,即对象与轮廓之间的间隙。0

阴影(Shadow)

属性名说明默认值是否继承布局影响扩展绘制
shadow_width设置阴影的宽度(以像素为单位)。0
shadow_offset_x设置阴影在 X 方向上的偏移量(以像素为单位)。0
shadow_offset_y设置阴影在 Y 方向上的偏移量(以像素为单位)。0
shadow_spread使阴影计算使用较大或较小的矩形作为基准。值可以是像素,表示区域变大/变小0
shadow_color设置阴影的颜色。0x000000
shadow_opa设置阴影的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER

图像(Image)

属性名说明默认值是否继承布局影响扩展绘制
image_opa设置图像的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
image_recolor设置要混合到图像中的颜色。0x000000
image_recolor_opa设置颜色混合的强度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。0

线条(Line)

属性名说明默认值是否继承布局影响扩展绘制
line_width设置线条的宽度(以像素为单位)。0
line_dash_width设置虚线的宽度(以像素为单位)。注意,虚线仅适用于水平和垂直线条。0
line_dash_gap设置虚线之间的间隙(以像素为单位)。注意,虚线仅适用于水平和垂直线条。0
line_rounded使线条的端点变圆。true:圆形,false:垂直线端点。0
line_color设置线条的颜色。0x000000
line_opa设置线条的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER

弧线(Arc)

属性名说明默认值是否继承布局影响扩展绘制
arc_width设置弧线的宽度(厚度)(以像素为单位)。0
arc_rounded使弧线的端点变圆。true:圆形,false:垂直线端点。0
arc_color设置弧线的颜色。0x000000
arc_opa设置弧线的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
arc_image_src设置将用于遮罩弧线的图像。它对于在弧线上显示复杂效果非常有用。可以是指向 lv_image_dsc_t 的指针或文件路径。NULL

文字(Text)

属性名说明默认值是否继承布局影响扩展绘制
text_color设置文字的颜色。0x000000
text_opa设置文字的不透明度。值 0、LV_OPA_0LV_OPA_TRANSP 表示完全透明,255、LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
text_font设置文字的字体(指向 lv_font_t * 的指针)。LV_FONT_DEFAULT
text_letter_space设置文字的字母间距(以像素为单位)。0
text_line_space设置文字的行间距(以像素为单位)。0
text_decor设置文字装饰。可能的值有 LV_TEXT_DECOR_NONE / UNDERLINE / STRIKETHROUGH。可以使用 OR-ed 值。LV_TEXT_DECOR_NONE
text_align设置文字行的对齐方式。请注意,它不会对齐对象本身,只对齐对象内的文字行。可能的值有 LV_TEXT_ALIGN_LEFT/CENTER/RIGHT/AUTOLV_TEXT_ALIGN_AUTO 检测文字的基准方向并相应地使用左对齐或右对齐。LV_TEXT_ALIGN_AUTO

其他

属性名说明默认值是否继承布局影响扩展绘制
radius设置每个角的半径。值解释为像素(>= 0)或 LV_RADIUS_CIRCLE 表示最大半径。0
clip_corner启用在圆角上裁剪溢出的内容。可以为 truefalse0
opa按此因子缩小对象的所有不透明度值。值 0LV_OPA_0LV_OPA_TRANSP 表示完全透明,255LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
opa_layered首先在图层上绘制对象,然后按比例缩小图层的不透明度因子。值 0LV_OPA_0LV_OPA_TRANSP 表示完全透明,255LV_OPA_100LV_OPA_COVER 表示完全覆盖,其他值或 LV_OPA_10LV_OPA_20 等表示半透明。LV_OPA_COVER
color_filter_dsc将颜色混合到对象的所有颜色中。NULL
color_filter_opa颜色滤镜的混合强度。LV_OPA_TRANSP
anim对象动画的动画模板。应为指向 lv_anim_t 的指针。动画参数是小部件特定的,例如,动画时间可以是光标在文本区域上的闪烁时间或滚轮的滚动时间。有关更多信息,请参阅小部件的文档。NULL
anim_duration动画持续时间(以毫秒为单位)。其含义是小部件特定的。例如,文本区域光标的闪烁时间或滚轮的滚动时间。有关更多信息,请参阅小部件的文档。0
transition初始化的 lv_style_transition_dsc_t,用于描述过渡效果。NULL
blend_mode描述如何将颜色与背景混合。可能的值有 LV_BLEND_MODE_NORMAL/ADDITIVE/SUBTRACTIVE/MULTIPLYLV_BLEND_MODE_NORMAL
layout设置对象的布局。子项将根据布局设置的策略重新定位和调整大小。有关可能的值,请参阅布局文档。0
base_dir设置对象的基准方向。可能的值有 LV_BIDI_DIR_LTR/RTL/AUTOLV_BASE_DIR_AUTO
bitmap_mask_src如果设置,将为小部件创建一个图层,并使用此 A8 位图蒙版对图层进行遮罩。NULL
rotary_sensitivity调整旋转编码器的灵敏度,以 1/256 为单位。意思是,128:将旋转速度减半,512:加倍,256:不变。256

Flex 布局

属性名说明默认值是否继承布局影响扩展绘制
flex_flow定义 Flex 布局应如何排列子项LV_FLEX_FLOW_NONE
flex_main_place定义如何在 Flex 布局方向上对齐子项LV_FLEX_ALIGN_NONE
flex_cross_place定义如何在 Flex 布局方向垂直方向上对齐子项LV_FLEX_ALIGN_NONE
flex_track_place定义如何对齐 Flex 布局轨道LV_FLEX_ALIGN_NONE
flex_grow定义在对象轨道的自由空间中应占据多少比例空间LV_FLEX_ALIGN_ROW

Grid 布局

属性名说明默认值是否继承布局影响扩展绘制
grid_column_dsc_array描述网格列的数组。应以 LV_GRID_TEMPLATE_LAST 终止。NULL
grid_column_align定义如何分配列。LV_GRID_ALIGN_START
grid_row_dsc_array描述网格行的数组。应以 LV_GRID_TEMPLATE_LAST 终止。NULL
grid_row_align定义如何分配行。LV_GRID_ALIGN_START
grid_cell_column_pos设置对象应放置的列。LV_GRID_ALIGN_START
grid_cell_x_align设置对象的水平对齐方式。LV_GRID_ALIGN_START
grid_cell_column_span设置对象应跨越的列数。需要 >= 1。LV_GRID_ALIGN_START
grid_cell_row_pos设置对象应放置的行。LV_GRID_ALIGN_START
grid_cell_y_align设置对象的垂直对齐方式。LV_GRID_ALIGN_START
grid_cell_row_span设置对象应跨越的行数。需要 >= 1。LV_GRID_ALIGN_START

Scroll(滚动)

概述

在LVGL中,滚动功能非常直观:如果一个对象超出了其父对象的内容区域(不包括内边距的大小),则该父对象变得可滚动,并且会出现滚动条。
任何对象都可以是可滚动的,包括 lv_objlv_imagelv_buttonlv_meter 等。
对象可以在一次操作中水平或垂直滚动;不支持对角滚动。

滚动条
模式

滚动条根据配置的 mode 显示。以下是存在的 mode

样式

滚动条有其专用部分,称为 LV_PART_SCROLLBAR。例如,可以通过以下代码将滚动条变为红色:

static lv_style_t style_red;
lv_style_init(&style_red);
lv_style_set_bg_color(&style_red, lv_color_red());
...
lv_obj_add_style(obj, &style_red, LV_PART_SCROLLBAR);

对象在被滚动时会进入 LV_STATE_SCROLLED 状态。这允许在对象滚动时为滚动条或对象本身添加不同的样式。以下代码在对象滚动时使滚动条变为蓝色:

static lv_style_t style_blue;
lv_style_init(&style_blue);
lv_style_set_bg_color(&style_blue, lv_color_blue());
...
lv_obj_add_style(obj, &style_blue, LV_STATE_SCROLLED | LV_PART_SCROLLBAR);

如果 LV_PART_SCROLLBAR 的基方向为 RTL(LV_BASE_DIR_RTL),则垂直滚动条将放置在左侧。请注意,base_dir 样式属性是可继承的。因此,可以直接在对象的 LV_PART_SCROLLBAR 部分或其父对象的主部分上设置 base_dir 属性,以使滚动条继承基方向。
pad_left/right/top/bottom 设置滚动条周围的间距,width 设置滚动条的宽度。

事件

以下事件与滚动相关:

滚动功能特点

除了管理“正常”的滚动之外,还有许多有趣且有用的附加功能。

可滚动

可以使用 lv_obj_remove_flag (obj, LV_OBJ_FLAG_SCROLLABLE) 使对象不可滚动。
不可滚动的对象仍然可以将滚动(链)传播给其父对象。
滚动的方向可以通过 lv_obj_set_scroll_dir(obj, LV_DIR_...) 控制。
方向的可能值有:

滚动链

如果一个对象无法进一步滚动(例如,其内容已经到达最底部),额外的滚动会传播到其父对象。如果父对象可以在该方向滚动,则它将被滚动。它会继续传播到祖父对象和曾祖父对象等。
recording
滚动的传播称为“滚动链”,可以使用 LV_OBJ_FLAG_SCROLL_CHAIN_HOR/VER 标志启用/禁用。如果禁用滚动链,传播会在对象上停止,其父对象不会被滚动。

滚动动量

当用户滚动一个对象并释放时,LVGL可以模拟滚动的惯性动量。就像对象被“抛出”一样,滚动会平滑地减慢。
滚动动量可以通过 LV_OBJ_FLAG_SCROLL_MOMENTUM 标志启用/禁用。

弹性滚动

通常情况下,对象无法滚动超过其内容的极限。也就是说,内容的顶部不能低于对象的顶部。
但是,通过 LV_OBJ_FLAG_SCROLL_ELASTIC 标志,当用户“过度滚动”内容时,会添加一个有趣的效果。滚动会减慢,内容可以滚动到对象内部。当对象被释放时,内容会通过动画返回到有效位置。

对齐

当滚动结束时,对象的子对象可以根据特定规则对齐。子对象可以通过 LV_OBJ_FLAG_SNAPPABLE 标志分别设为可对齐的。
对象可以以下四种方式对齐可对齐的子对象:

  1. 用户滚动一个对象并释放屏幕
  2. LVGL计算滚动动量考虑下滚动结束的位置
  3. LVGL找到最近的滚动点
  4. LVGL通过动画滚动到对齐点
单项滚动

“单项滚动”功能告诉LVGL只允许一次滚动一个可对齐的子对象。这需要使子对象可对齐并设置与 LV_SCROLL_SNAP_NONE 不同的滚动对齐方式。
可以通过 LV_OBJ_FLAG_SCROLL_ONE 标志启用此功能。

聚焦时滚动

想象一下,有很多对象在一个可滚动对象的组中。按下“Tab”键聚焦下一个对象,但它可能在可滚动对象的可见区域之外。如果启用“聚焦时滚动”功能,LVGL将自动滚动对象以使其子对象进入视野。滚动会递归地进行,因此即使是嵌套的可滚动对象也能正确处理。如果对象在选项卡视图的不同页面上,它也会被滚动到视野中。

手动滚动

以下API函数允许手动滚动对象:

  • lv_obj_scroll_by(obj, x, y, LV_ANIM_ON/OFF)xy 值滚动
  • lv_obj_scroll_to(obj, x, y, LV_ANIM_ON/OFF) 滚动以使给定坐标到达左上角
  • lv_obj_scroll_to_x(obj, x, LV_ANIM_ON/OFF) 滚动以使给定坐标到达左侧
  • lv_obj_scroll_to_y(obj, y, LV_ANIM_ON/OFF) 滚动以使给定坐标到达顶部
    有时您可能需要检索元素的滚动位置,以便稍后恢复或根据当前滚动动态显示一些元素。以下示例展示了如何结合滚动事件和存储滚动顶部位置:
static int scroll_value = 0;
static void store_scroll_value_event_cb(lv_event_t* e) {
  lv_obj_t* screen = lv_event_get_target(e);
  scroll_value = lv_obj_get_scroll_top(screen);
  printf("%d pixels are scrolled out on the top\n", scroll_value);
}
lv_obj_t* container = lv_obj_create(NULL);
lv_obj_add_event_cb(container, store_scroll_value_event_cb, LV_EVENT_SCROLL, NULL);

滚动坐标可以通过不同的轴检索:

  • lv_obj_get_scroll_x(obj) 获取对象的 x 坐标
  • lv_obj_get_scroll_y(obj) 获取对象的 y 坐标
  • lv_obj_get_scroll_top(obj) 获取顶部的滚动坐标
  • lv_obj_get_scroll_bottom(obj) 获取底部的滚动坐标
  • lv_obj_get_scroll_left(obj) 获取左侧的滚动坐标
  • lv_obj_get_scroll_right(obj) 获取右侧的滚动坐标

自身尺寸

自身尺寸是对象的一个属性。通常情况下,用户不应使用此参数,但如果创建自定义小部件,它可能会有用。
简而言之,自身尺寸确定对象内容的大小。为了更好地理解它,请以表格为例。假设它有10行,每行50像素高。因此,内容的总高度为500像素。换句话说,“自身高度”为500像素。如果用户仅为表格设置200像素的高度,LVGL会看到自身尺寸更大,并使表格可滚动。
这意味着不仅子对象可以使一个对象可滚动,更大的自身尺寸也会使其可滚动。
LVGL使用 LV_EVENT_GET_SELF_SIZE 事件获取对象的自身尺寸。以下是处理该事件的示例:

if(event_code == LV_EVENT_GET_SELF_SIZE) {
    lv_point_t * p = lv_event_get_param(e);
  //如果 x 或 y < 0,则不需要现在计算
  if(p->x >= 0) {
    p->x = 200; //设置或计算自身宽度
  }
  if(p->y >= 0) {
    p->y = 50;  //设置或计算自身高度
  }
}

Layers(图层)

在LVGL中,“层”可以有多种解释:

  1. 小部件创建的顺序自然形成了小部件的层次
  2. 可以使用永久的屏幕大小的层
  3. 对于某些绘图操作,LVGL会首先将一个小部件及其所有子小部件渲染到一个缓冲区(即“层”)中

创建顺序

默认情况下,LVGL会在旧对象上绘制新对象。
例如,假设我们向父对象添加一个名为button1的按钮,然后添加另一个名为button2的按钮。button1(及其子对象)将在背景中,button1会被button2及其子对象覆盖。
../_images/layers.png

/* 创建一个屏幕 */
lv_obj_t * scr = lv_obj_create(NULL, NULL);
lv_screen_load(scr);          /* 加载屏幕 */
/* 创建两个按钮 */
lv_obj_t * btn1 = lv_button_create(scr, NULL);     /* 在屏幕上创建一个按钮 */
lv_button_set_fit(btn1, true, true);               /* 启用根据内容自动设置大小 */
lv_obj_set_pos(btn1, 60, 40);                      /* 设置按钮的位置 */
lv_obj_t * btn2 = lv_button_create(scr, btn1);     /* 复制第一个按钮 */
lv_obj_set_pos(btn2, 180, 80);                     /* 设置按钮的位置 */
/* 向按钮添加标签 */
lv_obj_t * label1 = lv_label_create(btn1, NULL);   /* 在第一个按钮上创建一个标签 */
lv_label_set_text(label1, "Button 1");             /* 设置标签的文本 */
lv_obj_t * label2 = lv_label_create(btn2, NULL);   /* 在第二个按钮上创建一个标签 */
lv_label_set_text(label2, "Button 2");             /* 设置标签的文本 */
/* 删除第二个标签 */
lv_obj_delete(label2);
更改顺序

有四种显式方法可以将对象移到前景:

类似屏幕的层

Top顶层和sys系统层

LVGL使用两个特殊的层,分别是 layer_toplayer_sys。这两个层在显示器的所有屏幕上都是可见的。然而,它们不会在多个物理显示器之间共享。layer_top 总是位于默认屏幕(lv_screen_active())之上,而 layer_sys 则位于 layer_top 之上。
使用 lv_layer_top()lv_layer_sys() 获取这些层。
这些层的工作方式与其他小部件类似,可以设置样式、滚动,并在其上创建任何类型的小部件。
layer_top 可用于创建一些在所有地方可见的内容。例如,一个菜单栏,一个弹出窗口等。如果启用 click 属性,则 layer_top 会吸收所有用户点击并充当一个模态窗口。

lv_obj_add_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);

layer_sys 在LVGL中也用于类似目的。例如,它将鼠标光标放置在所有层之上,以确保它始终可见。

底层

类似于 topsys 层,底层也是屏幕大小,但它位于活动屏幕下方。仅当活动屏幕的背景不透明度小于255时才可见。
使用 lv_layer_bottom() 获取底层。

绘图层

某些样式属性使LVGL分配一个缓冲区并首先在其中渲染一个小部件及其子小部件。然后该层在应用一些转换或其他修改后会合并到屏幕或其父层。

简单层

以下样式属性会触发创建一个“简单层”:

  • opa_layered
  • bitmap_mask_src
  • blend_mode
    在这种情况下,小部件将被切片成 LV_DRAW_SW_LAYER_SIMPLE_BUF_SIZE 大小的块。
    如果没有足够内存用于新块,LVGL将在另一个块渲染并释放时尝试分配层。
转换层

当小部件进行转换时,需要渲染小部件的较大部分以提供足够的数据进行转换。LVGL尽量渲染小部件的较小区域,但由于转换的特性,在这种情况下无法进行切片。
以下样式属性会触发创建一个“转换层”:

  • transform_scale_x
  • transform_scale_y
  • transform_skew_x
  • transform_skew_y
  • transform_rotate
剪裁角

clip_corner 样式属性还会使LVGL为小部件的顶部和底部创建具有半径高度的两个层。

API

lv_obj_draw.h
lv_types.h
lv_display.h

Events(事件)

在LVGL中,当某个对象发生用户可能感兴趣的事情时,会触发事件,例如:

  • 被点击
  • 被滚动
  • 值改变
  • 重新绘制等
    除了小部件之外,显示器和输入设备也可以注册事件。

为小部件添加事件

用户可以为对象分配回调函数来处理其事件。实际上,它看起来像这样:

lv_obj_t * btn = lv_button_create(lv_screen_active());
lv_obj_add_event_cb(btn, my_event_cb, LV_EVENT_CLICKED, NULL);   /*分配事件回调*/
...
static void my_event_cb(lv_event_t * event)
{
    printf("Clicked\n");
}

在这个例子中,LV_EVENT_CLICKED 表示只有点击事件会调用 my_event_cb。查看事件代码列表 了解所有选项。可以使用 LV_EVENT_ALL 接收所有事件。
lv_obj_add_event() 的最后一个参数是一个指向任何自定义数据的指针,这些数据将在事件中可用。稍后将详细描述。
可以向对象添加更多事件,如下所示:

lv_obj_add_event_cb(obj, my_event_cb_1, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(obj, my_event_cb_2, LV_EVENT_PRESSED, NULL);
lv_obj_add_event_cb(obj, my_event_cb_3, LV_EVENT_ALL, NULL);       /*无过滤,接收所有事件*/

即使是同一个事件回调也可以在对象上使用不同的 user_data。例如:

lv_obj_add_event_cb(obj, increment_on_click, LV_EVENT_CLICKED, &num1);
lv_obj_add_event_cb(obj, increment_on_click, LV_EVENT_CLICKED, &num2);

事件将按添加的顺序调用。
其他对象也可以使用相同的事件回调。
以同样的方式,可以将事件附加到输入设备和显示器,如下所示:

lv_display_add_event_cb(disp, event_cb, LV_EVENT_RESOLUTION_CHANGED, NULL);
lv_indev_add_event_cb(indev, event_cb, LV_EVENT_CLICKED, NULL);

从小部件中删除事件

uint32_t i;
uint32_t event_cnt = lv_obj_get_event_count(obj);
for(i = 0; i < event_cnt; i++) {
    lv_event_dsc_t * event_dsc = lv_obj_get_event_dsc(obj, i);
    if(lv_event_dsc_get_cb(event_dsc) == some_event_cb) {
        lv_obj_remove_event(obj, i);
        break;
    }
}

事件代码

事件代码可以分为以下几类:

  • 输入设备事件
  • 绘图事件
  • 其他事件
  • 特殊事件
  • 自定义事件
    所有对象(如按钮、标签、滑块等)无论其类型如何,都会接收输入设备、绘图和其他事件。
    但是,特殊事件特定于特定的小部件类型。请参阅小部件文档 了解何时发送它们。
    自定义事件由用户添加,永远不会由LVGL发送。
    现有的事件代码如下:
输入设备事件
绘图事件
特殊事件
其他事件
显示事件
自定义事件

任何自定义事件代码都可以通过 uint32_t MY_EVENT_1 = lv_event_register_id() 注册。
它们可以使用 lv_event_send(obj, MY_EVENT_1, &some_data) 发送给任何对象。

发送事件

要手动将事件发送到对象,请使用 lv_obj_send_event(obj, <EVENT_CODE>, &some_data)
例如,这可以用来通过模拟按钮按下手动关闭消息框(尽管有更简单的方法来做到这一点):

/*模拟按下第一个按钮(索引从零开始)*/
uint32_t btn_id = 0;
lv_event_send(mbox, LV_EVENT_VALUE_CHANGED, &btn_id);

同样适用于显示和输入设备,使用 lv_display_send_event(obj, <EVENT_CODE>, &some_data)lv_indev_send_event(obj, <EVENT_CODE>, &some_data)

刷新事件

LV_EVENT_REFRESH 是一个特殊事件,因为它的设计目的是让用户通知对象刷新自己。一些示例:

  • 通知标签根据一个或多个变量(例如当前时间)刷新其文本
  • 当语言更改时刷新标签
  • 如果满足某些条件(例如输入正确的PIN码),则启用按钮
  • 如果超出限制,则向对象添加/移除样式等

lv_event_t 字段

lv_event_t 是传递给事件回调的唯一参数,它包含有关事件的所有数据。可以从中获取以下值:

事件冒泡

如果 lv_obj_add_flag(obj, LV_OBJ_FLAG_EVENT_BUBBLE) 被启用,所有事件也将发送到对象的父对象。如果父对象也启用了 LV_OBJ_FLAG_EVENT_BUBBLE,则事件将发送到其父对象,以此类推。
事件的目标参数始终是当前目标对象,而不是原始对象。要获取原始目标,请在事件处理程序中调用 lv_event_get_target_obj(e)。

API

lv_obj_event.h
lv_types.h
lv_event.h

Input devices(输入设备)

输入设备通常指:

  • 类似指针的输入设备,如触控板或鼠标
  • 键盘,如普通键盘或简单的数字键盘
  • 带有左右旋转和按压选项的编码器
  • 分配给屏幕特定点的外部硬件按钮
    重要:
    在继续阅读之前,请先阅读输入设备的移植部分。

指针

光标

指针输入设备(如鼠标)可以有一个光标。

lv_indev_t * mouse_indev = lv_indev_create();
...
LV_IMAGE_DECLARE(mouse_cursor_icon);                          /*声明图像源*/
lv_obj_t * cursor_obj = lv_image_create(lv_screen_active());  /*为光标创建图像对象*/
lv_image_set_src(cursor_obj, &mouse_cursor_icon);             /*设置图像源*/
lv_indev_set_cursor(mouse_indev, cursor_obj);                 /*将图像对象连接到驱动程序*/

注意,光标对象应该有 lv_obj_remove_flag(cursor_obj, LV_OBJ_FLAG_CLICKABLE)。对于图像,默认情况下禁用点击。

手势

指针输入设备可以检测基本的手势。默认情况下,大多数小部件会将手势发送到其父对象,因此最终可以在屏幕对象上以 LV_EVENT_GESTURE 事件的形式检测到手势。例如:

void my_event(lv_event_t * e)
{
  lv_obj_t * screen = lv_event_get_current_target(e);
  lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active());
  switch(dir) {
    case LV_DIR_LEFT:
      ...
      break;
    case LV_DIR_RIGHT:
      ...
      break;
    case LV_DIR_TOP:
      ...
      break;
    case LV_DIR_BOTTOM:
      ...
      break;
  }
}
...
lv_obj_add_event_cb(screen1, my_event, LV_EVENT_GESTURE, NULL);

要防止手势事件从对象传递到父对象,请使用 lv_obj_remove_flag(obj, LV_OBJ_FLAG_GESTURE_BUBBLE)
注意,如果对象正在被滚动,则不会触发手势。
如果您在手势上执行了一些操作,可以在事件处理程序中调用 lv_indev_wait_release(lv_indev_active()) 来防止LVGL发送进一步的输入设备相关事件。

键盘和编码器

您可以使用键盘或编码器(旋钮)完全控制用户界面,而无需触控板或鼠标。它的工作方式类似于PC上的TAB键,用于在应用程序或网页中选择一个元素。

组 Groups

您希望使用键盘或编码器控制的对象需要添加到一个组中。在每个组中,有且只有一个焦点对象接收按键或编码器的操作。例如,如果一个文本区域被聚焦并且您按下键盘上的一些字母,这些键将被发送并插入到文本区域中。同样,如果一个滑块被聚焦并且您按下左或右箭头,滑块的值将会改变。
您需要将输入设备与一个组关联。一个输入设备只能向一个组发送按键事件,但一个组可以接收来自多个输入设备的数据。
要创建一个组,请使用 lv_group_t * g = lv_group_create(),并使用 lv_group_add_obj(g, obj) 将对象添加到组中。
要将组与输入设备关联,请使用 lv_indev_set_group(indev, g)

键 Keys

有一些预定义的键具有特殊含义:

  • LV_KEY_NEXT:聚焦下一个对象
  • LV_KEY_PREV:聚焦上一个对象
  • LV_KEY_ENTER:触发 LV_EVENT_PRESSEDLV_EVENT_CLICKEDLV_EVENT_LONG_PRESSED 等事件
  • LV_KEY_UP:增加值或向上移动
  • LV_KEY_DOWN:减少值或向下移动
  • LV_KEY_RIGHT:增加值或向右移动
  • LV_KEY_LEFT:减少值或向左移动
  • LV_KEY_ESC:关闭或退出(例如关闭下拉列表)
  • LV_KEY_DEL:删除(例如,文本区域中的右侧字符)
  • LV_KEY_BACKSPACE:删除左侧的字符(例如在文本区域中)
  • LV_KEY_HOME:回到开头/顶部(例如在文本区域中)
  • LV_KEY_END:到达结尾(例如在文本区域中)
    read_cb() 函数中最重要的特殊键是:
  • LV_KEY_NEXT
  • LV_KEY_PREV
  • LV_KEY_ENTER
  • LV_KEY_UP
  • LV_KEY_DOWN
  • LV_KEY_LEFT
  • LV_KEY_RIGHT
    您应该将一些键转换为这些特殊键,以支持在组中导航和与选定对象交互。
    通常,只需要使用 LV_KEY_LEFTLV_KEY_RIGHT,因为大多数对象都可以通过它们完全控制。
    对于编码器,您应该只使用 LV_KEY_LEFTLV_KEY_RIGHTLV_KEY_ENTER
编辑和导航模式

由于键盘有很多键,因此可以轻松地在对象之间导航并使用键盘编辑它们。但是编码器的“键”数量有限,因此使用默认选项导航比较困难。为了解决这个问题,使用了导航和编辑模式。
在导航模式下,编码器的 LV_KEY_LEFTLV_KEY_RIGHT 被转换为 LV_KEY_NEXTLV_KEY_PREV。因此,通过旋转编码器将选择下一个或上一个对象。按下 LV_KEY_ENTER 将切换到编辑模式。
在编辑模式下,通常使用 LV_KEY_NEXTLV_KEY_PREV 修改对象。根据对象的类型,短按或长按 LV_KEY_ENTER 会切换回导航模式。通常,无法按下的对象(如滑块)在短按时离开编辑模式。但对于短按有意义的对象(如按钮),需要长按。

默认组

交互式小部件(如按钮、复选框、滑块等)可以自动添加到默认组。只需创建一个组 lv_group_t * g = lv_group_create() 并使用 lv_group_set_default(g) 设置默认组。
不要忘记使用 lv_indev_set_group(my_indev, g) 将一个或多个输入设备分配给默认组。

样式

如果一个对象通过触控板点击或通过编码器或键盘聚焦,它将进入 LV_STATE_FOCUSED 状态。因此,将应用聚焦样式。
如果一个对象切换到编辑模式,它将进入 LV_STATE_FOCUSED | LV_STATE_EDITED 状态,因此将显示这些样式属性。
欲了解更详细的描述,请阅读样式部分。

API

lv_indev_scroll.h
lv_api_map_v8.h
lv_indev.h
lv_types.h

Diaplays(显示)

LVGL中显示的基本概念在移植部分有解释。因此,在继续阅读之前,请先阅读该部分。

多显示支持

在LVGL中,你可以有多个显示,每个显示都有自己的驱动程序、小部件和色深。
创建更多显示非常简单:只需使用lv_display_create()并设置缓冲区和flush_cb。当你创建UI时,使用lv_display_set_default(disp)来告诉库在哪个显示上创建对象。
为什么你需要多显示支持?以下是一些例子:

  • 使用一个“正常”的TFT显示屏进行本地UI,并按需在VNC上创建“虚拟”屏幕。(你需要添加自己的VNC驱动程序)。
  • 有一个大TFT显示屏和一个小的单色显示屏。
  • 在一个大型仪器或技术中使用一些较小的简单显示屏。
  • 有两个大TFT显示屏:一个用于客户,一个用于店员。
使用单个显示

使用多个显示可能很有用,但在大多数情况下并不需要。因此,如果你只注册一个显示,多显示处理的整个概念完全隐藏。默认情况下,使用最后创建的(唯一)显示。
lv_screen_active()lv_screen_load()lv_layer_top()lv_layer_sys()LV_HOR_RESLV_VER_RES始终应用于最近创建的(默认)显示。如果你将NULL作为disp参数传递给显示相关函数,通常会使用默认显示。例如,lv_display_trigger_activity(NULL)将在默认显示上触发用户活动。(见下文不活动)。

镜像显示

要将一个显示的图像镜像到另一个显示,不需要使用多显示支持。只需将flush_cb中接收到的缓冲区传输到另一个显示即可。

分割图像

你可以从一组较小的显示中创建一个更大的虚拟显示。你可以如下创建:

  1. 将显示的分辨率设置为大显示的分辨率。
  2. flush_cb中,截断并修改每个显示的area参数。
  3. 使用截断的区域将缓冲区的内容发送到每个实际显示。

屏幕

每个显示都有自己的一组屏幕及其上的对象。
注意不要混淆显示和屏幕:

  • 显示是绘制像素的物理硬件。
  • 屏幕是与特定显示相关的高级根对象。一个显示可以有多个屏幕与之关联,但反之则不然。
    屏幕可以被认为是最高级别的容器,没有父对象。屏幕的大小始终等于其显示,其原点为(0, 0)。因此,屏幕的坐标无法更改,即lv_obj_set_pos()lv_obj_set_size()或类似函数不能用于屏幕。
    屏幕可以从任何对象类型创建,但最典型的类型是基础对象图像(用于创建壁纸)。
    要创建一个屏幕,使用lv_obj_t * scr = lv_<type>_create(NULL)NULL表示没有父对象。
    要加载一个屏幕,使用lv_screen_load(scr)。要获取活动屏幕,使用lv_screen_active()。这些函数在默认显示上工作。如果你想指定哪个显示工作,使用lv_display_get_screen_active(disp)和lv_display_load_screen(disp, scr)。屏幕也可以通过动画加载。更多内容请阅读这里
    屏幕可以使用lv_obj_delete(scr)删除,但确保不删除当前加载的屏幕。
    image-20240607162408300
透明屏幕

通常,屏幕的不透明度是LV_OPA_COVER,为其子对象提供一个实心背景。如果不是这种情况(不透明度< 100%),显示的bottom_layer会可见。如果底层的不透明度也不是LV_OPA_COVER,LVGL将没有实心背景进行绘制。
这种配置(透明屏幕和显示)可以用于创建例如OSD菜单,在较低层播放视频,在较高层叠加菜单。
为了正确渲染屏幕,显示的颜色格式需要设置为具有alpha通道的格式。
总之,要为类似OSD菜单的UI启用透明屏幕和显示:

显示功能

不活动

在每个显示上测量用户的不活动时间。每次使用输入设备(如果与显示关联)都算作一次活动。要获取自上次活动以来经过的时间,请使用lv_display_get_inactive_time(disp)。如果传递NULL,将返回所有显示中最短的不活动时间(NULL不仅仅是默认显示)。
你可以使用lv_display_trigger_activity(disp)手动触发一次活动。如果dispNULL,将使用默认屏幕(而不是所有显示)。

背景 Background

每个显示都有背景颜色、背景图像和背景不透明度属性。当当前屏幕是透明的或未定位以覆盖整个显示时,它们会变得可见。
背景颜色是填充显示的简单颜色。可以使用lv_obj_set_style_bg_color(obj, color)进行调整。
显示背景图像是文件路径或指向lv_image_dsc_t变量(转换后的图像数据)的指针,用作壁纸。可以使用lv_obj_set_style_bg_image_src(obj, &my_img)进行设置。如果配置了背景图像,背景将不会用bg_color填充。
背景颜色或图像的不透明度可以使用lv_obj_set_style_bg_opa(obj, opa)进行调整。
这些函数的disp参数可以为NULL以选择默认显示。

API

lv_refr.h
lv_display.h
lv_types.h

Colors(颜色)

颜色模块处理所有与颜色相关的功能,如更改颜色深度、从十六进制代码创建颜色、在颜色深度之间转换、混合颜色等。
lv_color_t 类型用于以RGB888格式存储颜色。无论LV_COLOR_DEPTH如何,这种类型和格式几乎在所有API中使用。

创建颜色

RGB

从红色、绿色和蓝色通道值创建颜色:

/* 所有通道的取值范围都是 0-255 */
lv_color_t c = lv_color_make(red, green, blue);
/* 同样可以用于常量初始化 */
lv_color_t c = LV_COLOR_MAKE(red, green, blue);
/* 从十六进制代码 0x000000 到 0xFFFFFF 解释为红 + 绿 + 蓝 */
lv_color_t c = lv_color_hex(0x123456);
/* 从3位数,相当于 lv_color_hex(0x112233) */
lv_color_t c = lv_color_hex3(0x123);
HSV

从色相、饱和度和明度值创建颜色:

// h = 0..359, s = 0..100, v = 0..100
lv_color_t c = lv_color_hsv_to_rgb(h, s, v);
// 所有通道的取值范围都是 0-255
lv_color_hsv_t c_hsv = lv_color_rgb_to_hsv(r, g, b);
// 从 lv_color_t 变量
lv_color_hsv_t c_hsv = lv_color_to_hsv(color);
调色板

LVGL 包含Material Design 的调色板的颜色。在这个系统中,所有命名的颜色都有一个名义上的主色,以及四个较暗的和五个较亮的变体。
颜色的名称如下:

  • LV_PALETTE_RED
  • LV_PALETTE_PINK
  • LV_PALETTE_PURPLE
  • LV_PALETTE_DEEP_PURPLE
  • LV_PALETTE_INDIGO
  • LV_PALETTE_BLUE
  • LV_PALETTE_LIGHT_BLUE
  • LV_PALETTE_CYAN
  • LV_PALETTE_TEAL
  • LV_PALETTE_GREEN
  • LV_PALETTE_LIGHT_GREEN
  • LV_PALETTE_LIME
  • LV_PALETTE_YELLOW
  • LV_PALETTE_AMBER
  • LV_PALETTE_ORANGE
  • LV_PALETTE_DEEP_ORANGE
  • LV_PALETTE_BROWN
  • LV_PALETTE_BLUE_GREY
  • LV_PALETTE_GREY
    要获取主颜色,使用 lv_color_t c = lv_palette_main(LV_PALETTE_...)
    对于调色板颜色的较亮变体,使用 lv_color_t c = lv_palette_lighten(LV_PALETTE_..., v)v 可以是 1…5。对于调色板颜色的较暗变体,使用 lv_color_t c = lv_palette_darken(LV_PALETTE_..., v)v 可以是 1…4。
修改和混合颜色

以下函数可以修改颜色:

// 变亮一个颜色。0:无变化,255:白色
lv_color_t c = lv_color_lighten(c, lvl);
// 变暗一个颜色。0:无变化,255:黑色
lv_color_t c = lv_color_darken(lv_color_t c, lv_opa_t lvl);
// 变亮或变暗一个颜色。0:黑色,128:无变化,255:白色
lv_color_t c = lv_color_change_lightness(lv_color_t c, lv_opa_t lvl);
// 按给定比例混合两种颜色。0:完全 c2,255:完全 c1,128:一半 c1 和 一半 c2
lv_color_t c = lv_color_mix(c1, c2, ratio);
内置颜色

lv_color_white()lv_color_black() 分别返回 0xFFFFFF0x000000

不透明度

为了描述不透明度,lv_opa_t 类型由 uint8_t 创建。还引入了一些特殊用途的定义:

  • LV_OPA_TRANSP 值:0,表示无不透明度,使颜色完全透明
  • LV_OPA_10 值:25,表示颜色只覆盖很少
  • LV_OPA_20 ... OPA_80 按逻辑顺序排列
  • LV_OPA_90 值:229,表示颜色几乎完全覆盖
  • LV_OPA_COVER 值:255,表示颜色完全覆盖(完全不透明)
    你还可以在lv_color_mix() 中使用 LV_OPA_* 定义作为混合比例。

API

lv_color.h
lv_blend_neon.h
lv_color_op.h

Fonts(字体)

在LVGL中,字体是包含位图和渲染单个字母(字形)所需的其他信息的集合。字体存储在lv_font_t变量中,可以在样式的text_font字段中设置。例如:

lv_style_set_text_font(&my_style, &lv_font_montserrat_28);  /*设置较大的字体*/

字体有一个格式属性。它描述了字形绘制数据的存储方式。它有2个类别:传统的简单格式和高级格式。在传统的简单格式中,字体存储在一个简单的位图数组中。在高级格式中,字体以不同的方式存储,如矢量、SVG等。
在传统的简单格式中,为像素存储的值决定了像素的不透明度。通过更高的bpp(每像素位数),字母的边缘可以更加平滑。可能的bpp值有1、2、4和8(值越高,质量越好)。
格式属性还影响存储字体所需的内存量。例如,format = LV_FONT_GLYPH_FORMAT_A4使字体的大小几乎是format = LV_FONT_GLYPH_FORMAT_A1的四倍。

Unicode支持

LVGL支持UTF-8编码的Unicode字符。你的编辑器需要配置为将代码/文本保存为UTF-8(通常这是默认设置),并确保在lv_conf.hLV_TXT_ENC设置为LV_TXT_ENC_UTF8。(这是默认值)
要测试它,可以尝试:

lv_obj_t * label1 = lv_label_create(lv_screen_active(), NULL);
lv_label_set_text(label1, LV_SYMBOL_OK);

如果一切正常,应显示一个✓字符。

内置字体

有几种不同大小的内置字体,可以在lv_conf.h中通过*LV_FONT_…*定义启用。

常规字体

包含所有ASCII字符、度数符号(U+00B0)、圆点符号(U+2022)和内置符号(见下文)。

特殊字体
  • LV_FONT_MONTSERRAT_28_COMPRESSED:与普通的28 px字体相同,但存储为3 bpp的压缩字体
  • LV_FONT_DEJAVU_16_PERSIAN_HEBREW:16 px字体,包含普通范围+希伯来语、阿拉伯语、波斯字母及其所有形式
  • LV_FONT_SIMSUN_16_CJK:16 px字体,包含普通范围加1000个最常见的CJK部首
  • LV_FONT_UNSCII_8:8 px像素完美字体,仅包含ASCII字符
  • LV_FONT_UNSCII_16:16 px像素完美字体,仅包含ASCII字符
    内置字体是全局变量,如16 px高度的字体名为lv_font_montserrat_16。要在样式中使用它们,只需添加一个指向字体变量的指针,如上所示。
    bpp = 4的内置字体包含ASCII字符,并使用Montserrat字体。
    除了ASCII范围之外,内置字体还添加了来自FontAwesome字体的以下符号。
    ../_images/symbols.png
    符号可以单独使用:
lv_label_set_text(my_label, LV_SYMBOL_OK);

也可以与字符串一起使用(编译时字符串连接):

lv_label_set_text(my_label, LV_SYMBOL_OK "Apply");

或者多个符号一起使用:

lv_label_set_text(my_label, LV_SYMBOL_OK LV_SYMBOL_WIFI LV_SYMBOL_PLAY);

特殊功能

双向支持

大多数语言使用从左到右(LTR)的书写方向,但一些语言(如希伯来语、波斯语或阿拉伯语)使用从右到左(RTL)的方向。
LVGL不仅支持RTL文本,还支持混合(即双向,BiDi)文本渲染。一些示例:
../_images/bidi.png
通过在lv_conf.h中启用LV_USE_BIDI来启用BiDi支持。
所有文本都有一个基方向(LTR或RTL),它决定了一些渲染规则和文本的默认对齐方式(左对齐或右对齐)。然而,在LVGL中,基方向不仅适用于标签。这是一个可以为每个对象设置的通用属性。如果未设置,则将从父对象继承。这意味着只需设置屏幕的基方向,所有对象将继承它。
可以通过在lv_conf.h中设置LV_BIDI_BASE_DIR_DEF来设置屏幕的默认基方向,其他对象将从其父对象继承基方向。
要设置对象的基方向,请使用lv_obj_set_style_base_dir(obj, base_dir, selector)。可能的基方向有:

  • LV_BASE_DIR_LTR:从左到右基方向
  • LV_BASE_DIR_RTL:从右到左基方向
  • LV_BASE_DIR_AUTO:自动检测基方向
    这个列表总结了RTL基方向对对象的影响:
  • 默认在右侧创建对象
  • lv_tabview:从右到左显示选项卡
  • lv_checkbox:在右侧显示复选框
  • lv_buttonmatrix:从右到左显示按钮
  • lv_list:在右侧显示图标
  • lv_dropdown:右对齐选项
  • lv_tablelv_buttonmatrixlv_keyboardlv_tabviewlv_dropdownlv_roller中的文本经过“BiDi处理”以正确显示
阿拉伯语和波斯语支持

显示阿拉伯语和波斯语字符有一些特殊规则:字符的形式取决于其在文本中的位置。同一字母在孤立、起始、中间或结束位置需要使用不同的形式。除此之外,还需要考虑一些结合规则。
如果启用了LV_USE_ARABIC_PERSIAN_CHARS,LVGL支持这些规则。
但是,有一些限制:

  • 仅支持显示文本(例如在标签上),不支持文本输入(例如文本区域)。
  • 静态文本(即常量)不处理。例如,通过lv_label_set_text()设置的文本将进行“阿拉伯处理”,但lv_label_set_text_static()不会。
  • 文本获取函数(例如lv_label_get_text())将返回处理后的文本。
亚像素渲染

亚像素渲染通过在红色、绿色和蓝色通道上渲染抗锯齿边缘而不是在像素级别上,使水平分辨率提高三倍。利用每个像素的物理颜色通道的位置,从而提高字母抗锯齿的质量。了解更多这里
对于亚像素渲染,字体需要以特殊设置生成:

  • 在在线转换器中勾选Subpixel
  • 在命令行工具中使用--lcd标志。请注意,生成的字体需要大约三倍的内存。
    亚像素渲染仅在像素的颜色通道具有水平布局时有效。即R、G、B通道彼此相邻,而不是彼此上下排列。颜色通道的顺序也需要与库设置匹配。默认情况下,LVGL假定为RGB顺序,但可以通过在lv_conf.h中设置LV_SUBPX_BGR1进行交换。
压缩字体

字体的位图可以通过以下方式压缩:

  • 在在线转换器中勾选Compressed复选框
  • 不传递--no-compress标志给离线转换器(默认应用压缩)
    对于较大的字体和更高的bpp,压缩效果更好。然而,渲染压缩字体的速度大约慢30%。因此,建议仅压缩用户界面中最大的字体,因为
  • 它们需要最多的内存
  • 它们可以更好地压缩
  • 它们可能比中等大小的字体使用频率更低,因此性能成本较小。
字符间距调整

字体可以提供字符间距信息以调整特定字符之间的间距。

  • 在线转换器生成字符间距表。
  • 除非指定了--no-kerning,否则离线转换器生成字符间距表。
  • FreeType集成目前不支持字符间距调整。
  • Tiny TTF字体引擎支持GPOS和Kern表。
    要在运行时配置字符间距调整,请使用lv_font_set_kerning()

添加新字体

有几种方法可以将新字体添加到项目中:

  1. 最简单的方法是使用在线字体转换器。只需设置参数,点击Convert按钮,将字体复制到项目中并使用它。请务必仔细阅读该网站提供的步骤,否则在转换时会出错。
  2. 使用离线字体转换器。(需要安装Node.js)
  3. 如果你想创建类似于内置字体(Montserrat字体和符号)的东西,但不同的大小和/或范围,你可以使用lvgl/scripts/built_in_font文件夹中的built_in_font_gen.py脚本。(这需要安装Python和lv_font_conv
    要在文件中声明字体,使用LV_FONT_DECLARE(my_font_name)
    要使字体全局可用(如内置字体),将它们添加到lv_conf.h中的LV_FONT_CUSTOM_DECLARE

添加新符号

内置符号是由FontAwesome字体创建的。

  1. https://fontawesome.com上搜索一个符号。例如USB符号。复制其Unicode ID,在这种情况下为0xf287
  2. 打开在线字体转换器。添加FontAwesome.woff
  3. 设置参数,如名称、大小、BPP。你将使用此名称在代码中声明和使用字体。
  4. 将符号的Unicode ID添加到范围字段。例如,对于USB符号为0xf287。可以用,枚举更多符号。
  5. 转换字体并将生成的源代码复制到项目中。确保编译字体的.c文件。
  6. 使用extern lv_font_t my_font_name;声明字体或直接使用LV_FONT_DECLARE(my_font_name)
    使用符号
  7. 这个网站上将Unicode值转换为UTF8。例如对于0xf287Hex UTF-8 bytesEF 8A 87
  8. 从UTF8值创建一个define字符串:#define MY_USB_SYMBOL "\xEF\x8A\x87"
  9. 创建一个标签并设置文本。例如,使用lv_label_set_text(label, MY_USB_SYMBOL)
  • 注意:
    lv_label_set_text(label, MY_USB_SYMBOL)在style.text.font属性中定义的字体中搜索此符号。要使用该符号,你可能需要更改它。例如style.text.font = my_font_name

运行时加载字体

lv_binfont_create()可用于从文件加载字体。字体需要具有特殊的二进制格式。(不是TTF或WOFF)。使用带有--format bin选项的lv_font_conv生成LVGL兼容的字体文件。

lv_font_t *my_font = lv_binfont_create("X:/path/to/my_font.bin");
if(my_font == NULL) return;
/*使用字体*/
/*如果不再需要字体,释放字体*/
lv_binfont_destroy(my_font);

从内存缓冲区加载字体

lv_binfont_create_from_buffer()可用于从内存缓冲区加载字体。此功能可能在从LVGL不支持的外部文件系统加载字体时有用。字体需要与从文件加载时的格式相同。

  • 注意:
    要从缓冲区加载字体,需要启用LVGL的文件系统并添加MEMFS驱动程序。
    示例:
lv_font_t *my_font;
uint8_t *buf;
uint32_t bufsize;
/*从外部文件系统将字体文件读取到缓冲区中*/
...
/*从缓冲区加载字体*/
my_font = lv_binfont_create_from_buffer((void *)buf, bufsize));
if(my_font == NULL) return;
/*使用字体*/
/*如果不再需要字体,释放字体*/
lv_binfont_destroy(my_font);

添加新的字体引擎

LVGL的字体接口设计得非常灵活,但即便如此,你仍可以添加自己的字体引擎来替代LVGL的内部引擎。例如,你可以使用FreeType实时渲染TTF字体中的字形,或者使用外部闪存存储字体的位图并在库需要时读取它们。
一个可用的FreeType示例可以在lv_freetype仓库中找到。
要做到这一点,需要创建一个自定义的lv_font_t变量:

/*描述字体的属性*/
lv_font_t my_font;
my_font.get_glyph_dsc = my_get_glyph_dsc_cb;        /*设置一个回调以获取字形信息*/
my_font.get_glyph_bitmap = my_get_glyph_bitmap_cb;  /*设置一个回调以获取字形位图*/
my_font.line_height = height;                       /*任何文本适合的实际行高*/
my_font.base_line = base_line;                      /*从行高顶部测量的基线*/
my_font.dsc = something_required;                   /*在这里存储任何特定于实现的数据*/
my_font.user_data = user_data;                      /*可选的额外用户数据*/
...
/* 获取 `unicode_letter` 在 `font` 字体中的字形信息。
 * 将结果存储在 `dsc_out` 中。
 * 下一个字母 (`unicode_letter_next`) 可能用于计算该字形所需的宽度(字距调整)
 */
bool my_get_glyph_dsc_cb(const lv_font_t * font, lv_font_glyph_dsc_t * dsc_out, uint32_t unicode_letter, uint32_t unicode_letter_next)
{
    /*在这里编写代码*/
    /* 存储结果。
     * 例如...
     */
    dsc_out->adv_w = 12;        /*字形所需的水平空间(以像素为单位)*/
    dsc_out->box_h = 8;         /*位图的高度(以像素为单位)*/
    dsc_out->box_w = 6;         /*位图的宽度(以像素为单位)*/
    dsc_out->ofs_x = 0;         /*位图的X偏移(以像素为单位)*/
    dsc_out->ofs_y = 3;         /*从基线测量的位图的Y偏移*/
    dsc_out->format= LV_FONT_GLYPH_FORMAT_A2;
    return true;                /*true:找到字形;false:未找到字形*/
}
/* 从 `font` 中获取 `unicode_letter` 的位图。 */
const uint8_t * my_get_glyph_bitmap_cb(const lv_font_t * font, uint32_t unicode_letter)
{
    /* 在这里编写代码 */
    /* 位图应该是一个连续的位流,
     * 其中每个像素由 `bpp` 位表示 */
    return bitmap;    /*如果未找到则返回NULL*/
}

使用字体回退

你可以在lv_font_t中指定fallback来为字体提供回退。当字体无法找到某个字母的字形时,它会尝试让fallback字体处理。
fallback可以链接,所以它会尝试解决,直到没有设置fallback

/* Roboto字体不支持CJK字形 */
lv_font_t *roboto = my_font_load_function();
/* Droid Sans Fallback有更多的字形,但其字体外观不如Roboto */
lv_font_t *droid_sans_fallback = my_font_load_function();
/* 因此,我们可以为支持的字符显示Roboto,同时具有更广泛的字符集支持 */
roboto->fallback = droid_sans_fallback;

API

lv_font.h
lv_types.h
lv_font_fmt_txt.h
PreviousNext

Images(图像)

图片可以是文件,也可以是存储位图本身及一些元数据的变量。

存储图片

你可以将图片存储在两个地方:

  • 内部内存(RAM或ROM)中的变量
  • 文件
变量

内部存储的图片主要由一个lv_image_dsc_t结构体组成,该结构体包含以下字段:

  • header:
    • cf: 颜色格式。见下面
    • w: 像素宽度(<= 2048)
    • h: 像素高度(<= 2048)
    • always zero: 需要始终为零的3位
    • reserved: 保留以供将来使用
  • data: 指向存储图片本身的数组的指针
  • data_size: data的长度,以字节为单位
    这些通常作为C文件存储在项目中。它们像任何其他常量数据一样链接到生成的可执行文件中。
文件

要处理文件,你需要向LVGL添加存储驱动。简而言之,驱动是注册到LVGL中的一组函数(openreadclose等),用于进行文件操作。你可以添加一个标准文件系统(如SD卡上的FAT32)接口,或创建一个简单的文件系统以从SPI闪存中读取数据。在每种情况下,驱动都只是一个用于从内存中读取和/或写入数据的抽象。请参阅文件系统部分了解更多信息。
作为文件存储的图片不会链接到生成的可执行文件中,必须在绘制之前读入RAM。因此,它们不像编译时链接的图片那样资源友好。然而,它们更容易替换而无需重建主程序。

颜色格式

支持各种内置颜色格式:

添加和使用图片

你可以通过两种方式向LVGL添加图片:

  • 使用在线转换器
  • 手动创建图片
在线转换器

在线图像转换器可在此处找到:https://lvgl.io/tools/imageconverter
通过在线转换器向LVGL添加图片非常简单。

  1. 首先需要选择一个BMPPNGJPG图片。
  2. 给图片取一个将在LVGL中使用的名称。
  3. 选择颜色格式
  4. 选择你想要的图片类型。选择二进制文件将生成一个.bin文件,必须单独存储并使用文件支持进行读取。选择变量将生成一个标准C文件,可以链接到你的项目中。
  5. 点击Convert按钮。转换完成后,你的浏览器会自动下载生成的文件。
    在生成的C数组(变量)中,包含所有颜色深度(1、8、16或32)的位图,但只有与lv_conf.h中的LV_COLOR_DEPTH匹配的颜色深度会实际链接到生成的可执行文件中。
    对于二进制文件,你需要指定所需的颜色格式:
  • 8位颜色深度的RGB332
  • 16位颜色深度的RGB565
  • 16位颜色深度的RGB565交换(两个字节交换)
  • 32位颜色深度的RGB888
手动创建图片

如果你在运行时生成图片,你可以制作一个图像变量来使用LVGL显示它。例如:

uint8_t my_img_data[] = {0x00, 0x01, 0x02, ...};
static lv_image_dsc_t my_img_dsc = {
    .header.always_zero = 0,
    .header.w = 80,
    .header.h = 60,
    .data_size = 80 * 60 * LV_COLOR_DEPTH / 8,
    .header.cf = LV_COLOR_FORMAT_NATIVE,          /*设置颜色格式*/
    .data = my_img_data,
};

另一种(可能更简单)的方法是在运行时创建和显示图片是使用Canvas对象。

使用图片

在LVGL中使用图片的最简单方法是使用lv_image.h对象显示它:

lv_obj_t * icon = lv_image_create(lv_screen_active(), NULL);
/*来自变量*/
lv_image_set_src(icon, &my_icon_dsc);
/*来自文件*/
lv_image_set_src(icon, "S:my_icon.bin");

如果图片是通过在线转换器转换的,你应该使用LV_IMAGE_DECLARE(my_icon_dsc)在你想使用它的文件中声明图片。

图片解码器

正如你在颜色格式部分中看到的,LVGL支持几种内置图像格式。在许多情况下,这些格式将满足你的需求。然而,LVGL并不直接支持诸如PNG或JPG等通用图像格式。
要处理非内置图像格式,你需要使用外部库并通过图片解码器接口将它们附加到LVGL。
图片解码器包含4个回调:

  • info 获取关于图片的一些基本信息(宽度、高度和颜色格式)。
  • open 打开图片:存储已解码的图片集,如果图片可以逐行读取,则设置为NULL
  • get_area 如果open没有完全打开图片,此函数应返回解码数据的一部分图片。
  • close 关闭已打开的图片,释放分配的资源。
    你可以添加任意数量的图片解码器。当需要绘制图片时,库将尝试所有已注册的图片解码器,直到找到一个可以打开图片的解码器,即知道该格式的解码器。
    内置解码器理解以下格式:
  • LV_COLOR_FORMAT_I1
  • LV_COLOR_FORMAT_I2
  • LV_COLOR_FORMAT_I4
  • LV_COLOR_FORMAT_I8
  • LV_COLOR_FORMAT_RGB888
  • LV_COLOR_FORMAT_XRGB8888
  • LV_COLOR_FORMAT_ARGB8888
  • LV_COLOR_FORMAT_RGB565
  • LV_COLOR_FORMAT_RGB565A8
自定义图像格式

创建自定义图像的最简单方法是使用在线图像转换器并选择RawRaw with alpha格式。它将逐字节读取你上传的二进制文件,并将其写为图像“位图”。然后,你需要附加一个图片解码器来解析该位图并生成实际的、可渲染的位图。
header.cf将分别是LV_COLOR_FORMAT_RAWLV_COLOR_FORMAT_RAW_ALPHA。你应根据需要选择正确的格式:一个完全不透明的图像,使用alpha通道。
解码后,raw格式被库认为是真彩色。换句话说,图片解码器必须根据颜色格式部分描述的格式,将Raw图像解码为真彩色

注册图片解码器

下面是一个使LVGL支持PNG图片的示例。
首先,你需要创建一个新的图片解码器并设置一些函数来打开/关闭PNG文件。它看起来像这样:

/*创建一个新的解码器并注册函数*/
lv_image_decoder_t * dec = lv_image_decoder_create();
lv_image_decoder_set_info_cb(dec, decoder_info);
lv_image_decoder_set_open_cb(dec, decoder_open);
lv_image_decoder_set_get_area_cb(dec, decoder_get_area);
lv_image_decoder_set_close_cb(dec, decoder_close);
/**
 * 获取PNG图片的信息
 * @param decoder   指向解码器的指针
 * @param src       可以是文件名或指向C数组的指针
 * @param header    图片信息存储在header参数中
 * @return          LV_RESULT_OK:无错误;LV_RESULT_INVALID:无法获取信息
 */
static lv_result_t decoder_info(lv_image_decoder_t * decoder, const void * src, lv_image_header_t * header)
{
  /*检查解码器是否知道`src`的类型*/
  if(is_png(src) == false) return LV_RESULT_INVALID;
  /*读取PNG头并找到`width`和`height`*/
  ...
  header->cf = LV_COLOR_FORMAT_ARGB8888;
  header->w = width;
  header->h = height;
}
/**
 * 打开PNG图片并将其解码到dsc.decoded
 * @param decoder   指向解码器的指针
 * @param dsc       图片描述符
 * @return          LV_RESULT_OK:无错误;LV_RESULT_INVALID:无法打开图片
 */
static lv_result_t decoder_open(lv_image_decoder_t * decoder, lv_image_decoder_dsc_t * dsc)
{
  (void) decoder; /*未使用*/
  /*检查解码器是否知道`src`的类型*/
  if(is_png(dsc->src) == false) return LV_RESULT_INVALID;
  /*解码并存储图片。如果`dsc->decoded`为`NULL`,则将调用`decoder_get_area`函数逐行获取图像数据*/
  dsc->decoded = my_png_decoder(dsc->src);
  /*如果解码后的图像格式与原始格式不同,则更改颜色格式。对于PNG,它通常解码为ARGB8888格式*/
  dsc->decoded.header.cf = LV_COLOR_FORMAT_...
  /*如果需要,调用二进制图片解码器函数。如果`my_png_decoder`以真彩色格式打开图片,则不需要。*/
  lv_result_t res = lv_bin_decoder_open(decoder, dsc);
  return res;
}
/**
 * 解码图片的一个区域
 * @param decoder      指向解码器的指针
 * @param dsc          图片解码器描述符
 * @param full_area    输入参数。要在多次调用后解码的完整区域
 * @param decoded_area 输入+输出参数。在第一次调用时将值设置为`LV_COORD_MIN`,并重置解码。
 *                     解码区域在每次调用后存储在此处。
 * @return             LV_RESULT_OK:正常;LV_RESULT_INVALID:失败或没有剩余的解码内容
 */
static lv_result_t decoder_get_area(lv_image_decoder_t * decoder, lv_image_decoder_dsc_t * dsc,
                                 const lv_area_t * full_area, lv_area_t * decoded_area)
{
  /**
  * 如果`dsc->decoded`总是在`decoder_open`中设置,则不需要实现`decoder_get_area`。
  * 如果`dsc->decoded`仅在某些情况下设置或从未设置,则使用`decoder_get_area`
  * 通过多次调用来逐步解码图像,直到它返回`LV_RESULT_INVALID`。
  * 在下面的示例中,图像按行解码,但解码区域可以有任何形状和大小,
  * 取决于图像解码器的要求和能力。
  */
  my_decoder_data_t * my_decoder_data = dsc->user_data;
  /* 如果`decoded_area`的字段设置为`LV_COORD_MIN`,则重置解码 */
  if(decoded_area->y1 == LV_COORD_MIN) {
    decoded_area->x1 = full_area->x1;
    decoded_area->x2 = full_area->x2;
    decoded_area->y1 = full_area->y1;
    decoded_area->y2 = decoded_area->y1; /* 逐行解码,从第一行开始 */
    /* 创建大小为一行的绘制缓冲区 */
    bool reshape_success = NULL != lv_draw_buf_reshape(my_decoder_data->partial,
                                                       dsc->decoded.header.cf,
                                                       lv_area_get_width(full_area),
                                                       1,
                                                       LV_STRIDE_AUTO);
    if(!reshape_success) {
      lv_draw_buf_destroy(my_decoder_data->partial);
      my_decoder_data->partial = lv_draw_buf_create(lv_area_get_width(full_area),
                                                    1,
                                                    dsc->decoded.header.cf,
                                                    LV_STRIDE_AUTO);
      my_png_decode_line_reset(full_area);
    }
  }
  /* 否则解码已经在进行中。解码下一行 */
  else {
    /* 所有行都已解码。通过返回`LV_RESULT_INVALID`表示完成 */
    if (decoded_area->y1 >= full_area->y2) return LV_RESULT_INVALID;
    decoded_area->y1++;
    decoded_area->y2++;
  }
  my_png_decode_line(my_decoder_data->partial);
  return LV_RESULT_OK;
}
/**
 * 关闭PNG图片并释放数据
 * @param decoder   指向解码器的指针
 * @param dsc       图片解码器描述符
 * @return          LV_RESULT_OK:无错误;LV_RESULT_INVALID:无法打开图片
 */
static void decoder_close(lv_image_decoder_t * decoder,
 lv_image_decoder_dsc_t * dsc)
{
  /*释放所有分配的数据*/
  my_png_cleanup();
  my_decoder_data_t * my_decoder_data = dsc->user_data;
  lv_draw_buf_destroy(my_decoder_data->partial);
  /*如果使用了内置打开/获取区域功能,则调用内置关闭功能*/
  lv_bin_decoder_close(decoder, dsc);
}

总结:

  • decoder_info中,你应收集一些关于图片的基本信息并存储在header中。
  • decoder_open中,你应尝试打开由dsc->src指向的图片源。其类型已在dsc->src_type == LV_IMG_SRC_FILE/VARIABLE中。若该格式/类型不受解码器支持,则返回LV_RESULT_INVALID。如果可以打开图片,则应在dsc->decoded中设置一个指向解码后图片的指针。如果知道格式但不想解码整个图片(例如没有足够内存),则设置dsc->decoded = NULL并使用decoder_get_area逐行获取图片区域像素。
  • decoder_close中,你应释放所有分配的资源。
  • decoder_get_area是可选的。在这种情况下,你应在decoder_open函数中解码整个图片,并在dsc->decoded中存储图片数据。解码整个图片需要额外的内存和计算开销。
手动使用图片解码器

LVGL会自动使用注册的图片解码器,如果你尝试绘制一个原始图片(即使用lv_image对象),但你也可以手动使用它们。创建一个lv_image_decoder_dsc_t变量来描述解码会话并调用lv_image_decoder_open()
color参数仅用于LV_COLOR_FORMAT_A1/2/4/8图像,以指示图像的颜色。

lv_result_t res;
lv_image_decoder_dsc_t dsc;
lv_image_decoder_args_t args = { 0 }; /*通过args定制解码器行为*/
res = lv_image_decoder_open(&dsc, &my_img_dsc, &args);
if(res == LV_RESULT_OK) {
  /*对`dsc->decoded`做些操作。你可以通过`lv_draw_buf_dup(dsc.decoded)`复制解码后的图片*/
  lv_image_decoder_close(&dsc);
}
图片后处理

考虑到某些硬件对图像格式有特殊要求,例如预乘alpha和步幅对齐,大多数图片解码器(如PNG解码器)可能不会直接输出满足硬件要求的图像数据。
为此,LVGL提供了解决方案。首先,在lv_image_decoder_open后调用一个自定义后处理函数,以调整图像缓存中的数据,然后在cache_entry->process_state中标记处理状态(以避免重复后处理)。
请参阅下面的详细代码:

  • 步幅对齐和预乘后处理示例:
/* 定义后处理状态 */
typedef enum {
  IMAGE_PROCESS_STATE_NONE = 0,
  IMAGE_PROCESS_STATE_STRIDE_ALIGNED = 1 << 0,
  IMAGE_PROCESS_STATE_PREMULTIPLIED_ALPHA = 1 << 1,
} image_process_state_t;
lv_result_t my_image_post_process(lv_image_decoder_dsc_t * dsc)
{
  lv_color_format_t color_format = dsc->header.cf;
  lv_result_t res = LV_RESULT_OK;
  if(color_format == LV_COLOR_FORMAT_ARGB8888) {
    lv_cache_lock();
    lv_cache_entry_t * entry = dsc->cache_entry;
    if(!(entry->process_state & IMAGE_PROCESS_STATE_PREMULTIPLIED_ALPHA)) {
      lv_draw_buf_premultiply(dsc->decoded);
      LV_LOG_USER("预乘alpha OK");
      entry->process_state |= IMAGE_PROCESS_STATE_PREMULTIPLIED_ALPHA;
    }
    if(!(entry->process_state & IMAGE_PROCESS_STATE_STRIDE_ALIGNED)) {
       uint32_t stride_expect = lv_draw_buf_width_to_stride(decoded->header.w, decoded->header.cf);
       if(decoded->header.stride != stride_expect) {
           LV_LOG_WARN("步幅不匹配");
           lv_draw_buf_t * aligned = lv_draw_buf_adjust_stride(decoded, stride_expect);
           if(aligned == NULL) {
               LV_LOG_ERROR("步幅调整没有内存。");
               return NULL;
           }
           decoded = aligned;
       }
       entry->process_state |= IMAGE_PROCESS_STATE_STRIDE_ALIGNED;
    }
alloc_failed:
    lv_cache_unlock();
  }
  return res;
}
  • GPU绘制单元示例:
void gpu_draw_image(lv_draw_unit_t * draw_unit, const lv_draw_image_dsc_t * draw_dsc, const lv_area_t * coords)
{
  ...
  lv_image_decoder_dsc_t decoder_dsc;
  lv_result_t res = lv_image_decoder_open(&decoder_dsc, draw_dsc->src, NULL);
  if(res != LV_RESULT_OK) {
    LV_LOG_ERROR("打开图片失败");
    return;
  }
  res = my_image_post_process(&decoder_dsc);
  if(res != LV_RESULT_OK) {
    LV_LOG_ERROR("图片后处理失败");
    return;
  }
  ...
}

图片缓存

有时打开图片需要很长时间。连续解码PNG/JPEG图片或从慢速外部内存加载图片将效率低下,影响用户体验。
因此,LVGL会缓存图片数据。缓存意味着一些图片将保持打开状态,因此LVGL可以快速访问它们,而不是需要再次解码。
当然,缓存图片会消耗更多的RAM以存储解码后的图片。LVGL尽可能优化此过程(见下文),但你仍需要评估这对你的平台是否有利。如果你有一个深度嵌入的目标,从相对快速的存储介质解码小图片,那么图片缓存可能不值得。

缓存大小

缓存的大小(以字节为单位)可以通过lv_conf.h中的LV_CACHE_DEF_SIZE定义。默认值为0,因此没有图片被缓存。
可以通过lv_cache_set_max_size(size_t size)在运行时更改缓存大小,并使用lv_cache_get_max_size()获取。

图片的价值

当你使用的图片超过可用缓存大小时,LVGL无法缓存所有图片。相反,库将关闭一个缓存的图片以释放空间。
为了决定关闭哪个图片,LVGL使用先前测量的打开图片所需的时间。缓存条目中包含较慢打开的图片被认为更有价值,并尽可能长时间保持在缓存中。
如果你想覆盖LVGL的测量,可以手动设置缓存条目中的weight值(cache_entry->weight = time_ms),以赋予更高或更低的值。(如果不更改,则由LVGL控制)。
每个缓存条目都有一个*“生命值”。每次通过缓存打开一个图片时,所有条目的生命值将按其权重值增加,使它们变得更老。当一个缓存的图片被使用时,其使用计数*值将增加,使其更加“活跃”。
如果缓存中没有更多空间,则 使用计数等于0且生命值最低的条目将被删除。

内存使用情况

请注意,缓存的图片可能会持续占用内存。例如,如果缓存了三张PNG图片,它们将在打开时消耗内存。
因此,用户有责任确保同时缓存最大的图片时有足够的RAM。

清理缓存

假设你已将PNG图片加载到一个lv_image_dsc_tmy_png变量中,并在lv_image对象中使用它。如果图片已被缓存,然后更改了底层PNG文件,你需要通知LVGL再次缓存图片。否则,没有简单的方法检测底层文件已更改,LVGL将仍然从缓存中绘制旧图片。
为此,请使用lv_cache_invalidate(lv_cache_find(&my_png, LV_CACHE_SRC_TYPE_PTR, 0, 0));

自定义缓存算法

如果你想实现自己的缓存算法,可以参考以下代码替换LVGL内置缓存管理器:

static lv_cache_entry_t * my_cache_add_cb(size_t size)
{
  ...
}
static lv_cache_entry_t * my_cache_find_cb(const void * src, lv_cache_src_type_t
 src_type, uint32_t param1, uint32_t param2)
{
  ...
}
static void my_cache_invalidate_cb(lv_cache_entry_t * entry)
{
  ...
}
static const void * my_cache_get_data_cb(lv_cache_entry_t * entry)
{
  ...
}
static void my_cache_release_cb(lv_cache_entry_t * entry)
{
  ...
}
static void my_cache_set_max_size_cb(size_t new_size)
{
  ...
}
static void my_cache_empty_cb(void)
{
  ...
}
void my_cache_init(void)
{
 /*初始化新缓存管理器*/
 lv_cache_manager_t my_manager;
 my_manager.add_cb = my_cache_add_cb;
 my_manager.find_cb = my_cache_find_cb;
 my_manager.invalidate_cb = my_cache_invalidate_cb;
 my_manager.get_data_cb = my_cache_get_data_cb;
 my_manager.release_cb = my_cache_release_cb;
 my_manager.set_max_size_cb = my_cache_set_max_size_cb;
 my_manager.empty_cb = my_cache_empty_cb;
 /*用新缓存管理器替换现有缓存管理器*/
 lv_cache_lock();
 lv_cache_set_manager(&my_manager);
 lv_cache_unlock();
}

API

lv_draw_image.h
lv_image.h
lv_types.h
lv_draw_buf.h
lv_image_dsc.h
lv_api_map_v8.h
lv_image_header_cache.h
lv_image_cache.h
lv_image_decoder.h
lv_api_map_v9_0.h

File system(文件系统)

LVGL 具有一个“文件系统”抽象模块,使您可以连接任何类型的文件系统。文件系统通过指定的驱动器字母进行识别。例如,如果 SD 卡与字母 S 关联,可以使用 "S:path/to/file.txt" 访问文件。

可直接使用的驱动程序

LVGL 包含为 POSIX、标准 C、Windows 和 FATFS API 准备的驱动程序。了解更多信息请点击 这里

添加驱动程序

注册驱动程序

要添加驱动程序,需要初始化一个 lv_fs_drv_t,如下所示。lv_fs_drv_t 需要是静态、全局或动态分配的变量,不能是局部变量。

static lv_fs_drv_t drv;                   /*需要是静态或全局变量*/
lv_fs_drv_init(&drv);                     /*基本初始化*/
drv.letter = 'S';                         /*用于识别驱动器的大写字母*/
drv.cache_size = my_cache_size;           /*缓存大小(字节),设置为0表示不缓存*/
drv.ready_cb = my_ready_cb;               /*用于判断驱动器是否准备就绪的回调函数*/
drv.open_cb = my_open_cb;                 /*用于打开文件的回调函数*/
drv.close_cb = my_close_cb;               /*用于关闭文件的回调函数*/
drv.read_cb = my_read_cb;                 /*用于读取文件的回调函数*/
drv.write_cb = my_write_cb;               /*用于写入文件的回调函数*/
drv.seek_cb = my_seek_cb;                 /*用于在文件中移动光标的回调函数*/
drv.tell_cb = my_tell_cb;                 /*用于获取光标位置的回调函数*/
drv.dir_open_cb = my_dir_open_cb;         /*用于打开目录以读取其内容的回调函数*/
drv.dir_read_cb = my_dir_read_cb;         /*用于读取目录内容的回调函数*/
drv.dir_close_cb = my_dir_close_cb;       /*用于关闭目录的回调函数*/
drv.user_data = my_user_data;             /*需要的任何自定义数据*/
lv_fs_drv_register(&drv);                 /*最后注册驱动*/

任何回调函数都可以是 NULL,以表示不支持该操作。

实现回调函数
打开回调

open_cb 的原型如下:

void * (*open_cb)(lv_fs_drv_t * drv, const char * path, lv_fs_mode_t mode);

path 是驱动器字母之后的路径(例如 “S:path/to/file.txt” -> “path/to/file.txt”)。mode 可以是 LV_FS_MODE_WRLV_FS_MODE_RD 以打开写入或读取模式。
返回值是一个指向文件对象的指针,该对象描述了打开的文件,如果有任何问题(例如找不到文件),则返回 NULL。返回的文件对象将传递给其他文件系统相关的回调函数(见下文)。

其他回调函数

其他回调函数类似。例如,write_cb 看起来如下:

lv_fs_res_t (*write_cb)(lv_fs_drv_t * drv, void * file_p, const void * buf, uint32_t btw, uint32_t * bw);

对于 file_p,LVGL 传递 open_cb 的返回值,buf 是要写入的数据,btw 是要写入的字节数,bw 是实际写入的字节数。
有关这些回调函数的模板,请参见 lv_fs_template.c

使用示例

下面的示例显示如何从文件中读取:

lv_fs_file_t f;
lv_fs_res_t res;
res = lv_fs_open(&f, "S:folder/file.txt", LV_FS_MODE_RD);
if(res != LV_FS_RES_OK) my_error_handling();
uint32_t read_num;
uint8_t buf[8];
res = lv_fs_read(&f, buf, 8, &read_num);
if(res != LV_FS_RES_OK || read_num != 8) my_error_handling();
lv_fs_close(&f);

lv_fs_open() 中的模式可以是 LV_FS_MODE_WR 以仅打开写入模式,或 LV_FS_MODE_RD | LV_FS_MODE_WR 以同时打开读取和写入模式。
此示例显示如何读取目录的内容。由驱动程序决定如何在结果中标记目录,但可以将 '/' 插入每个目录名称前作为一种好的做法。

lv_fs_dir_t dir;
lv_fs_res_t res;
res = lv_fs_dir_open(&dir, "S:/folder");
if(res != LV_FS_RES_OK) my_error_handling();
char fn[256];
while(1) {
    res = lv_fs_dir_read(&dir, fn, sizeof(fn));
    if(res != LV_FS_RES_OK) {
        my_error_handling();
        break;
    }
    /*fn为空,如果没有更多文件可读取*/
    if(strlen(fn) == 0) {
        break;
    }
    printf("%s\n", fn);
}
lv_fs_dir_close(&dir);

使用驱动器加载图像

Image 对象也可以从文件中打开(除了存储在已编译程序中的变量)。
要在图像小部件中使用文件,需要以下回调函数:

  • open
  • close
  • read
  • seek
  • tell

可选的文件缓冲/缓存

如果相应的 LV_FS_*_CACHE_SIZE 配置选项设置为大于零的值,文件将缓冲它们的读取。每个打开的文件将缓冲多达该数量的字节,以减少文件系统驱动程序调用的次数。
一般来说,文件缓冲可以针对不同的访问模式进行优化。这里实现的是针对大文件块读取的优化,这是图像解码器所做的。它有可能比 lv_fs_read 调用更少次地调用驱动程序的 read。在最佳情况下,当缓存大小大于等于文件大小时,read 只会被调用一次。这种策略适用于线性读取大文件,但对跨文件的短随机读取帮助较小,因为数据将在下一次 seek 和 read 之后被丢弃。缓存应该足够大或在这种情况下禁用。如果文件内容预计会被外部因素更改,例如特殊的操作系统文件,则应禁用缓存。
以下是启用缓存时的 FS 函数的行为。

lv_fs_read(启用缓存时的行为)
lv_fs_write(启用缓存时的行为)

缓存中与写入内容一致的部分将被更新以反映写入内容。

lv_fs_seek(启用缓存时的行为)

除非 whenceLV_FS_SEEK_END,否则不会实际调用驱动程序的 seek,在这种情况下,将调用 seektell 以确定文件的结尾位置。

lv_fs_tell(启用缓存时的行为)

不会实际调用驱动程序的 tell

API

lv_fs.h

Animations(动画)

动画可以自动在一个变量的起始值和结束值之间变化,通过定期调用一个“动画函数”并传递相应的值参数来实现。
动画函数的原型如下:

void func(void * var, lv_anim_var_t value);

这个原型与 LVGL 中大多数属性设置函数兼容。例如 lv_obj_set_x(obj, value) 或 lv_obj_set_width(obj, value)。

创建动画

要创建动画,需要初始化并使用 lv_anim_set_...() 函数配置一个 lv_anim_t 变量。

/* 初始化动画 */
lv_anim_t a;
lv_anim_init(&a);
/* 必须设置 */
/* 设置“动画函数” */
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t) lv_obj_set_x);
/* 设置动画目标 */
lv_anim_set_var(&a, obj);
/* 动画时长 [毫秒] */
lv_anim_set_duration(&a, duration);
/* 设置起始和结束值。例如 0, 150 */
lv_anim_set_values(&a, start, end);
/* 可选设置 */
/* 动画开始前等待的时间 [毫秒] */
lv_anim_set_delay(&a, delay);
/* 设置路径(曲线)。默认是线性的 */
lv_anim_set_path_cb(&a, lv_anim_path_ease_in);
/* 设置动画完成时的回调 */
lv_anim_set_completed_cb(&a, completed_cb);
/* 设置动画删除(空闲)时的回调 */
lv_anim_set_deleted_cb(&a, deleted_cb);
/* 设置动画开始(延迟后)的回调 */
lv_anim_set_start_cb(&a, start_cb);
/* 动画结束后反向播放的时间。默认是 0(禁用)[毫秒] */
lv_anim_set_playback_duration(&a, time);
/* 反向播放前的延迟。默认是 0(禁用)[毫秒] */
lv_anim_set_playback_delay(&a, delay);
/* 重复次数。默认是 1。LV_ANIM_REPEAT_INFINITE 表示无限重复 */
lv_anim_set_repeat_count(&a, cnt);
/* 重复前的延迟。默认是 0(禁用)[毫秒] */
lv_anim_set_repeat_delay(&a, delay);
/* true(默认):立即应用起始值,false:在动画真正开始时(延迟后)应用起始值 */
lv_anim_set_early_apply(&a, true/false);
/* 开始动画 */
lv_anim_start(&a);                             /* 开始动画 */

可以对同一个变量应用多个不同的动画。例如,可以使用 lv_obj_set_x()lv_obj_set_y() 分别动画化 x 和 y 坐标。然而,对于给定的变量和函数对,只能存在一个动画,lv_anim_start() 将移除任何现有的动画。

动画路径

可以控制动画的路径。最简单的情况是线性的,这意味着在 startend 之间的当前值以固定步长变化。路径 是一个函数,它根据动画的当前状态计算下一个值。目前,有以下内置路径函数:

速度与时间

默认情况下,直接设置动画时间。但在某些情况下,设置动画速度更为实用。
lv_anim_speed_to_time(speed, start, end) 函数计算以给定速度从起始值达到结束值所需的时间(毫秒)。速度以 unit/sec 为单位解释。例如,lv_anim_speed_to_time(20, 0, 100) 将得出 5000 毫秒。例如,在 lv_obj_set_x() 的情况下,unit 是像素,因此 20 表示 20 px/sec 的速度。

删除动画

可以使用 lv_anim_delete(var, func) 删除动画,只需提供被动画化的变量及其动画函数。

时间轴

时间轴是多个动画的集合,使创建复杂的复合动画变得容易。
首先,创建一个动画元素,但不要调用 lv_anim_start()
其次,通过调用 lv_anim_timeline_create() 创建一个动画时间轴对象。
第三,通过调用 lv_anim_timeline_add(at, start_time, &a) 将动画元素添加到动画时间轴。start_time 是时间轴上动画的开始时间。请注意,start_time 将覆盖 delay 的值。
最后,调用 lv_anim_timeline_start(at) 启动动画时间轴。
它支持整个动画组的正向和反向播放,使用 lv_anim_timeline_set_reverse(at, reverse)。请注意,如果希望从时间轴的末尾反向播放,需要在添加所有动画后并在开始播放之前调用 lv_anim_timeline_set_progress(at, LV_ANIM_TIMELINE_PROGRESS_MAX)。
调用 lv_anim_timeline_stop(at) 停止动画时间轴。
调用 lv_anim_timeline_set_progress(at, progress) 函数设置与时间轴进度对应的对象状态。
调用 lv_anim_timeline_get_playtime(at) 函数获取整个动画时间轴的总持续时间。
调用 lv_anim_timeline_get_reverse(at) 函数获取动画时间轴是否反向。
调用 lv_anim_timeline_delete(at) 函数删除动画时间轴。注意:如果需要在动画期间删除对象,请确保在删除对象之前删除动画时间轴。否则,程序可能会崩溃或行为异常。
../_images/anim-timeline.png

Timers(定时器)

LVGL 具有内置的定时器系统,可以注册一个函数以便定期调用。定时器在 lv_timer_handler() 中处理和调用,该函数需要每隔几毫秒调用一次。有关更多信息,请参阅Porting
定时器是非抢占式的,这意味着一个定时器不能中断另一个定时器。因此,可以在定时器中调用任何与 LVGL 相关的函数。

创建定时器

要创建新的定时器,请使用 lv_timer_create(timer_cb, period_ms, user_data)。它会创建一个 lv_timer_t * 变量,该变量可用于稍后修改定时器的参数。也可以使用 lv_timer_create_basic()。这允许您在不指定任何参数的情况下创建新的定时器。
定时器回调应具有 void (*lv_timer_cb_t)(lv_timer_t *) 原型。
例如:

void my_timer(lv_timer_t * timer)
{
  /* 使用 user_data */
  uint32_t * user_data = timer->user_data;
  printf("my_timer called with user data: %d\n", *user_data);
  /* 执行与 LVGL 相关的操作 */
  if(something_happened) {
    something_happened = false;
    lv_button_create(lv_screen_active(), NULL);
  }
}
...
static uint32_t user_data = 10;
lv_timer_t * timer = lv_timer_create(my_timer, 500, &user_data);

准备和重置

lv_timer_ready(timer) 使定时器在下一次调用 lv_timer_handler() 时运行。
lv_timer_reset(timer) 重置定时器的周期。定义的毫秒数过后,它将再次调用。

设置参数

您可以稍后修改一些定时器参数:

重复计数

您可以通过 lv_timer_set_repeat_count(timer, count) 设置定时器只重复给定次数。定时器将在调用定义次数后自动删除。将计数设置为 -1 表示无限重复。

启用和禁用

您可以使用 lv_timer_enable(en) 启用或禁用定时器。

暂停和恢复

lv_timer_pause(timer) 暂停指定的定时器。
lv_timer_resume(timer) 恢复指定的定时器。

测量空闲时间

您可以使用 lv_timer_get_idle() 获取 lv_timer_handler() 的空闲百分比时间。请注意,这不会测量整个系统的空闲时间,仅测量 lv_timer_handler() 的空闲时间。如果您使用操作系统并在定时器中调用 lv_timer_handler(),这可能会产生误导,因为它不会实际测量操作系统在空闲线程中花费的时间。

定时器处理程序恢复回调

lv_timer_handler 停止时,如果您想注意 lv_timer_handler 的唤醒时间,可以使用 lv_timer_handler_set_resume_cb(cb, user_data) 设置恢复回调。回调应该有一个 void (*lv_timer_handler_resume_cb_t)(void*) 原型。

异步调用

在某些情况下,您不能立即执行某个操作。例如,您不能删除一个对象,因为其他东西仍在使用它,或者您不想现在阻塞执行。在这些情况下,可以使用 lv_async_call(my_function, data_p) 在下一次调用 lv_timer_handler() 时调用 my_function。当函数被调用时,data_p 将被传递给该函数。请注意,仅保存数据指针,因此需要确保变量在函数调用时“活着”。它可以是 static、全局或动态分配的数据。如果要取消异步调用,请调用 lv_async_call_cancel(my_function, data_p),它将清除所有与 my_functiondata_p 匹配的异步调用。
例如:

void my_screen_clean_up(void * scr)
{
  /* 释放与 `scr` 相关的一些资源 */
  /* 最后删除屏幕 */
  lv_obj_delete(scr);
}
...
/* 对当前屏幕上的对象执行一些操作 */
/* 在下一次调用 `lv_timer_handler` 时删除屏幕,而不是现在删除。*/
lv_async_call(my_screen_clean_up, lv_screen_active());
/* 屏幕仍然有效,所以您可以执行其他操作 */

如果您只想删除一个对象,并且不需要在 my_screen_cleanup 中清理任何东西,可以直接使用 lv_obj_delete_async(),它将在下一次调用 lv_timer_handler() 时删除该对象。

API

lv_types.h
lv_timer.h
lv_api_map_v8.h

Profiler(性能分析器)

随着应用程序复杂性的增加,性能问题(如低 FPS 和频繁的缓存未命中导致的卡顿)可能会出现。LVGL 内部设置了一些性能测量的钩子,帮助开发人员分析和定位性能问题。

介绍

LVGL 有一个内置的追踪系统,用于跟踪和记录运行时重要事件(如渲染事件和用户输入事件)的时间戳。这些事件时间戳是性能分析的重要指标。
追踪系统有一个可配置的记录缓冲区,用于存储事件函数的名称和它们的时间戳。当缓冲区满时,追踪系统会通过提供的用户界面打印日志信息。
输出的追踪日志格式符合 Android 的 systrace 格式,可以使用 Perfetto 可视化。

使用

配置分析器

要启用分析器,在 lv_conf.h 中设置 LV_USE_PROFILER 并配置以下选项:

  1. 通过设置 LV_USE_PROFILER_BUILTIN 启用内置分析器功能。
  2. 缓冲区配置:设置 LV_PROFILER_BUILTIN_BUF_SIZE 的值以配置缓冲区大小。较大的缓冲区可以存储更多的追踪事件信息,减少对渲染的干扰,但也会导致更高的内存消耗。
  3. 时间戳配置:默认情况下,LVGL 使用具有 1ms 精度的 lv_tick_get() 函数获取事件发生时的时间戳。因此,它无法准确测量低于 1ms 的间隔。如果您的系统环境可以提供更高的精度(例如 1us),可以按如下配置分析器:
  • UNIX 环境推荐配置:
    #include <sys/syscall.h>
    #include <sys/types.h>
    #include <time.h>
    static uint32_t my_get_tick_us_cb(void)
    {
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
    }
    static int my_get_tid_cb(void)
    {
        return (int)syscall(SYS_gettid);
    }
    static int my_get_cpu_cb(void)
    {
        int cpu_id = 0;
        syscall(SYS_getcpu, &cpu_id, NULL);
        return cpu_id;
    }
    void my_profiler_init(void)
    {
        lv_profiler_builtin_config_t config;
        lv_profiler_builtin_config_init(&config);
        config.tick_per_sec = 1000000; /* 一秒等于 1000000 微秒 */
        config.tick_get_cb = my_get_tick_us_cb;
        config.tid_get_cb = my_get_tid_cb;
        config.cpu_get_cb = my_get_cpu_cb;
        lv_profiler_builtin_init(&config);
    }
    
  • Arduino 环境推荐配置:
    void my_profiler_init(void)
    {
        lv_profiler_builtin_config_t config;
        lv_profiler_builtin_config_init(&config);
        config.tick_per_sec = 1000000; /* 一秒等于 1000000 微秒 */
        config.tick_get_cb = micros; /* 使用 Arduino 提供的微秒时间戳 */
        lv_profiler_builtin_init(&config);
    }
    
  1. 日志输出配置:默认情况下,LVGL 使用 LV_LOG() 接口输出追踪信息。如果您希望使用其他接口输出日志信息(例如文件流),可以使用以下代码重定向日志输出:
    static void my_log_print_cb(const char * buf)
    {
        printf("%s", buf);
    }
    void my_profiler_init(void)
    {
        lv_profiler_builtin_config_t config;
        lv_profiler_builtin_config_init(&config);
        ... /* 其他配置 */
        config.flush_cb = my_log_print_cb;
        lv_profiler_builtin_init(&config);
    }
    
运行测试场景

运行您想要测量的 UI 场景,例如上下滚动一个可滚动页面或进入/退出一个应用程序。

处理日志

将输出日志保存为 my_trace.txt,使用 trace_filter.py 进行过滤和预处理:

./lvgl/scripts/trace_filter.py my_trace.txt

python3 ./lvgl/scripts/trace_filter.py my_trace.txt

您将获得一个名为 trace.systrace 的处理后的文本文件,内容大致如下:

# tracer: nop
#
LVGL-1 [0] 2892.002993: tracing_mark_write: B|1|lv_timer_handler
LVGL-1 [0] 2892.002993: tracing_mark_write: B|1|_lv_display_refr_timer
LVGL-1 [0] 2892.003459: tracing_mark_write: B|1|refr_invalid_areas
LVGL-1 [0] 2892.003461: tracing_mark_write: B|1|lv_draw_rect
LVGL-1 [0] 2892.003550: tracing_mark_write: E|1|lv_draw_rect
LVGL-1 [0] 2892.003552: tracing_mark_write: B|1|lv_draw_rect
LVGL-1 [0] 2892.003556: tracing_mark_write: E|1|lv_draw_rect
LVGL-1 [0] 2892.003560: tracing_mark_write: B|1|lv_draw_rect
LVGL-1 [0] 2892.003573: tracing_mark_write: E|1|lv_draw_rect
...

将处理后的 trace.systrace 文件导入 Perfetto 并等待解析。

性能分析

如果日志解析成功,您将看到以下界面:
在 Perfetto UI 中,使用 A 或 D 键水平平移时间轴,使用 W 或 S 键缩放时间轴。使用鼠标移动焦点并点击时间轴上的函数以观察其执行时间。

添加测量点

用户可以添加自己的测量函数:

void my_function_1(void)
{
    LV_PROFILER_BEGIN;
    do_something();
    LV_PROFILER_END;
}
void my_function_2(void)
{
    LV_PROFILER_BEGIN_TAG("do_something_1");
    do_something_1();
    LV_PROFILER_END_TAG("do_something_1");
    LV_PROFILER_BEGIN_TAG("do_something_2");
    do_something_2();
    LV_PROFILER_END_TAG("do_something_2");
}

自定义分析器实现

如果您希望使用操作系统提供的分析器方法,可以在 lv_conf.h 中修改以下配置:

#define LV_PROFILER_INCLUDE "nuttx/sched_note.h"
#define LV_PROFILER_BEGIN          sched_note_begin(NOTE_TAG_ALWAYS)
#define LV_PROFILER_END            sched_note_end(NOTE_TAG_ALWAYS)
#define LV_PROFILER_BEGIN_TAG(str) sched_note_beginex(NOTE_TAG_ALWAYS, str)
#define LV_PROFILER_END_TAG(str)   sched_note_endex(NOTE_TAG_ALWAYS, str)

常见问题

Perfetto 日志解析失败

请检查日志的完整性。如果日志不完整,可能是以下原因导致的:

  1. 高波特率导致的串口接收错误。需要降低波特率。
  2. 在打印追踪日志时插入了其他线程日志导致的数据损坏。需要禁用其他线程的日志输出,或参考上面的配置使用单独的日志输出接口。
  3. 确保 LV_PROFILER_BEGIN_TAG/END_TAG 传入的字符串不是堆栈上的局部变量或共享内存中的字符串,因为目前只记录字符串地址而不复制内容。
Perfetto 中函数执行时间显示为 0s

如果函数执行时间低于时间戳的精度,就会出现这种情况。可以参考上面的配置说明使用更高精度的
时间戳。

分析过程中出现明显卡顿

当用于存储追踪事件的缓冲区满时,分析器会输出缓冲区中的所有数据,这可能导致 UI 阻塞和卡顿。可以通过以下措施优化:

  1. 增加 LV_PROFILER_BUILTIN_BUF_SIZE 的值。较大的缓冲区可以减少日志打印的频率,但也会消耗更多内存。
  2. 优化日志打印函数的执行时间,例如增加串口波特率或提高文件写入速度。
追踪日志未输出

如果在缓冲区未满时追踪日志未自动打印,可以尝试以下方法强制日志输出:

  1. 减小 LV_PROFILER_BUILTIN_BUF_SIZE 的值,以更快填满缓冲区并触发自动打印。
  2. 手动调用或使用定时器调用 lv_profiler_builtin_flush() 函数强制日志输出。
  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值