LVGL源码(6):Style样式介绍
关于样式对象lv_style_t的成员定义,主要包含三类样式,分别为单属性样式、多属性样式和只读属性样式,只读属性样式的存在我们可以知道是为了减少RAM的占用以及保证程序运行过程中的不可修改性,但为什么还要分单属性样式和多属性样式两者存储方式呢,这不是会增加操作样式时的复杂度吗?在查阅了相关资料后,个人认为这是出于空间和性能优化的考量,从空间角度来说,绝大部分样式实例只会存储 1~2 个属性,如果
LVGL版本:8.3
LVGL的样式用于设置对象的外观,如果对前端有所了解了应该知道HTML、CSS、JavaScript三件套,其中CSS用于设置控件对象的外观、颜色、大小等,LVGL的样式就和CSS的作用类似,因此要让界面和交互达到想要的效果,学会使用Style样式是必不可少的。
特性:
1、样式定义:样式是一个 lv_style_t 类型的变量,它可以存储各种属性。类似于CSS中的类,可以集中定义一组样式规则;
2、样式应用:样式可以被分配给对象以改变其外观。在应用时,可以指定目标部分和目标状态。例如,当滑块处于按下状态时,可以为滑块的旋钮部分添加样式,由于对象由一个或多个组件元素组成同时拥有多种状态,因此在对一个对象施加样式时,我们能设置的最小单元部分为该对象处于某种状态下的某个组件元素的样式;
3、样式复用:一个样式可以被多个对象使用;
4、样式层叠:样式支持层叠效果,即一个对象可以被赋予多个样式;
5、样式优先级:如果一个属性在两个样式中都被定义,则对象将采用最新添加的那个样式的属性值,也叫样式级联,这带来的好处之一就是增加样式复用,例如对象A和对象B样式基本相同,只是B的背景颜色和A不一样,那么对象B就能使用A的样式,然后新增加一行代码用于设置对象B的背景颜色即可;
6、属性继承:一些属性如果在对象本身没有明确指定,可以从父级对象继承,我们也可以强制设置对象继承其父对象的样式或者强制设置对象使用默认样式;
7、本地样式:对象还可以拥有优先级高于普通样式的本地样式。本地样式(local styles)是指特定对象(如按钮、标签、图像等)上应用的特定样式设置。这些样式设置将覆盖全局样式,以便在特定对象上实现个性化的外观和行为。当对象使用本地样式时会自动分配存储空间,并在删除对象时释放本地样式,不需要手动管理,需要注意的是本地样式不能在其他对象之间共享;
//普通样式(多个对象可共享)
static lv_style_t style;
lv_style_init(&style);
lv_style_set_bg_color(&style, lv_palette_main(LV_PALETTE_BLUE));
lv_obj_add_style(btn1, &style, 0);
lv_obj_add_style(btn2, &style, 0); //共享样式
// 本地样式(只作用于 btn3)
lv_obj_set_style_bg_color(btn3, lv_palette_main(LV_PALETTE_RED), 0);
像Gui_guider生成的LVGL代码对控件样式的处理基本上都是靠本地样式,这样简化了样式管理,但会占用更多 RAM,如果要优化内存占用,可以手动修改代码,使用 lv_style_t 普通样式然后在多个对象上进行复用;
8、与CSS的区别及状态关联:在CSS中,伪类用于描述元素(控件对象)的不同状态,例如 :hover 用于鼠标悬停状态,:focus 用于获得焦点状态等。伪元素则是将控件对象的一部分(这一部分没有实际 DOM 结构,不能单独存在,如一个文本框对象,该对象的第一行或者开头就可以作为伪元素)进行选择并添加样式;如
格式:selector::pseudo-element {}
p::first-line { font-weight: bold; } /* 选取 <p> 的第一行 */
p::before { content: "👉 "; } /* 在 <p> 开头插入 "👉 " */
而在LVGL中也有类似的概念,如状态(对应CSS的伪类)和部件(对应CSS的伪元素),但是他们之间又有区别,例如伪元素是虚拟元素没有实际 DOM 结构,不能脱离对象主体去对伪元素进行操作,而LVGL中的部件是真实存在的子部分(可独立设置样式),我们可以对其进行独立控制;伪类则只是描述状态,不存储属性,如下:
input {
border: 1px solid gray; /* 默认边框 */
}
input:focus {
border: 2px solid blue; /* 获取焦点时加粗边框 */
}
而在LVGL中的状态就不一样了,状态和样式直接绑定,每一个状态都有独立样式,伪类和LVGL状态的区别就类似于:CSS 伪类(:hover、:focus) 类似于回调函数,当元素进入特定状态时,CSS会 自动应用对应的样式,伪类本身不会存储属性,只是一个 触发条件;LVGL 的 LV_STATE_xx 更像是“多个单独的实例”,每个状态 存储了独立的样式属性,切换状态就像切换不同的实例,这种设计使得在LVGL中更加直接地控制对象的状态;
为了实现状态样式独立的特性,LVGL中每个状态的样式在存储上是独立的,但它采用了优化的存储方式,不会重复存储相同的样式,从而减少 RAM/Flash 的占用,具体表现为当你使用 lv_obj_set_style_xx() 绑定样式时,LVGL 会 将该样式存入对象的状态表。不同状态的样式是独立存储的,但只存储发生变化的属性;
9、过渡效果:当对象状态发生变化时,可以应用过渡动画效果,类似页面切换时的过渡;
控件状态
控件对象可以处于以下基本状态,一个对象可以同时处于多个基本状态的组合,例如同时处于“聚焦”和“按下”状态,这可以表示为 LV_STATE_FOCUSED | LV_STATE_PRESSED。如果某个状态下未定义某个属性,则会使用最匹配状态的该属性,匹配原则为:如果某个状态没有明确设定的属性,则会采用与其最接近且优先级较高的状态的属性。举例就是当该状态为多个基本状态的组合时最接近且优先级最高的状态就是组合中的某个基本状态,当状态本身就是基本状态时,最接近且优先级最高的状态就是默认状态LV_STATE_DEFAULT。
例如我定义一个控件的LV_PART_MAIN部分(一般为背景)处于 LV_STATE_DEFAULT时,其背景颜色属性为白色,处于LV_STATE_FOCUSED状态时其背景颜色属性为红色,对于LV_STATE_PRESSED状态和 LV_STATE_FOCUSED | LV_STATE_PRESSED我们不定义背景颜色属性,当处于 LV_STATE_FOCUSED | LV_STATE_PRESSED状态时,根据匹配原则我们寻找最接近且优先级较高的有明确属性定义的状态,即LV_STATE_FOCUSED状态,那么此时背景颜色属性为红色;当处于LV_STATE_PRESSED状态时,根据匹配原则我们匹配到 LV_STATE_DEFAULT状态,此时背景颜色属性为白色。
lv_obj.h:
/**
* Possible states of a widget.
* OR-ed values are possible
*/
enum {
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_ANY = 0xFFFF, /**< 在某些函数中可以使用特殊值来针对所有状态 */
};
typedef uint16_t lv_state_t;
状态优先级:
状态有优先级,这由它们的数值决定(详见上方列表),数值越高,优先级越高。举例:
1. 默认状态:对象最初处于默认状态,因此属性直接从默认状态中获取,即白色。
2. 按下状态 (LV_STATE_PRESSED):此时有两个相关属性:默认状态的白色(默认状态适用于所有状态)和按下状态的灰色。由于按下状态的优先级 (0x0020) 高于默认状态 (0x0000),因此使用灰色。
3. 聚焦状态 (LV_STATE_FOCUSED):与按下状态类似,聚焦状态的优先级高于默认状态,因此使用红色。
4. (LV_STATE_PRESSED | LV_STATE_FOCUSED):此时既有灰色(按下状态)又有红色(聚焦状态),由于按下状态的优先级 (0x0020) 高于聚焦状态 (0x0002),因此使用灰色。
5. 组合状态的特殊情况:如果为 LV_STATE_PRESSED | LV_STATE_FOCUSED 组合状态设置了玫瑰色,则此组合状态的优先级为 0x0020 + 0x0002 = 0x0022,高于单独的按下状态,因此将使用玫瑰色。
选择器(控件元素):
样式通过选择器选择需要作用的元素;而 LVGL 中,样式通过选择器作用于控件的部分,要明白什么是控件的部分,需要分析控件的组成。
仔细观察滑块的组成,滑块可以由:
- 主体外形(main)
- 把手(knob), (就是可变化的小圆点,有的控件没有这个模块)
- 进度指示条(indicator)
以上三种组成,可以通过选择器单独设置这三个构成部分的样式。
选择器的所有类型如下:
lv_obj.h:
/**
* The possible parts of widgets.
* The parts can be considered as the internal building block of the widgets.
* E.g. slider = background + indicator + knob
* Not all parts are used by every widget
*/
enum {
LV_PART_MAIN = 0x000000, /**< 主体,一般是背景 */
LV_PART_SCROLLBAR = 0x010000, /**< 滚动条 */
LV_PART_INDICATOR = 0x020000, /**< 指示器,例如滑块、栏、开关或复选框的勾框 */
LV_PART_KNOB = 0x030000, /**< 就像抓住手柄来调整数值,例如:滑块控件上的滑动块,弧控件上的滑动块,开关控件上的开关小白点 */
LV_PART_SELECTED = 0x040000, /**< 指示当前选择的选项或部分 */
LV_PART_ITEMS = 0x050000, /**< 当小部件有多个相似元素时使用(例如表格单元格) */
LV_PART_TICKS = 0x060000, /**< 刻度上的刻度,如图表或仪表上的刻度 */
LV_PART_CURSOR = 0x070000, /**< 标记一个特定的位置,例如文本区域的光标或图表 */
LV_PART_CUSTOM_FIRST = 0x080000, /**< 自定义小部件的扩展点* */
LV_PART_ANY = 0x0F0000, /**< 在某些函数中可以使用特殊值来针对所有部分 */
};
typedef uint32_t lv_part_t;
选择器的一个更妙的用途是和控件状态做按位或运算,从而可以修改某个部分在某个状态下的样式。这种组合可以说是对对象样式设置(选择器)的最小单元;
样式定义:
关于样式对象lv_style_t的成员定义,主要包含三类样式,分别为单属性样式、多属性样式和只读属性样式,只读属性样式的存在我们可以知道是为了减少RAM的占用以及保证程序运行过程中的不可修改性,但为什么还要分单属性样式和多属性样式两者存储方式呢,这不是会增加操作样式时的复杂度吗?在查阅了相关资料后,个人认为这是出于空间和性能优化的考量,从空间角度来说,绝大部分样式实例只会存储 1~2 个属性,如果所有样式都使用动态分配,那么每个样式对象至少需要 malloc 一块内存,即使它只存储一个属性,由于 malloc 需要额外的元数据(通常 8~16 字节),对于小对象,内存浪费会非常大,同时malloc和free的次数过多也会引入额外的内存碎片;从性能角度考虑,动态分配的访问速度较慢。
/**
* Descriptor of a style (a collection of properties and values).
*/
typedef struct {
#if LV_USE_ASSERT_STYLE
uint32_t sentinel;
#endif
/*一个联合体,当样式只有一个属性时,使用value1直接存储
当样式有多个属性时,使用数组指针values_and_props存储,
当样式为静态样式时,使用数组指针const_props存储只读的常量属性*/
union {
lv_style_value_t value1;
uint8_t * values_and_props;
const lv_style_const_prop_t * const_props;
} v_p;
uint16_t prop1; // 存储样式的第一个属性ID
uint8_t has_group; // 使用|标记该样式包含的所有属性ID类别,用于加速样式匹配。
uint8_t prop_cnt; // 该样式包含的属性数量
} lv_style_t;
/**
* Descriptor of a constant style property.
*/
typedef struct {
lv_style_prop_t prop; // 样式属性ID
lv_style_value_t value; // 样式属性值
} lv_style_const_prop_t;
/**
* A common type to handle all the property types in the same way.
*/
typedef union {
int32_t num; //用于存储数值属性,例如 LV_STYLE_BORDER_WIDTH(边框宽度)、不透明度
const void * ptr; //用于存储指针类型的属性,例如 LV_STYLE_TEXT_FONT(字体)、图片
lv_color_t color; //用于存储颜色属性,例如 LV_STYLE_BG_COLOR(背景颜色)
} lv_style_value_t;
/**
* 枚举所有内置样式属性ID
*
* 属性ID每16个为一组Group,向Group添加新的属性ID时,请确保它不会溢出到下一个Group中。
*/
typedef enum {
LV_STYLE_PROP_INV = 0,
/*Group 0*/
LV_STYLE_WIDTH = 1,
LV_STYLE_MIN_WIDTH = 2,
...
/*Group 1*/
LV_STYLE_PAD_TOP = 16,
LV_STYLE_PAD_BOTTOM = 17,
...
/*Group 6*/
LV_STYLE_OPA = 96,
LV_STYLE_OPA_LAYERED = 97,
...
_LV_STYLE_LAST_BUILT_IN_PROP = 112,
_LV_STYLE_NUM_BUILT_IN_PROPS = _LV_STYLE_LAST_BUILT_IN_PROP + 1,
LV_STYLE_PROP_ANY = 0xFFFF,
_LV_STYLE_PROP_CONST = 0xFFFF /* magic value for const styles */
} lv_style_prop_t;
样式属性:
样式属性就是lv_style_prop_t enum数据类型中的上百种属性ID,每种属性ID都表示不同的作用,有设置对象对象的宽度、高度的 LV_STYLE_WIDTH和LV_STYLE_HEIGHT;有设置对象在屏幕上的X坐标和Y坐标LV_STYLE_X、LV_STYLE_Y;有设置对象的背景颜色和背景颜色不透明度的LV_STYLE_BG_COLOR、LV_STYLE_BG_OPA等等,详见下面两篇参考文章。
参考文章:
【LVGL 学习】样式(style)风格学习_lvgl style-CSDN博客
使用样式:
样式初始化、添加、删除:
样式存储在 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);
初始化样式:
void lv_style_init(lv_style_t * style)
{
#if LV_USE_ASSERT_STYLE
if(style->sentinel == LV_STYLE_SENTINEL_VALUE && style->prop_cnt > 1) {
LV_LOG_WARN("Style might be already inited. (Potential memory leak)");
}
#endif
lv_memset_00(style, sizeof(lv_style_t));
#if LV_USE_ASSERT_STYLE
style->sentinel = LV_STYLE_SENTINEL_VALUE;
#endif
}
添加样式:
我们从上面的例子中看到了一些样式添加函数例如lv_style_set_bg_color(&style, <value>);等,这些样式添加函数都在lv_style_gen.c中,其实都是lv_style_set_prop_internal()函数的进一步封装,lv_style_set_prop_internal()函数的主要工作流程为:
1. 断言检查:确保 style 指针有效(如果编译时开启了 LV_USE_ASSERT_STYLE)。
2. 检查是否为只读样式,如果 style->prop1 被标记为 LV_STYLE_PROP_ANY,说明这个 style 是常量样式(只读),不能修改,因此直接返回。
3. 使用LV_STYLE_PROP_ID_MASK 宏用于从 prop_and_meta 提取纯属性 ID(去掉元数据)。
4. 处理多属性存储情况,遍历已有属性ID的values_and_props 指针数组,检查属性是否已存在,如果找到匹配的 prop_id,则直接更新值,调用 value_adjustment_helper 并返回。如果是添加新属性,就重新分配 values_and_props以存储新属性和新值,新大小 = (旧属性数+1) * (属性值大小 + 属性 ID 大小),将旧属性向后移动,为新属性腾出空间,更新 prop_cnt(属性数量+1)并设置新属性值。
5. 处理 prop_cnt == 1仅有一个属性时的情况,如果添加的是相同属性ID,直接更新,否则就转换存储方式为数组指针多属性存储。
6. 处理 prop_cnt == 0(第一次存储)prop_cnt == 0 说明当前样式没有任何属性,直接存储 prop1 和 value1。
7. 设置样式属性的分组信息,标记该样式包含的属性类别,用于加速样式匹配。
lv_style_gen.c:
void lv_style_set_bg_color(lv_style_t * style, lv_color_t value)
{
lv_style_value_t v = {
.color = value
};
lv_style_set_prop(style, LV_STYLE_BG_COLOR, v);
}
lv_style.c:
void lv_style_set_prop(lv_style_t * style, lv_style_prop_t prop, lv_style_value_t value)
{
lv_style_set_prop_internal(style, prop, value, lv_style_set_prop_helper);
}
static void lv_style_set_prop_helper(lv_style_prop_t prop, lv_style_value_t value, uint16_t * prop_storage,
lv_style_value_t * value_storage)
{
*prop_storage = prop;
*value_storage = value;
}
/**
* @param: style lv_style_t * 指向样式对象的指针
* @param: prop_and_meta lv_style_prop_t 样式属性 ID,包含元数据(Meta)
* @param: value lv_style_value_t 属性值
* @param: value_adjustment_helper 函数指针处理属性值的回调函数
*/
static void lv_style_set_prop_internal(lv_style_t * style, lv_style_prop_t prop_and_meta, lv_style_value_t value,
void (*value_adjustment_helper)(lv_style_prop_t, lv_style_value_t, uint16_t *, lv_style_value_t *))
{
LV_ASSERT_STYLE(style); //断言检查,确保 style 指针有效
if(style->prop1 == LV_STYLE_PROP_ANY) { //检查是否为只读样式
LV_LOG_ERROR("Cannot set property of constant style");
return;
}
//LV_STYLE_PROP_ID_MASK宏用于从 prop_and_meta 提取纯属性 ID(去掉元数据)。
lv_style_prop_t prop_id = LV_STYLE_PROP_ID_MASK(prop_and_meta);
if(style->prop_cnt > 1) { //处理多属性存储情况
uint8_t * tmp = style->v_p.values_and_props + style->prop_cnt * sizeof(lv_style_value_t);
uint16_t * props = (uint16_t *)tmp;
int32_t i;
for(i = style->prop_cnt - 1; i >= 0; i--) { //遍历已有属性
if(LV_STYLE_PROP_ID_MASK(props[i]) == prop_id) {
lv_style_value_t * values = (lv_style_value_t *)style->v_p.values_and_props;
value_adjustment_helper(prop_and_meta, value, &props[i], &values[i]);
return;
}
}
//添加新属性
size_t size = (style->prop_cnt + 1) * (sizeof(lv_style_value_t) + sizeof(uint16_t));
uint8_t * values_and_props = lv_mem_realloc(style->v_p.values_and_props, size);
if(values_and_props == NULL) return;
style->v_p.values_and_props = values_and_props;
tmp = values_and_props + style->prop_cnt * sizeof(lv_style_value_t);
props = (uint16_t *)tmp;
/*将旧属性向后移动,为新属性腾出空间*/
for(i = style->prop_cnt - 1; i >= 0; i--) {
props[i + sizeof(lv_style_value_t) / sizeof(uint16_t)] = props[i];
}
style->prop_cnt++;
/*Go to the new position wit the props*/
tmp = values_and_props + style->prop_cnt * sizeof(lv_style_value_t);
props = (uint16_t *)tmp;
lv_style_value_t * values = (lv_style_value_t *)values_and_props;
/*设置新属性值*/
value_adjustment_helper(prop_and_meta, value, &props[style->prop_cnt - 1], &values[style->prop_cnt - 1]);
}
else if(style->prop_cnt == 1) { //处理单属性存储情况
if(LV_STYLE_PROP_ID_MASK(style->prop1) == prop_id) {
value_adjustment_helper(prop_and_meta, value, &style->prop1, &style->v_p.value1);
return;
}
size_t size = (style->prop_cnt + 1) * (sizeof(lv_style_value_t) + sizeof(uint16_t));
uint8_t * values_and_props = lv_mem_alloc(size);
if(values_and_props == NULL) return;
lv_style_value_t value_tmp = style->v_p.value1;
style->v_p.values_and_props = values_and_props;
style->prop_cnt++;
uint8_t * tmp = values_and_props + style->prop_cnt * sizeof(lv_style_value_t);
uint16_t * props = (uint16_t *)tmp;
lv_style_value_t * values = (lv_style_value_t *)values_and_props;
props[0] = style->prop1;
values[0] = value_tmp;
value_adjustment_helper(prop_and_meta, value, &props[1], &values[1]);
}
else { //处理 prop_cnt == 0(第一次存储)
style->prop_cnt = 1;
value_adjustment_helper(prop_and_meta, value, &style->prop1, &style->v_p.value1);
}
//设置样式属性的分组信息,标记该样式包含的属性类别,用于加速样式匹配。
uint8_t group = _lv_style_get_prop_group(prop_id);
style->has_group |= 1 << group;
}
添加静态样式示例:
/* 定义不可修改的常量样式属性 */
const lv_style_const_prop_t style1_props[] = {
LV_STYLE_CONST_WIDTH(50),
LV_STYLE_CONST_HEIGHT(50),
LV_STYLE_PROP_INV,
};
LV_STYLE_CONST_INIT(style1, style1_props);
// *INDENT-OFF*
#if LV_USE_ASSERT_STYLE //默认为0
#define LV_STYLE_CONST_INIT(var_name, prop_array) \
const lv_style_t var_name = { \
.sentinel = LV_STYLE_SENTINEL_VALUE, \
.v_p = { .const_props = prop_array }, \
.has_group = 0xFF, \
.prop1 = LV_STYLE_PROP_ANY, \
.prop_cnt = (sizeof(prop_array) / sizeof((prop_array)[0])), \
}
#else
#define LV_STYLE_CONST_INIT(var_name, prop_array) \
const lv_style_t var_name = { \
.v_p = { .const_props = prop_array }, \
.has_group = 0xFF, \
.prop1 = LV_STYLE_PROP_ANY, \
.prop_cnt = (sizeof(prop_array) / sizeof((prop_array)[0])), \
}
#endif
// *INDENT-ON*
删除样式:
lv_style_remove_prop函数的主要工作流程为:
1. 断言检查:确保 style 指针有效(如果编译时开启了 LV_USE_ASSERT_STYLE)。
2. 检查是否为只读样式,如果 style->prop1 被标记为 LV_STYLE_PROP_ANY,说明这个 style 是常量样式(只读),不能修改,因此直接返回。
3. 如果 style->prop_cnt == 0,说明当前样式没有任何属性,直接返回 false。
4. 处理 prop_cnt == 1(仅有一个属性),检查 prop1 是否等于 prop,如果是,则将 prop1 置为 LV_STYLE_PROP_INV(表示无效属性)同时将 prop_cnt 设为 0,表示样式不再存储任何属性,如果 prop1 不是 prop,返回 false(说明该属性不存在)。
5. 处理 prop_cnt > 1(多个属性),遍历 old_props,查找匹配 prop 的属性 ID。仅剩两个属性时,转换回单属性存储,属性ID个数大于两个时,创建一个新的 values_and_props 数组,遍历 old_props,将非 prop 的属性复制到 new_values_and_props,释放旧的 values_and_props,表示删除完成。
bool lv_style_remove_prop(lv_style_t * style, lv_style_prop_t prop)
{
LV_ASSERT_STYLE(style);
if(style->prop1 == LV_STYLE_PROP_ANY) {
LV_LOG_ERROR("Cannot remove prop from const style");
return false;
}
if(style->prop_cnt == 0) return false;
if(style->prop_cnt == 1) {
if(LV_STYLE_PROP_ID_MASK(style->prop1) == prop) {
style->prop1 = LV_STYLE_PROP_INV;
style->prop_cnt = 0;
return true;
}
return false;
}
uint8_t * tmp = style->v_p.values_and_props + style->prop_cnt * sizeof(lv_style_value_t);
uint16_t * old_props = (uint16_t *)tmp;
uint32_t i;
for(i = 0; i < style->prop_cnt; i++) {
if(LV_STYLE_PROP_ID_MASK(old_props[i]) == prop) {
lv_style_value_t * old_values = (lv_style_value_t *)style->v_p.values_and_props;
if(style->prop_cnt == 2) {
style->prop_cnt = 1;
style->prop1 = i == 0 ? old_props[1] : old_props[0];
style->v_p.value1 = i == 0 ? old_values[1] : old_values[0];
}
else {
size_t size = (style->prop_cnt - 1) * (sizeof(lv_style_value_t) + sizeof(uint16_t));
uint8_t * new_values_and_props = lv_mem_alloc(size);
if(new_values_and_props == NULL) return false;
style->v_p.values_and_props = new_values_and_props;
style->prop_cnt--;
tmp = new_values_and_props + style->prop_cnt * sizeof(lv_style_value_t);
uint16_t * new_props = (uint16_t *)tmp;
lv_style_value_t * new_values = (lv_style_value_t *)new_values_and_props;
uint32_t j;
for(i = j = 0; j <= style->prop_cnt;
j++) { /*<=: because prop_cnt already reduced but all the old props. needs to be checked.*/
if(old_props[j] != prop) {
new_values[i] = old_values[j];
new_props[i++] = old_props[j];
}
}
}
lv_mem_free(old_values);
return true;
}
}
return false;
}
修改样式:
如果在程序运行过程中我们修改了某一个样式your_style(注意不是向对象添加样式),那么修改完样式之后,由于所有使用了your_style样式的对象并不会自动将最新的样式更新到自身中,因此我们需要调用lv_obj_report_style_change(&style);进行手动通知,让所有用your_style的对象都重新应用最新的your_style样式并刷新一下;
样式过渡:
默认情况下,当对象的状态发生变化(例如被按下时),新状态的属性会立即生效。然而,使用过渡效果(Transitions),可以在状态变化时播放动画。例如,按下按钮时,其背景颜色可以在 300 毫秒 内逐渐变为按下状态的颜色。
过渡效果也是一个样式属性ID,为 LV_STYLE_TRANSITION,其参数为ptr指针类型,可以设置以下内容:
-
过渡时间(transition duration)
-
过渡延迟(transition delay,动画开始前的等待时间)
-
动画路径(animation path,也称为时间曲线或缓动函数)
-
需要动画过渡的属性(如颜色、大小等)
每个状态都可以单独定义过渡参数。例如:
-
在 默认状态(
LV_STATE_DEFAULT)下设置 500ms 过渡时间,意味着当对象恢复到默认状态时,动画持续 500ms,平滑恢复。 -
在 按下状态(
LV_STATE_PRESSED)下设置 100ms 过渡时间,意味着当对象进入按下状态时,动画持续 100ms,迅速变更。
示例:
lv_style_t style;
lv_style_init(&style);
static lv_style_transition_dsc_t transition_dsc; //定义了一个过渡属性ID实例
static const lv_style_prop_t props[] = {LV_STYLE_BG_COLOR, 0}; // 需要有动画的属性ID
/*初始化过渡属性ID实例,需要动画的属性ID为LV_STYLE_BG_COLOR,设定线性变化(lv_anim_path_linear),动画时间为300ms,无延迟。*/
lv_style_transition_dsc_init(&transition_dsc, props, lv_anim_path_linear, 300, 0, NULL);
//应用该过渡效果到style样式
lv_style_set_transition(&style, &transition_dsc);
lv_style.c:
void lv_style_transition_dsc_init(lv_style_transition_dsc_t * tr, const lv_style_prop_t props[],
lv_anim_path_cb_t path_cb, uint32_t time, uint32_t delay, void * user_data)
{
lv_memset_00(tr, sizeof(lv_style_transition_dsc_t));
tr->props = props;
tr->path_xcb = path_cb == NULL ? lv_anim_path_linear : path_cb;
tr->time = time;
tr->delay = delay;
#if LV_USE_USER_DATA
tr->user_data = user_data;
#else
LV_UNUSED(user_data);
#endif
}
lv_style_gen.c:
void lv_style_set_transition(lv_style_t * style, const lv_style_transition_dsc_t * value)
{
lv_style_value_t v = {
.ptr = value
};
lv_style_set_prop(style, LV_STYLE_TRANSITION, v);
}
/**
* Descriptor for style transitions
*/
typedef struct {
const lv_style_prop_t * props; /**< An array with the properties to animate.*/
#if LV_USE_USER_DATA
void * user_data; /**< A custom user data that will be passed to the animation's user_data */
#endif
lv_anim_path_cb_t path_xcb; /**< A path for the animation.*/
uint32_t time; /**< Duration of the transition in [ms]*/
uint32_t delay; /**< Delay before the transition in [ms]*/
} lv_style_transition_dsc_t;
主题:
主题是一组样式的集合。当 激活某个主题 后,LVGL 会自动将该主题应用到所有新创建的小部件。这样,UI 界面会有一个默认的外观,当然,你仍然可以通过额外的样式进行修改。
每个显示屏都可以有不同的主题。例如:在 TFT 显示屏 上使用 彩色主题;在 单色屏 上使用 单色主题。这样可以确保不同显示设备的 UI 具有合适的风格。
要为某个显示屏设置主题,需要两个步骤:(1)初始化主题:使用 lv_theme_xxx_init() 来初始化一个主题,如lv_theme_default_init()、lv_theme_basic_init()等等;(2)将主题应用到某个显示屏;示例如下:
lv_theme_default.c:
lv_theme_t * th = lv_theme_default_init(display, /*Use the DPI, size, etc from this display*/
LV_COLOR_PALETTE_BLUE, LV_COLOR_PALETTE_CYAN, /*Primary and secondary palette*/
false, /*Light or dark mode*/
&lv_font_montserrat_10, &lv_font_montserrat_14, &lv_font_montserrat_18); /*Small, normal, large fonts*/
lv_disp.c:
lv_disp_set_theme(display, th); /*Assign the theme to the display*/
typedef struct _lv_theme_t {
lv_theme_apply_cb_t apply_cb;
struct _lv_theme_t * parent; /**< Apply the current theme's style on top of this theme.*/
void * user_data;
struct _lv_disp_t * disp;
lv_color_t color_primary;
lv_color_t color_secondary;
const lv_font_t * font_small;
const lv_font_t * font_normal;
const lv_font_t * font_large;
uint32_t flags; /*Any custom flag used by the theme*/
} lv_theme_t;
我们可以在lv_conf.h中通过宏#define LV_USE_THEME_DEFAULT 1控制是否启用默认主题,如果启用,则会在lvgl的显示屏幕驱动函数中进行初始化默认主题,实际代码如下:
lv_hal_disp.c:
lv_disp_t * lv_disp_drv_register(lv_disp_drv_t * driver)
{
...
#if LV_USE_THEME_DEFAULT
if(lv_theme_default_is_inited() == false) {
disp->theme = lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE),
lv_palette_main(LV_PALETTE_RED),
LV_THEME_DEFAULT_DARK, LV_FONT_DEFAULT);
}
else {
disp->theme = lv_theme_default_get();
}
#endif
...
}
主题扩展:
LVGL 允许使用lv_theme_set_parent(new_theme, base_theme);继承并扩展主题,例如在默认主题上增加自己的定制风格,从而达到效果默认主题 → 自定义主题 → 暗黑主题(通过样式优先级的特性,新主题会覆盖继承的旧主题相同的样式)。
lv_theme_t * dark_theme = lv_theme_default_init(NULL, lv_palette_main(LV_PALETTE_BLACK), lv_palette_main(LV_PALETTE_GREY), true, LV_FONT_DEFAULT);
lv_theme_set_parent(custom_theme, dark_theme);
将样式应用到对象:
我们在知道样式是如何定义、使用后,我们还需要将样式应用在实际的控件和对象上才能让样式的功能展现出来。
添加对象样式:
使用 lv_obj_add_style(obj, &style, <selector>)为对象添加样式,其中,<selector> 是 部件(part) 和 状态(state) 的按位或(OR) 组合,表示该样式适用于对象的哪个部分和状态。其中关于状态值LV_STATE_DEFAULT和部件值LV_PART_MAIN都为默认值,例如当<selector>为LV_STATE_PRESSED时,其实就是LV_PART_MAIN|LV_STATE_PRESSED,使用示例如下:
/* 初始化按下时的按钮样式 */
lv_style_init(&btn_red);
lv_style_set_bg_color(&btn_red, lv_palette_main(LV_PALETTE_RED)); // 按下时背景变红
//第一个 lv_obj_add_style: 将 style_btn 应用于按钮的默认状态。
//第二个 lv_obj_add_style: 当按钮按下时(LV_STATE_PRESSED),覆盖LV_PART_MAIN部分的颜色,使其背景变红。
lv_obj_add_style(btn, &style_btn, 0); /* 默认按钮样式 */
lv_obj_add_style(btn, &btn_red, LV_STATE_PRESSED); /* 按下按钮时,部分颜色变红 */
添加样式函数的大概工作逻辑如下:
1、先删除可能存在的样式过渡动画trans_del(obj, selector, LV_STYLE_PROP_ANY, NULL);
作用:删除 obj 在 selector 作用下的 所有样式过渡动画。为什么要删除?如果对象已经有正在执行的样式过渡动画,但当前直接修改样式,可能会导致动画和新样式冲突,所以先删除已有的动画效果。
2、计算插入新样式的位置,通过for 循环遍历style数组,跳过所有正在进行的过渡样式(is_trans = 1);跳过所有本地样式(is_local = 1);找到第一个“正常”样式的位置,i 记录了此位置。(跳过过渡样式和本地样式的原因是样式优先级是由style数组obj->styles[]的下标决定的,,下标越小,优先级越高,而过渡样式优先级>本地样式优先级>“正常”样式优先级,因此插入“正常”样式需要跳过前两类样式,同时新插入的“正常”样式的数组下标比其他“正常”样式的数组下标都小,因此可以覆盖之前添加的相同<selector>的“正常”样式)
3、扩展 styles 数组大小,obj->style_cnt++:对象的样式数加 1;lv_mem_realloc():重新分配 styles 数组的内存,使其能够容纳新的样式,然后将样式数组后移,腾出插入位置。
4、使用lv_obj_refresh_style(obj, selector, LV_STYLE_PROP_ANY);通知 obj 应用新的样式 并刷新界面使新样式生效,LV_STYLE_PROP_ANY 表示 刷新该对象的所有属性。
lv_obj_style.c:
void lv_obj_add_style(lv_obj_t * obj, lv_style_t * style, lv_style_selector_t selector)
{
trans_del(obj, selector, LV_STYLE_PROP_ANY, NULL);
uint32_t i;
/*Go after the transition and local styles*/
for(i = 0; i < obj->style_cnt; i++) {
if(obj->styles[i].is_trans) continue;
if(obj->styles[i].is_local) continue;
break;
}
/*Now `i` is at the first normal style. Insert the new style before this*/
/*Allocate space for the new style and shift the rest of the style to the end*/
obj->style_cnt++;
obj->styles = lv_mem_realloc(obj->styles, obj->style_cnt * sizeof(_lv_obj_style_t));
uint32_t j;
for(j = obj->style_cnt - 1; j > i ; j--) {
obj->styles[j] = obj->styles[j - 1];
}
lv_memset_00(&obj->styles[i], sizeof(_lv_obj_style_t));
obj->styles[i].style = style;
obj->styles[i].selector = selector;
lv_obj_refresh_style(obj, selector, LV_STYLE_PROP_ANY);
}
lv_obj.h:
typedef struct _lv_obj_t {
...
_lv_obj_style_t * styles;
...
uint16_t style_cnt : 6;
...
} lv_obj_t;
lv_obj_style.h:
typedef struct {
lv_style_t * style;
uint32_t selector : 24;
uint32_t is_local : 1; //表示该样式是否为对象的本地样式
uint32_t is_trans : 1; //表示该样式是否为临时的过渡样式
} _lv_obj_style_t;
移除对象样式:
lv_obj_remove_style(lv_obj_t * obj, lv_style_t * style, lv_style_selector_t selector)主要用于从 lv_obj_t 对象上移除样式,包括 普通样式、本地样式和过渡样式。
obj:目标对象;
style:NULL:移除选择器匹配的所有样式;具体样式指针:只移除这个特定样式;
selector:指定PART(部分)和STATE(状态),用于筛选要删除选择器的样式;LV_PART_ANY | LV_STATE_ANY:移除所有部件和状态下的样式;
示例:lv_obj_remove_style(obj, NULL, LV_STATE_PRESSED); /* 移除所有 `LV_STATE_PRESSED` 状态下的样式
移除样式函数的大概工作逻辑如下:
1、将输入参数selector中的PART(部分)和STATE(状态)解析出来,默认情况下,我们会刷新所有属性,if(style && style->prop_cnt == 0) prop = LV_STYLE_PROP_INV;(如果 style 指定的样式没有属性prop_cnt == 0,则不进行刷新);
2、遍历 obj->styles[] 数组,获取数组当前样式的 state 和 part,判断是否符合删除条件:
如果 state 不是 ANY,且 state_act != state,跳过当前样式,判断数组的下一个样式;
如果 part 不是 ANY,且 part_act != part,跳过当前样式;
如果 style 不为空,且 style != obj->styles[i].style,跳过当前样式;
3、当符合删除条件后,如果当前符合删除条件样式是“过渡样式”(is_trans = 1),调用 trans_del() 终止过渡动画。如果当前样式是“本地样式”或“过渡样式”,我们会释放其内存后再将其从obj->styles[]中删除(过渡样式与 本地样式都是 仅针对单个对象,不会被多个对象共享,因此在对象不再需要时可以安全释放内存。)
lv_style_reset(obj->styles[i].style); 清空样式中指针指向的内存
lv_mem_free(obj->styles[i].style); 释放样式本身的内存
obj->styles[i].style = NULL; 指针置空
4、数组 styles[] 进行删除操作,从 i 开始,把后面的样式往前移动一位,减少 style_cnt,缩小数组,重新分配数组内存;
5、deleted = true;记录当前 styles[] 发生了变化,如果deleted = true并且 prop 不是 LV_STYLE_PROP_INV,调用 lv_obj_refresh_style(obj, part, prop); 刷新 UI;
/**
* Remove all styles from an object
* @param obj pointer to an object
*/
static inline void lv_obj_remove_style_all(struct _lv_obj_t * obj)
{
lv_obj_remove_style(obj, NULL, LV_PART_ANY | LV_STATE_ANY);
}
lv_state_t lv_obj_style_get_selector_state(lv_style_selector_t selector)
{
return selector & 0xFFFF;
}
lv_part_t lv_obj_style_get_selector_part(lv_style_selector_t selector)
{
return selector & 0xFF0000;
}
void lv_obj_remove_style(lv_obj_t * obj, lv_style_t * style, lv_style_selector_t selector)
{
lv_state_t state = lv_obj_style_get_selector_state(selector);
lv_part_t part = lv_obj_style_get_selector_part(selector);
lv_style_prop_t prop = LV_STYLE_PROP_ANY;
if(style && style->prop_cnt == 0) prop = LV_STYLE_PROP_INV;
uint32_t i = 0;
bool deleted = false;
while(i < obj->style_cnt) {
lv_state_t state_act = lv_obj_style_get_selector_state(obj->styles[i].selector);
lv_part_t part_act = lv_obj_style_get_selector_part(obj->styles[i].selector);
if((state != LV_STATE_ANY && state_act != state) ||
(part != LV_PART_ANY && part_act != part) ||
(style != NULL && style != obj->styles[i].style)) {
i++;
continue;
}
if(obj->styles[i].is_trans) {
trans_del(obj, part, LV_STYLE_PROP_ANY, NULL);
}
if(obj->styles[i].is_local || obj->styles[i].is_trans) {
lv_style_reset(obj->styles[i].style);
lv_mem_free(obj->styles[i].style);
obj->styles[i].style = NULL;
}
/*Shift the styles after `i` by one*/
uint32_t j;
for(j = i; j < (uint32_t)obj->style_cnt - 1 ; j++) {
obj->styles[j] = obj->styles[j + 1];
}
obj->style_cnt--;
obj->styles = lv_mem_realloc(obj->styles, obj->style_cnt * sizeof(_lv_obj_style_t));
deleted = true;
/*The style from the current `i` index is removed, so `i` points to the next style.
*Therefore it doesn't needs to be incremented*/
}
if(deleted && prop != LV_STYLE_PROP_INV) {
lv_obj_refresh_style(obj, part, prop);
}
}
实际案例:
由于在LVGL的样式状态优先级中,Focus状态优先级要大于Default状态和Checked状态,因此假设Switch开关控件的Default状态背景颜色为红色,Checked状态背景颜色为绿色,Focus状态背景颜色为透明的情况下,我们就会发现当焦点聚焦在Switch开关控件上时会发现控件变透明了,如下图:
![]()
![]()
![]()
![]()
而我们想要的效果是不管Switch开关控件是否处于Focus状态,控件的背景颜色都应该是未Checked时为红色,Checked时为绿色。
一开始想的办法是不设置Switch开关控件的LV_PART_MAIN部分以及LV_PART_INDICATOR部分Focus状态下的样式,但是这样的后果就是不管开关控件是否Checked,处于Focus状态下都会使用Default默认样式;
因此最后的解决办法就是在switch开关的event中,对当前状态进行判断,如果为checked就将Focused状态样式改为和checked一致,如果不为checked就将Focused状态样式改为和Default一致。下面给出了用GuiGuider生成的部分switch开关控件代码以及自己定义的switch开关控件事件逻辑:
//Write codes scrScan_sw_StartCollect
ui->scrScan_sw_StartCollect = lv_switch_create(ui->scrScan);
lv_obj_set_pos(ui->scrScan_sw_StartCollect, 253, 29);
lv_obj_set_size(ui->scrScan_sw_StartCollect, 40, 19);
//Write style for scrScan_sw_StartCollect, Part: LV_PART_MAIN, State: LV_STATE_DEFAULT.
lv_obj_set_style_bg_opa(ui->scrScan_sw_StartCollect, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(ui->scrScan_sw_StartCollect, lv_color_hex(0xf00000), LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_bg_grad_dir(ui->scrScan_sw_StartCollect, LV_GRAD_DIR_NONE, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_border_width(ui->scrScan_sw_StartCollect, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_radius(ui->scrScan_sw_StartCollect, 9, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_shadow_width(ui->scrScan_sw_StartCollect, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
//Write style for scrScan_sw_StartCollect, Part: LV_PART_MAIN, State: LV_STATE_FOCUSED.
lv_obj_set_style_bg_opa(ui->scrScan_sw_StartCollect, 255, LV_PART_MAIN|LV_STATE_FOCUSED);
lv_obj_set_style_bg_color(ui->scrScan_sw_StartCollect, lv_color_hex(0xF00000), LV_PART_MAIN|LV_STATE_FOCUSED);
lv_obj_set_style_bg_grad_dir(ui->scrScan_sw_StartCollect, LV_GRAD_DIR_NONE, LV_PART_MAIN|LV_STATE_FOCUSED);
lv_obj_set_style_border_width(ui->scrScan_sw_StartCollect, 0, LV_PART_MAIN|LV_STATE_FOCUSED);
lv_obj_set_style_radius(ui->scrScan_sw_StartCollect, 8, LV_PART_MAIN|LV_STATE_FOCUSED);
lv_obj_set_style_shadow_width(ui->scrScan_sw_StartCollect, 0, LV_PART_MAIN|LV_STATE_FOCUSED);
//Write style for scrScan_sw_StartCollect, Part: LV_PART_INDICATOR, State: LV_STATE_CHECKED.
lv_obj_set_style_bg_opa(ui->scrScan_sw_StartCollect, 255, LV_PART_INDICATOR|LV_STATE_CHECKED);
lv_obj_set_style_bg_color(ui->scrScan_sw_StartCollect, lv_color_hex(0x28b620), LV_PART_INDICATOR|LV_STATE_CHECKED);
lv_obj_set_style_bg_grad_dir(ui->scrScan_sw_StartCollect, LV_GRAD_DIR_NONE, LV_PART_INDICATOR|LV_STATE_CHECKED);
lv_obj_set_style_border_width(ui->scrScan_sw_StartCollect, 0, LV_PART_INDICATOR|LV_STATE_CHECKED);
//Write style for scrScan_sw_StartCollect, Part: LV_PART_INDICATOR, State: LV_STATE_FOCUSED.
lv_obj_set_style_bg_opa(ui->scrScan_sw_StartCollect, 255, LV_PART_INDICATOR|LV_STATE_FOCUSED);
lv_obj_set_style_bg_color(ui->scrScan_sw_StartCollect, lv_color_hex(0xffffff), LV_PART_INDICATOR|LV_STATE_FOCUSED);
lv_obj_set_style_bg_grad_dir(ui->scrScan_sw_StartCollect, LV_GRAD_DIR_NONE, LV_PART_INDICATOR|LV_STATE_FOCUSED);
lv_obj_set_style_border_width(ui->scrScan_sw_StartCollect, 0, LV_PART_INDICATOR|LV_STATE_FOCUSED);
/***** Screen_Scan 初始化函数 *****/
void Init_Screen_Scan (lv_ui *ui)
{
//首先将控件Focus状态下的样式和控件当前Checked/未Checked样式保持一致
if (lv_obj_has_state(guider_ui.scrScan_sw_StartCollect, LV_STATE_CHECKED)) {
lv_obj_set_style_bg_color(guider_ui.scrScan_sw_StartCollect, lv_color_hex(0x28b620), LV_PART_MAIN | LV_STATE_FOCUSED);
lv_obj_set_style_bg_color(guider_ui.scrScan_sw_StartCollect, lv_color_hex(0x28b620), LV_PART_INDICATOR | LV_STATE_FOCUSED);
}
else {
lv_obj_set_style_bg_color(guider_ui.scrScan_sw_StartCollect, lv_color_hex(0xf00000), LV_PART_MAIN | LV_STATE_FOCUSED);
lv_obj_set_style_bg_color(guider_ui.scrScan_sw_StartCollect, lv_color_hex(0xf00000), LV_PART_INDICATOR | LV_STATE_FOCUSED);
}
//绑定按键事件
lv_obj_add_event_cb(guider_ui.scrScan_sw_StartCollect, Screen_Scan_event_handler, LV_EVENT_ALL, &guider_ui);
}
///*****Screen_Scan 触发事件回调函数 *****/
static void Screen_Scan_event_handler (lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e); //获取当前事件触发的触发类型
lv_obj_t *target = lv_event_get_target(e); //获取触发该回调的控件
switch (code)
{
case LV_EVENT_VALUE_CHANGED:
{
if (target == guider_ui.scrScan_sw_StartCollect)
{
//更改sw开关LV_PART_MAIN部分LV_STATE_FOCUSED状态下的样式,保证开关的状态和显示一致
if (lv_obj_has_state(target, LV_STATE_CHECKED)) {
lv_obj_set_style_bg_color(target, lv_color_hex(0x28b620), LV_PART_MAIN | LV_STATE_FOCUSED);
lv_obj_set_style_bg_color(target, lv_color_hex(0x28b620), LV_PART_INDICATOR | LV_STATE_FOCUSED);
}
else {
lv_obj_set_style_bg_color(target, lv_color_hex(0xf00000), LV_PART_MAIN | LV_STATE_FOCUSED);
lv_obj_set_style_bg_color(target, lv_color_hex(0xf00000), LV_PART_INDICATOR | LV_STATE_FOCUSED);
}
}
break;
}
default:
break;
}
}
我们可以看到,上面对开关控件添加样式是通过本地样式的方式添加,这里我们想要修改控件绑定的本地样式,只需要再次调用一次本地样式绑定函数即可,如lv_obj_set_style_bg_color函数(),它们会直接将属性写入对象自身的局部样式缓存,不会像lv_obj_add_style() 一样叠加或堆积控件的样式,而是覆盖对应 part | state 的同一属性值。
如果你希望移除某个属性的本地样式设置,可以用:
lv_obj_remove_style(obj, NULL, part | state); // 清除该状态所有样式,包括本地样式
参考文章:
更多推荐



所有评论(0)