一,前言
继续分析按钮的创建及显示的过程。昨天很大的篇幅都是在研究lv_style_t结构体的初始化填充,简单理解就是主题默认样式的创建,它包括很多数据,都保存在*map指向的内存中。
二,应用主题源码分析
关于apply_theme中调用theme_apply函数,那么就很容易理解了。就是把lv_style_t加入到lv_style_list_t结构体的lv_style_t指针成员中。lv_style_list_t中style_cnt是6个bit长度的,说明这个list可以保存很多lv_style_t样式成员。最后还对调用lv_obj_refresh_style刷新主题,此函数在这里不展开,后面会分析
case LV_THEME_BTN:
list = lv_obj_get_style_list(obj, LV_BTN_PART_MAIN);
_lv_style_list_add_style(list, &styles->btn);
break;
......
lv_obj_refresh_style();
然后lv_theme_apply算是分析完了,又从最开始的函数btn创建开始分析,lv_btn_create先调用创建容器,然后创建容器调用创建基类对象。这个创建对象的过程真的和c++构造子类对象一样呀!学习到了,原来还可以这样设计。然后他们个初始化对象格式雷同,都是设置回调函数,然后设置对应的属性后进行主题应用。不过我个人认为,还可以再抽象些不同的内容打包为回调函数,然后用向父对象归递的方式实现lv_btn_create。因为这些调用代码展开后看上去就像是归递函数在运行呢~
lv_obj_t * lv_btn_create(lv_obj_t * par, const lv_obj_t * copy)
{
lv_obj_t * btn;
// 创建容器
btn = lv_cont_create(par, copy);
LV_ASSERT_MEM(btn);
if(btn == NULL) return NULL;
......
lv_obj_set_signal_cb(btn, lv_btn_signal);
lv_obj_set_design_cb(btn, lv_btn_design);
/*If no copy do the basic initialization*/
if(copy == NULL) {
/*Set layout if the button is not a screen*/
if(par) {
lv_obj_set_size(btn, LV_DPI, LV_DPI / 3);
lv_btn_set_layout(btn, LV_LAYOUT_CENTER);
}
lv_obj_set_click(btn, true); /*Be sure the button is clickable*/
lv_theme_apply(btn, LV_THEME_BTN);
}
}
lv_obj_t * lv_cont_create(lv_obj_t * par, const lv_obj_t * copy)
{
// 创建基类对象
lv_obj_t * cont = lv_obj_create(par, copy);
LV_ASSERT_MEM(cont);
if(cont == NULL) return NULL;
......
lv_obj_set_signal_cb(cont, lv_cont_signal);
/*Init the new container*/
if(copy == NULL) {
/*Set the default styles if it's not screen*/
if(par != NULL) {
lv_theme_apply(cont, LV_THEME_CONT);
}
}
}
这里比较关键的就是signal_cb回调函数,什么时候去调用呢!就是在lv_obj_refresh_style中会调用,一开始分析apply_theme就提及了此函数。用signal_cb来设置obj的一些属性,比如lv_cont_signal->lv_cont_refr_layout->lv_cont_layout_col->lv_obj_align
一路就是根据style设置列的布局,最后lv_obj_align中会修改obj的x,y位置。这里面等于也说到了style的使用。包括用到了obj的ext_attr属性,ext属性是属于每个obj的特色属性。ext_attr是void *,属于万能的地址链接。比如如下,不同对象申请的空间类型都不同。
lv_btn_ext_t * ext = lv_obj_allocate_ext_attr(btn, sizeof(lv_btn_ext_t));
lv_calendar_ext_t * ext = lv_obj_allocate_ext_attr(calendar, sizeof(lv_calendar_ext_t));
lv_bar_ext_t * ext = lv_obj_allocate_ext_attr(bar, sizeof(lv_bar_ext_t));
lv_canvas_ext_t * ext = lv_obj_allocate_ext_attr(new_canvas, sizeof(lv_canvas_ext_t));
代码看到现在,基本上对lv_obj_t基类结构体中的成员含义大多数能能理解了。lv_layout_t就是个uint8的枚举,因为控件对齐功能属于常用的,所以我就记录下,注释如下。其实这个signal_cb我现在立即就是一个个小的功能函数用switch case集成在一起,要使用哪个case的小功能就用signal_cb传入不同的参数。
void lv_cont_set_layout(lv_obj_t * cont, lv_layout_t layout)
{
LV_ASSERT_OBJ(cont, LV_OBJX_NAME);
// 获取当前对齐属性
lv_cont_ext_t * ext = lv_obj_get_ext_attr(cont);
if(ext->layout == layout) return;
// 若设置属性与当前不同,则使用新的对齐属性
ext->layout = layout;
// 发送信号直接处理对齐算法
/*Send a signal to refresh the layout*/
cont->signal_cb(cont, LV_SIGNAL_CHILD_CHG, NULL);
}
然后说下lv_obj_refresh_style中先调用invalidate_style_cache(obj, part, prop);
里面是设置此obj的所有属性及其子obj的所有属性为无效,这个用for循环不太好吧!干嘛要设置所有的都无效,某些要改动的设置为无效就好了呀?结果看到代码备注上已经写了,将来待优化,哈哈~
接着继续看btn创建中lv_obj_set_size设置。里面为什么要调用2次lv_obj_invalidate,我理解就是设置obj区域无效,obj怎么还分原来的obj和新的obj,再仔细想想是有道理的,因为这是设置区域,无效区域是要重绘的,删除老的大小的区域要重绘,设置完新的size的区域也要重绘。
void lv_obj_set_size(lv_obj_t * obj, lv_coord_t w, lv_coord_t h)
{
LV_ASSERT_OBJ(obj, LV_OBJX_NAME);
/* Do nothing if the size is not changed */
/* It is very important else recursive resizing can
* occur without size change*/
if(lv_obj_get_width(obj) == w && lv_obj_get_height(obj) == h) {
return;
}
/*Invalidate the original area*/
lv_obj_invalidate(obj);
/*Save the original coordinates*/
lv_area_t ori;
lv_obj_get_coords(obj, &ori);
/*Set the length and height*/
obj->coords.y2 = obj->coords.y1 + h - 1;
if(lv_obj_get_base_dir(obj) == LV_BIDI_DIR_RTL) {
obj->coords.x1 = obj->coords.x2 - w + 1;
}
else {
obj->coords.x2 = obj->coords.x1 + w - 1;
}
/*Send a signal to the object with its new coordinates*/
obj->signal_cb(obj, LV_SIGNAL_COORD_CHG, &ori);
/*Send a signal to the parent too*/
lv_obj_t * par = lv_obj_get_parent(obj);
if(par != NULL) par->signal_cb(par, LV_SIGNAL_CHILD_CHG, obj);
/*Tell the children the parent's size has changed*/
lv_obj_t * i;
_LV_LL_READ(obj->child_ll, i) {
i->signal_cb(i, LV_SIGNAL_PARENT_SIZE_CHG, &ori);
}
/*Invalidate the new area*/
lv_obj_invalidate(obj);
/*Automatically realign the object if required*/
#if LV_USE_OBJ_REALIGN
if(obj->realign.auto_realign) lv_obj_realign(obj);
#endif
}
接下来貌似初始化赋值完成了,并没有直接调用渲染绘图,渲染绘图是在周期task中统一调度的,所以我第一篇理解的说设置一个task为中优先级才会启动绘图渲染的理解是错误的,它不是触发式的,就是周期扫描,有无效区则渲染。然后就看看渲染绘图函数吧!调用关系如下
image.png
lv_refr_obj_and_children->lv_refr_obj会调用obj->design_cb,我说过2个callback是比较重要的,关于填充color值就是lv_obj_design函数。关键的渲染绘图函数如下,初始化矩形,获取初始化时候的style,然后画矩形。
lv_draw_rect_dsc_t draw_dsc;
lv_draw_rect_dsc_init(&draw_dsc);
/*If the border is drawn later disable loading its properties*/
if(lv_obj_get_style_border_post(obj, LV_OBJ_PART_MAIN)) {
draw_dsc.border_post = 1;
}
lv_obj_init_draw_rect_dsc(obj, LV_OBJ_PART_MAIN, &draw_dsc);
lv_coord_t w = lv_obj_get_style_transform_width(obj, LV_OBJ_PART_MAIN);
lv_coord_t h = lv_obj_get_style_transform_height(obj, LV_OBJ_PART_MAIN);
lv_area_t coords;
lv_area_copy(&coords, &obj->coords);
coords.x1 -= w;
coords.x2 += w;
coords.y1 -= h;
coords.y2 += h;
// 进行画布填充绘制
lv_draw_rect(&coords, clip_area, &draw_dsc);
lv_obj_init_draw_rect_dsc中就是为lv_draw_rect_dsc_t对象赋值,这里用到了很多获取style,这些style就是在初始化的时候赋值的。
void lv_obj_init_draw_rect_dsc(lv_obj_t * obj, uint8_t part, lv_draw_rect_dsc_t * draw_dsc)
{
draw_dsc->radius = lv_obj_get_style_radius(obj, part);
if(draw_dsc->bg_opa != LV_OPA_TRANSP) {
draw_dsc->bg_opa = lv_obj_get_style_bg_opa(obj, part);
if(draw_dsc->bg_opa > LV_OPA_MIN) {
draw_dsc->bg_color = lv_obj_get_style_bg_color(obj, part);
draw_dsc->bg_grad_dir = lv_obj_get_style_bg_grad_dir(obj, part);
if(draw_dsc->bg_grad_dir != LV_GRAD_DIR_NONE) {
draw_dsc->bg_grad_color = lv_obj_get_style_bg_grad_color(obj, part);
draw_dsc->bg_main_color_stop = lv_obj_get_style_bg_main_stop(obj, part);
draw_dsc->bg_grad_color_stop = lv_obj_get_style_bg_grad_stop(obj, part);
}
......
}
接着的问题就是每个像素的color在哪个函数中绘制的,继续看code,如下图
image.png
lv_draw_rect函数进入后就会涉及到framebuffer对象的填充。
draw_bg主要就是绘制矩形的背景,然后还有绘制边框的等等。为了美观一般矩形绘制都是带圆角的,这里用了mask方法来绘制矩形,最终就是带圆角的。
/*In not corner areas apply the mask only if required*/
if(y > coords_bg.y1 + rout + 1 &&
y < coords_bg.y2 - rout - 1) {
mask_res = LV_DRAW_MASK_RES_FULL_COVER;
if(simple_mode == false) {
_lv_memset(mask_buf, opa, draw_area_w);
mask_res = lv_draw_mask_apply(mask_buf, vdb->area.x1 + draw_area.x1, vdb->area.y1 + h, draw_area_w);
}
}
/*In corner areas apply the mask anyway*/
else {
_lv_memset(mask_buf, opa, draw_area_w);
mask_res = lv_draw_mask_apply(mask_buf, vdb->area.x1 + draw_area.x1, vdb->area.y1 + h, draw_area_w);
}
lv_draw_mask_apply->lv_draw_mask_radius函数。里面有圆角mask的算法。
fill_normal函数就是填充color的,这里是绘制矩形,不是划线或者画文字的填充方法。如下是软件渲染的方法一次循环赋值。
LV_ATTRIBUTE_FAST_MEM static void fill_normal(const lv_area_t * disp_area, lv_color_t * disp_buf,
const lv_area_t * draw_area,
lv_color_t color, lv_opa_t opa,
const lv_opa_t * mask, lv_draw_mask_res_t mask_res)
{
/*Get the width of the `disp_area` it will be used to go to the next line*/
int32_t disp_w = lv_area_get_width(disp_area);
int32_t draw_area_w = lv_area_get_width(draw_area);
int32_t draw_area_h = lv_area_get_height(draw_area);
// 获取画布首地址
lv_color_t * disp_buf_first = disp_buf + disp_w * draw_area->y1 + draw_area->x1;
int32_t x;
int32_t y;
// 开始填充画布,当然这里面还有判断是否mask
if(mask_res == LV_DRAW_MASK_RES_FULL_COVER) {
if(opa > LV_OPA_MAX) {
/*Software rendering*/
for(y = 0; y < draw_area_h; y++) {
lv_color_fill(disp_buf_first, color, draw_area_w);
disp_buf_first += disp_w;
}
......
}
三,总结
昨天看了style初始化及其存储的的数据结构,今天主要看了哪些样式是什么时候用的,怎么用的。因为我对风格化最感兴趣。主要就是在signal_cb函数中最一些变化进行设置,然后就是周期任务中绘制对象,画布填充color主要是lv_obj_design函数进行处理的。而画布显示到显示屏之前第一篇源码分析已经说过了,主要思路就是设置invalid区,则要重绘,然后有单framebuffer是双framebuffer(部分+完整)的3中重绘方式。
源码分析1到4系列,分析完了关于如何画一个按钮的大体流程,对于各个层次的对象结构体设计也有了一定的认识。反正我觉得在图像引擎中归递的设计方法看的比较多。然后就是看到c++的影子,比如创建子类的时候构造父类对象,以及用钩子函数充当虚方法。
网友评论