一,前言
之前的4篇littlevgl源码分析已经完成一个按钮的显示的流程的分析。但是littlevgl要比我之前看的5K行的guilite做的控件要漂亮,比如这个按钮,他有各种样式,包括圆角矩形按钮,因为我看了下guilite源码好像是不支持绘制圆角矩形的。之前是分析框架,今天则分析细节的特色功能。
二,分析lvgl是如何绘制带圆角的矩形
首先我要提下mask的概念,在一副图片上,加上100%的mask遮盖后,你再给他绘制原色,那么遮盖的部分是不会改变颜色的。而把mask的概念理解为透明度叠加也可以。2个颜色若是透明的,那么就是2个颜色的rgb相加,否则就是后面绘制的颜色。比如先画蓝色,后画白色,不透明的话就是白色,若是透明的话,加入白色后的效果就是蓝色变淡蓝色。
好了,那么lvgl中若颜色完全覆盖,则用0xff来mask这个像素点,若完全透明就是0,在0~0xff间的就代表存在不同层度的透明度。而绘制圆角矩形的原理就是利用圆角mask。
之前学习游戏中的刚体碰撞了解过圆形和矩形碰撞,可以计算圆点到矩形边界的距离。大于r则无碰撞,否则有碰撞。那么要构造一个圆角矩阵,也是可以理解为每个圆角都是一个圆,见下图,那么要把下图的4个白色区间为透明,其它涂色,则变成了一个圆角矩形。
圆角矩形绘制的方法就是描点法。来看代码,要绘制矩形,就是一行行绘制。然后每行设置mask,当mask为0则为透明,就等于上图的白色区间。我现在将源码修改了下,屏蔽了绘制矩形轮廓等,仅绘制矩形背景,背景色为白色。此时周期刷新函数会调用draw_bg函数进行绘制。
LV_ATTRIBUTE_FAST_MEM static void draw_bg(const lv_area_t * coords, const lv_area_t * clip,
const lv_draw_rect_dsc_t * dsc)
{
。。。。。。
// mask数据准备,为每一行准备mask
for(h = draw_area.y1; h <= draw_area.y2; h++) {
int32_t y = h + vdb->area.y1;
opa2 = opa;
/*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*/
// 若有圆角,色申请mask空间。mask空间为这一行的长度
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_blend_fill(xxx) //rgb数据混合及渲染
}
然后看看lv_draw_mask_apply算法的使用,它会调用res = dsc->cb(mask_buf, abs_x, abs_y, len, (void *)m->param);
等于进入lv_draw_mask_radius函数。lv_draw_mask_radius里面做的事情就是先计算点的坐标,通过半径及这一个行y和y-1行的坐标计算x和x-1的坐标。如下
y = radius - (h - abs_y) + 1;
/* Get the x intersection points for `abs_y` and `abs_y-1`
* Use the circle's equation x = sqrt(r^2 - y^2)
* Try to use the values from the previous run*/
if((y - 1) == p->y_prev) {
x1.f = p->y_prev_x.f;
x1.i = p->y_prev_x.i;
}
else {
_lv_sqrt(r2 - ((y - 1) * (y - 1)), &x1, sqrt_mask);
}
_lv_sqrt(r2 - (y * y), &x0, sqrt_mask);
p->y_prev = y;
p->y_prev_x.f = x0.f;
p->y_prev_x.i = x0.i;
然后为x和x-1的坐标添加mask渐变过滤。Kl和Kr就是左边和右边的像素位置,所以用mask_buf[kl]和mask_buf[kr]来表示,mask填充的值为m。
/*Set all points which are crossed by the circle*/
for(; i <= x1.i; i++) {
/* These values are very close to each other. It's enough to approximate sqrt
* The non-approximated version is lv_sqrt(r2 - (i * i), &y_next, sqrt_mask); */
sqrt_approx(&y_next, &y_prev, r2 - (i * i));
m = (y_prev.f + y_next.f) >> 1;
if(outer) m = 255 - m;
if(kl >= 0 && kl < len) mask_buf[kl] = mask_mix(mask_buf[kl], m);
if(kr >= 0 && kr < len) mask_buf[kr] = mask_mix(mask_buf[kr], m);
kl--;
kr++;
y_prev.f = y_next.f;
}
调试截图如下,x-1和x相差6。
然后除了2边对称需要设置mask,中间点也要设置mask。
if(y_prev.f) {
m = (y_prev.f * x1.f) >> 9;
if(outer) m = 255 - m;
if(kl >= 0 && kl < len) mask_buf[kl] = mask_mix(mask_buf[kl], m);
if(kr >= 0 && kr < len) mask_buf[kr] = mask_mix(mask_buf[kr], m);
kl--;
kr++;
}
最后再设置x和x-1没有交叉的部分,直接设置为0。
if(outer == 0) {
kl++;
if(kl > len) {
return LV_DRAW_MASK_RES_TRANSP;
}
if(kl >= 0) _lv_memset_00(&mask_buf[0], kl);
if(kr < 0) {
return LV_DRAW_MASK_RES_TRANSP;
}
if(kr < len) _lv_memset_00(&mask_buf[kr], len - kr);
}
交叉的部分可以理解为是要描点绘制曲线的,所以用了渐变算法,否则的话会看到明显的锯齿。outer应该理解为是绘制里面或者绘制外部吧!见下图,对每一行矩形其实都是这样kl和kr的对称mask赋值。分为3个阶段赋值,一个是曲线段,一个是中间点,最后是曲线外。
我把在绘图板中画的一个圆,进行了5被扩大。让大家可以理解下描点与锯齿。下图第一行y和下一行y-1对应的的x和x-1也差距6。看坐标一个是30,一个是24。
我把lvgl绘制出的按钮背景的左上角矩形圆角截图,然后把截图放大8倍,也可以看到渐变的锯齿效果。其实就是这段代码的作用啦~标准尺寸的时候看上去就很平滑了。
三,总结
主要了解了下圆角矩形的绘制思路,简单来说就是用行扫描法加上形状mask,用渐变的mask值让描点的曲线效果更平滑。