巧用lvgl的图层(layer)编写模态对话框

什么是对话框

模态对话框(Modal Dialogue Box,又叫做模式对话框),是指在用户想要对对话框以外的应用程序进行操作时,必须首先对该对话框进行响应。如单击【确定】或【取消】按钮等将该对话框关闭。一般来说,对话框分为模态对话框和非模态对话框两种。

二者的区别在于当对话框打开时,是否允许用户进行其他对象的操作。

对话框的分类

对话框分类无模式对话框和模式对话框。

无模式对话框

这是一种非强制回应的对话框,用于向用户请求非必须资料。即可以不理会这种对话框或不向其提供任何信息而继续进行当前工作,所以窗口均可打开并处于活动状态或是获得焦点(人机交互热点)。一类无模式对话框表现为工具栏,比如设置用于文字颜色的设置。查找/替换对话框可算是无模式对话框,也可以设计为工具栏。^1

模式对话框

这种对话框强制要求用户回应,否则用户不能再继续进行操作,直到与该对话框完成交互。这种对话框设计用于程序运行必须停下来,直到从用户获得一些额外的信息,然后才可以继续进行的操作,或可能只想确认使用者想要进行一项具有潜在危险性的操作。有模式对话框一般被视为坏的设计方案,因为以一种用户不喜欢看到方式出现,或是被习惯不去注意对话框提示的用户忽略,导致无法避免危险操作。^1

有模式对话框一般分为系统级和应用程序级。系统级对话框出现时,用户在完成与这个对话框交互之前不能进行其它操作,比如关机对话框、Windows Vista 中的 UAC。应用程序级的则只对它的母程序有所限制,但可能在允许运行多个实际的不同软件中有不同的表现:只限定其中的一个程序窗口使之无法操作或全部限定。^1

lvgl上的解决方案

两种对话框模式简单而言就是:

  • 无模式对话框:停留在屏幕上,随时可用,并且允许进行其他操作
  • 模式对话框:要求用户在继续程序之前做出响应

在lvgl上怎么实现这种效果呢?
非模式对话框很简单,直接让窗口弹出即可,用户可操作弹出的对话框也可以继续其他操作。
lvgl上有两个特殊的图层,通过这两个图层我们可以实现模式对话框的效果。

本文只讲解lvgl中模式对话框的实现过程。

lvgl无模式对话框效果

在这里插入图片描述

lvgl模式对话框效果

在这里插入图片描述

实现原理

lvgl的具有图层(layer)的概念,默认规则是最后创建的在最上层,一般我们都是使用在 lv_scr_act 层上面创建各种控件(widgets),每个控件其实就相当于一个个小小的图层展示在屏幕上。

在 lv_scr_act 之上还有另外两个层 layer_top 和 layer_sys,这是两个特殊的层。两者在显示器的所有屏幕上都是可见的和通用的。但是,它们不会在多个物理显示器之间共享。 layer_top 始终位于默认屏幕 (lv_scr_act()) 的顶部,而 layer_sys 位于 layer_top 的顶部。

用户可以使用 layer_top 来创建一些随处可见的内容。例如,一个菜单栏,一个弹出窗口等。如果启用了click属性,那么layer_top将吸收所有用户点击并充当模态。

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

layer_sys > layer_top > lv_scr_act

使用lvgl实现模式对话框的关键点就是利用 layer_sys 和 layer_top 的特点。也就是说创建对话框的时候在 layer_sys 和 layer_top 之上创建即可。

示例中使用到了 layer_top 实现模式对话框效果。下面是示例源码。

lvgl无模式对话框示例代码

看注释是阅读理解源码的好方法,下面给出的代码可直接在lvgl开发环境下运行。

/**
 * @file lv_100ask_demo_layer.c
 *
 */

/*********************
 *      INCLUDES
 *********************/
#include "lv_100ask_demo_layer.h"

/*********************
 *      DEFINES
 *********************/
 
 /**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/
/* 对象事件处理函数 */
static void ta_event_cb(lv_event_t * e);
static void birthday_event_cb(lv_event_t * e);
static void calendar_event_cb(lv_event_t * e);

/**********************
 *  STATIC VARIABLES
 **********************/
static lv_obj_t * bottom_bg;
static lv_obj_t * calendar;
static lv_obj_t * calendar_header;

/**********************
 *   STATIC FUNCTIONS
 **********************/
static void birthday_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * ta = lv_event_get_target(e);         // 获取事件最初指向的对象

    // 判断事件类型
    if(code == LV_EVENT_FOCUSED) {
        //获取输入设备的类型
        if(lv_indev_get_type(lv_indev_get_act()) == LV_INDEV_TYPE_POINTER) {
            if(calendar == NULL) {
                calendar = lv_calendar_create(lv_scr_act());                                    // 创建日历对象
                lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_50, 0);                            // 设置对象透明度
                lv_obj_set_style_bg_color(lv_scr_act(), lv_palette_main(LV_PALETTE_GREY), 0);   // 设置对象颜色
                lv_obj_set_size(calendar, 300, 300);                                            // 设置对象大小
                lv_calendar_set_showed_date(calendar, 1990, 01);                                // 给日历的指定打开时显示的日期
                lv_obj_align(calendar, LV_ALIGN_CENTER, 0, 30);                                 // 设置对象对齐、偏移
                lv_obj_add_event_cb(calendar, calendar_event_cb, LV_EVENT_VALUE_CHANGED, ta);   // 为对象分配事件及其处理函数

                calendar_header = lv_calendar_header_dropdown_create(lv_scr_act(), calendar);   // 创建一个包含 2 个下拉列表的标题:一个用于年份,另一个用于月份。
            }
        }
    }
}

static void calendar_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);    // 获取事件代码
    lv_obj_t * ta = lv_event_get_user_data(e);      // 获取在对象上注册事件时传递的 user_data

    // 判断事件类型
    if(code == LV_EVENT_VALUE_CHANGED) {
        lv_calendar_date_t d;
        lv_calendar_get_pressed_date(e->target, &d);    // 获取当前选中的日期
        char buf[32];

        lv_snprintf(buf, sizeof(buf), "%02d.%02d.%d", d.day, d.month, d.year);
        lv_textarea_set_text(ta, buf);

        lv_obj_del(calendar);           // 删除对象及其所有子对象
        lv_obj_del(calendar_header);    // 删除对象及其所有子对象
        calendar = NULL;
        calendar_header = NULL;
    }
    else if ((code == LV_EVENT_CLICKED))
    {
        lv_obj_del(calendar);           // 删除对象及其所有子对象
        lv_obj_del(calendar_header);    // 删除对象及其所有子对象
        calendar = NULL;
        calendar_header = NULL;
    }
}

static void ta_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);    // 获取事件代码
    lv_obj_t * ta = lv_event_get_target(e);         // 获取事件最初指向的对象
    lv_obj_t * kb = lv_event_get_user_data(e);      // 获取在对象上注册事件时传递的 user_data

    // 判断事件类型(当焦点在键盘上时)
    if(code == LV_EVENT_FOCUSED) {
        //获取输入设备的类型
        if(lv_indev_get_type(lv_indev_get_act()) != LV_INDEV_TYPE_KEYPAD) {
            lv_keyboard_set_textarea(kb, ta);                                   // 为键盘分配一个文本区域
            lv_obj_set_style_max_height(kb, LV_HOR_RES * 2 / 3, 0);             // 设置键盘的最高宽度为屏幕高度的一半
            lv_obj_update_layout(bottom_bg);                                    // 确保尺寸更新
            lv_obj_set_height(bottom_bg, LV_VER_RES - lv_obj_get_height(kb));   // 设置对象的高度,当唤出键盘时应该偏移背景
            lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN);                          // 清除标志
            lv_obj_scroll_to_view_recursive(ta, LV_ANIM_OFF);                   // 滚动到一个对象,直到它在其父对象上可见。对父母的父母做同样的事情,依此类推。即使对象具有嵌套的可滚​​动父对象,它也会滚动到视图中
        }
    }
    // 判断事件类型(当焦点不在键盘上时)
    else if(code == LV_EVENT_DEFOCUSED) {
        lv_keyboard_set_textarea(kb, NULL);
        lv_obj_set_height(bottom_bg, LV_VER_RES);
        lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);
    }
    // 判断事件类型(当键盘点击确定或取消键)
    else if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL) {
        lv_obj_set_height(bottom_bg, LV_VER_RES);
        lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);            // 标志为隐藏对象(使对象隐藏,就像它根本不存在一样)
        lv_obj_clear_state(e->target, LV_STATE_FOCUSED);    // 删除对象的一种或多种状态。其他状态位将保持不变。
        lv_indev_reset(NULL, e->target);                    // 忘记最后一次点击的对象,使其再次成为可关注的对象
    }
}

/**********************
 *   GLOBAL FUNCTIONS
 **********************/
void lv_100ask_demo_layer2(void)
{
    static lv_style_t style_title;
    static lv_style_t style_text_muted;

    bottom_bg = lv_obj_create(lv_scr_act());
    lv_obj_set_size(bottom_bg, LV_HOR_RES, LV_VER_RES);

    // 设置样式
    lv_style_init(&style_title);
    lv_style_set_text_font(&style_title, &lv_font_montserrat_20);   // 字体

    lv_style_init(&style_text_muted);
    lv_style_set_text_opa(&style_text_muted, LV_OPA_50);    // 文本透明度

    //  创建键盘
    lv_obj_t * kb = lv_keyboard_create(lv_scr_act());
    lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);    // 标志为隐藏对象(使对象隐藏,就像它根本不存在一样)

    // 创建对象并初始化
    lv_obj_t * title = lv_label_create(bottom_bg);
    lv_label_set_text(title, "Your profile");                       // 为标签设置新文本
    lv_obj_add_style(title, &style_title, 0);                       // 给对象添加样式
    lv_obj_align(bottom_bg, LV_ALIGN_TOP_LEFT, 0, 0);               // 更改对象的对齐方式并设置新坐标。

    lv_obj_t * user_name_label = lv_label_create(bottom_bg);
    lv_label_set_text(user_name_label, "User name");                // 为标签设置新文本
    lv_obj_add_style(user_name_label, &style_text_muted, 0);        // 给对象添加样式
    lv_obj_align(user_name_label, LV_ALIGN_TOP_LEFT, 0, 30);        // 更改对象的对齐方式并设置新坐标

    lv_obj_t * user_name = lv_textarea_create(bottom_bg);
    lv_textarea_set_one_line(user_name, true);                      // 单行模式
    lv_textarea_set_placeholder_text(user_name, "Your name");       // 设置文本区域的占位符文本(输入框提示文本内容)
    lv_obj_add_event_cb(user_name, ta_event_cb, LV_EVENT_ALL, kb);  // 为对象分配事件及其处理函数
    lv_obj_align(user_name, LV_ALIGN_TOP_LEFT, 0, 50);              // 更改对象的对齐方式并设置新坐标

    lv_obj_t * password_label = lv_label_create(bottom_bg);
    lv_label_set_text(password_label, "Password");                  // 为标签设置新文本
    lv_obj_align(password_label, LV_ALIGN_TOP_MID, -95, 30);        // 更改对象的对齐方式并设置新坐标。
    lv_obj_add_style(password_label, &style_text_muted, 0);         // 给对象添加样式

    lv_obj_t * password = lv_textarea_create(bottom_bg);
    lv_textarea_set_one_line(password, true);                       // 单行模式
    lv_textarea_set_password_mode(password, true);                  // 密码模式
    lv_textarea_set_placeholder_text(password, "Min. 8 chars.");    // 设置文本区域的占位符文本(输入框提示文本内容)
    lv_obj_align(password, LV_ALIGN_TOP_MID, 0, 50);                // 更改对象的对齐方式并设置新坐标
    lv_obj_add_event_cb(password, ta_event_cb, LV_EVENT_ALL, kb);   // 为对象分配事件及其处理函数

    lv_obj_t * gender_label = lv_label_create(bottom_bg);
    lv_label_set_text(gender_label, "Gender");                      // 为标签设置新文本
    lv_obj_add_style(gender_label, &style_text_muted, 0);           // 给对象添加样式
    lv_obj_align(gender_label, LV_ALIGN_TOP_LEFT, 0, 100);          // 更改对象的对齐方式并设置新坐标

    lv_obj_t * gender = lv_dropdown_create(bottom_bg);
    lv_dropdown_set_options_static(gender, "Male\nFemale\nOther");  // 设置下拉列表选项
    lv_obj_align(gender, LV_ALIGN_TOP_LEFT, 0, 120);                // 更改对象的对齐方式并设置新坐标

    lv_obj_t * birthday_label = lv_label_create(bottom_bg);
    lv_label_set_text(birthday_label, "Birthday");                  // 为标签设置新文本
    lv_obj_align(birthday_label, LV_ALIGN_TOP_MID, -95, 100);       // 更改对象的对齐方式并设置新坐标
    lv_obj_add_style(birthday_label, &style_text_muted, 0);         // 给对象添加样式

    lv_obj_t * birthdate = lv_textarea_create(bottom_bg);
    lv_textarea_set_one_line(birthdate, true);                      // 单行模式
    lv_obj_align(birthdate, LV_ALIGN_TOP_MID, 0, 120);              // 更改对象的对齐方式并设置新坐标
    lv_obj_add_event_cb(birthdate, birthday_event_cb, LV_EVENT_ALL, NULL);  // 为对象分配事件及其处理函数
}

lvgl模式对话框代码

看注释是阅读理解源码的好方法,下面给出的代码可直接在lvgl开发环境下运行。

/**
 * @file lv_100ask_demo_layer.c
 *
 */

/*********************
 *      INCLUDES
 *********************/
#include "lv_100ask_demo_layer.h"


/*********************
 *      DEFINES
 *********************/


 /**********************
 *      TYPEDEFS
 **********************/


/**********************
 *  STATIC VARIABLES
 **********************/
static lv_obj_t * bottom_bg;
static lv_obj_t * calendar;
static lv_obj_t * calendar_header;


static void ta_event_cb(lv_event_t * e);
static void birthday_event_cb(lv_event_t * e);
static void calendar_event_cb(lv_event_t * e);


static void birthday_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * ta = lv_event_get_target(e);

    if(code == LV_EVENT_FOCUSED) {
        if(lv_indev_get_type(lv_indev_get_act()) == LV_INDEV_TYPE_POINTER) {
            if(calendar == NULL) {
                lv_obj_add_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);                         // 使能 lv_layer_top 点击
                calendar = lv_calendar_create(lv_layer_top());                                  // 在 lv_layer_top 层上创建日历对象
                lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_50, 0);                          // 设置对象透明度
                lv_obj_set_style_bg_color(lv_layer_top(), lv_palette_main(LV_PALETTE_GREY), 0); // 设置对象颜色
                lv_obj_set_size(calendar, 300, 300);                                            // 设置对象大小
                lv_calendar_set_showed_date(calendar, 1990, 01);                                // 给日历的指定打开时显示的日期
                lv_obj_align(calendar, LV_ALIGN_CENTER, 0, 30);                                 // 设置对象对齐、偏移
                lv_obj_add_event_cb(calendar, calendar_event_cb, LV_EVENT_VALUE_CHANGED, ta);   // 给对象分配事件

                calendar_header = lv_calendar_header_dropdown_create(lv_layer_top(), calendar); // 创建一个包含 2 个下拉列表的标题:一个用于年份,另一个用于月份。
            }
        }
    }
}

static void calendar_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);    // 获取事件代码
    lv_obj_t * ta = lv_event_get_user_data(e);      // 获取在对象上注册事件时传递的 user_data

    // 判断事件类型
    if(code == LV_EVENT_VALUE_CHANGED) {
        char buf[32];
        lv_calendar_date_t d;
        lv_calendar_get_pressed_date(e->target, &d);        // 获取当前选中的日期

        lv_snprintf(buf, sizeof(buf), "%02d.%02d.%d", d.day, d.month, d.year);
        lv_textarea_set_text(ta, buf);  // 在文本区域展示日期信息

        lv_obj_del(calendar);           // 删除对象及其所有子对象
        lv_obj_del(calendar_header);    // 删除对象及其所有子对象
        calendar = NULL;
        calendar_header = NULL;
        lv_obj_clear_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);   // 清除标志
        lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_TRANSP, 0);  // 设置透明度
    }
    else if ((code == LV_EVENT_CLICKED))
    {
        lv_obj_del(calendar);           // 删除对象及其所有子对象
        lv_obj_del(calendar_header);    // 删除对象及其所有子对象
        calendar = NULL;
        calendar_header = NULL;
        lv_obj_clear_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);   // 清除标志
        lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_TRANSP, 0);  // 设置透明度
    }
}


static void ta_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);    // 获取事件代码
    lv_obj_t * ta = lv_event_get_target(e);         // 获取事件最初指向的对象
    lv_obj_t * kb = lv_event_get_user_data(e);      // 获取在对象上注册事件时传递的 user_data

    // 判断事件类型(当焦点在键盘上时)
    if(code == LV_EVENT_FOCUSED) {
        //获取输入设备的类型
        if(lv_indev_get_type(lv_indev_get_act()) != LV_INDEV_TYPE_KEYPAD) {
            lv_keyboard_set_textarea(kb, ta);                                   // 为键盘分配一个文本区域
            lv_obj_set_style_max_height(kb, LV_HOR_RES * 2 / 3, 0);             // 设置键盘的最高宽度为屏幕高度的一半
            lv_obj_update_layout(bottom_bg);                                    // 确保尺寸更新
            lv_obj_set_height(bottom_bg, LV_VER_RES - lv_obj_get_height(kb));   // 设置对象的高度,当唤出键盘时应该偏移背景
            lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN);                          // 清除标志
            lv_obj_scroll_to_view_recursive(ta, LV_ANIM_OFF);                   // 滚动到一个对象,直到它在其父对象上可见。对父母的父母做同样的事情,依此类推。即使对象具有嵌套的可滚​​动父对象,它也会滚动到视图中
        }
    }
    // 判断事件类型(当焦点不在键盘上时)
    else if(code == LV_EVENT_DEFOCUSED) {
        lv_keyboard_set_textarea(kb, NULL);
        lv_obj_set_height(bottom_bg, LV_VER_RES);
        lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);
    }
    // 判断事件类型(当键盘点击确定或取消键)
    else if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL) {
        lv_obj_set_height(bottom_bg, LV_VER_RES);
        lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);            // 标志为隐藏对象(使对象隐藏,就像它根本不存在一样)
        lv_obj_clear_state(e->target, LV_STATE_FOCUSED);    // 删除对象的一种或多种状态。其他状态位将保持不变。
        lv_indev_reset(NULL, e->target);                    // 忘记最后一次点击的对象,使其再次成为可关注的对象
    }
}

void lv_100ask_demo_layer(void)
{
    static lv_style_t style_title;
    static lv_style_t style_text_muted;

    bottom_bg = lv_obj_create(lv_scr_act());
    lv_obj_set_size(bottom_bg, LV_HOR_RES, LV_VER_RES);

    // 设置样式
    lv_style_init(&style_title);
    lv_style_set_text_font(&style_title, &lv_font_montserrat_20);   // 字体

    lv_style_init(&style_text_muted);
    lv_style_set_text_opa(&style_text_muted, LV_OPA_50);    // 文本透明度

    //  创建键盘
    lv_obj_t * kb = lv_keyboard_create(lv_scr_act());
    lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);    // 标志为隐藏对象(使对象隐藏,就像它根本不存在一样)

    // 创建对象并初始化
    lv_obj_t * title = lv_label_create(bottom_bg);
    lv_label_set_text(title, "Your profile");                       // 为标签设置新文本
    lv_obj_add_style(title, &style_title, 0);                       // 给对象添加样式
    lv_obj_align(bottom_bg, LV_ALIGN_TOP_LEFT, 0, 0);               // 更改对象的对齐方式并设置新坐标。

    lv_obj_t * user_name_label = lv_label_create(bottom_bg);
    lv_label_set_text(user_name_label, "User name");                // 为标签设置新文本
    lv_obj_add_style(user_name_label, &style_text_muted, 0);        // 给对象添加样式
    lv_obj_align(user_name_label, LV_ALIGN_TOP_LEFT, 0, 30);        // 更改对象的对齐方式并设置新坐标

    lv_obj_t * user_name = lv_textarea_create(bottom_bg);
    lv_textarea_set_one_line(user_name, true);                      // 单行模式
    lv_textarea_set_placeholder_text(user_name, "Your name");       // 设置文本区域的占位符文本(输入框提示文本内容)
    lv_obj_add_event_cb(user_name, ta_event_cb, LV_EVENT_ALL, kb);  // 为对象分配事件及其处理函数
    lv_obj_align(user_name, LV_ALIGN_TOP_LEFT, 0, 50);              // 更改对象的对齐方式并设置新坐标

    lv_obj_t * password_label = lv_label_create(bottom_bg);
    lv_label_set_text(password_label, "Password");                  // 为标签设置新文本
    lv_obj_align(password_label, LV_ALIGN_TOP_MID, -95, 30);        // 更改对象的对齐方式并设置新坐标。
    lv_obj_add_style(password_label, &style_text_muted, 0);         // 给对象添加样式

    lv_obj_t * password = lv_textarea_create(bottom_bg);
    lv_textarea_set_one_line(password, true);                       // 单行模式
    lv_textarea_set_password_mode(password, true);                  // 密码模式
    lv_textarea_set_placeholder_text(password, "Min. 8 chars.");    // 设置文本区域的占位符文本(输入框提示文本内容)
    lv_obj_align(password, LV_ALIGN_TOP_MID, 0, 50);                // 更改对象的对齐方式并设置新坐标
    lv_obj_add_event_cb(password, ta_event_cb, LV_EVENT_ALL, kb);   // 为对象分配事件及其处理函数

    lv_obj_t * gender_label = lv_label_create(bottom_bg);
    lv_label_set_text(gender_label, "Gender");                      // 为标签设置新文本
    lv_obj_add_style(gender_label, &style_text_muted, 0);           // 给对象添加样式
    lv_obj_align(gender_label, LV_ALIGN_TOP_LEFT, 0, 100);          // 更改对象的对齐方式并设置新坐标

    lv_obj_t * gender = lv_dropdown_create(bottom_bg);
    lv_dropdown_set_options_static(gender, "Male\nFemale\nOther");  // 设置下拉列表选项
    lv_obj_align(gender, LV_ALIGN_TOP_LEFT, 0, 120);                // 更改对象的对齐方式并设置新坐标

    lv_obj_t * birthday_label = lv_label_create(bottom_bg);
    lv_label_set_text(birthday_label, "Birthday");                  // 为标签设置新文本
    lv_obj_align(birthday_label, LV_ALIGN_TOP_MID, -95, 100);       // 更改对象的对齐方式并设置新坐标
    lv_obj_add_style(birthday_label, &style_text_muted, 0);         // 给对象添加样式

    lv_obj_t * birthdate = lv_textarea_create(bottom_bg);
    lv_textarea_set_one_line(birthdate, true);                      // 单行模式
    lv_obj_align(birthdate, LV_ALIGN_TOP_MID, 0, 120);              // 更改对象的对齐方式并设置新坐标
    lv_obj_add_event_cb(birthdate, birthday_event_cb, LV_EVENT_ALL, NULL);  // 为对象分配事件及其处理函数
}

改进

上面的代码实现的效果是标准的模式对话框的效果,我们必须完成日历的设置才能退出对话框,如果是不小心点错了或者不想现在就设置,那么应该是点击空白处就可以退出当前对话框,实现效果是这样的:

在这里插入图片描述

实现源码

基于上面模式对话框来改进代码,非常简单,我们只要在 calendar_event_cbbirthday_event_cb 函数中稍作修改即可。重点就是我们在进入模式对话框的时候,因为设置日期的对话框是在 lv_layer_top 层上创建的,所以 lv_layer_top 层也就位于设置日期对话框之下了,所以我们只要让用户点击位于对话框之下的 lv_layer_top 层的任意地方时关闭对话框即可。

看注释是阅读理解源码的好方法,下面给出的代码可直接在lvgl开发环境下运行。

static void birthday_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * ta = lv_event_get_target(e);

    if(code == LV_EVENT_FOCUSED) {
        if(lv_indev_get_type(lv_indev_get_act()) == LV_INDEV_TYPE_POINTER) {
            if(calendar == NULL) {
                lv_obj_add_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);                         // 使能 lv_layer_top 点击
                calendar = lv_calendar_create(lv_layer_top());                                  // 在 lv_layer_top 层上创建日历对象
                lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_50, 0);                          // 设置对象透明度
                lv_obj_set_style_bg_color(lv_layer_top(), lv_palette_main(LV_PALETTE_GREY), 0); // 设置对象颜色
                lv_obj_set_size(calendar, 300, 300);                                            // 设置对象大小
                lv_calendar_set_showed_date(calendar, 1990, 01);                                // 给日历的指定打开时显示的日期
                lv_obj_align(calendar, LV_ALIGN_CENTER, 0, 30);                                 // 设置对象对齐、偏移
                lv_obj_add_event_cb(calendar, calendar_event_cb, LV_EVENT_VALUE_CHANGED, ta);   // 给对象分配事件
                lv_obj_add_event_cb(lv_layer_top(), calendar_event_cb, LV_EVENT_CLICKED, NULL); // 给对象分配事件(lv_layer_top层分配点击回调处理函数)

                calendar_header = lv_calendar_header_dropdown_create(lv_layer_top(), calendar); // 创建一个包含 2 个下拉列表的标题:一个用于年份,另一个用于月份。
            }
        }
    }
}

static void calendar_event_cb(lv_event_t * e)
{
    lv_event_code_t code = lv_event_get_code(e);    // 获取事件代码
    lv_obj_t * ta = lv_event_get_user_data(e);      // 获取在对象上注册事件时传递的 user_data

    // 判断事件类型
    if(code == LV_EVENT_VALUE_CHANGED) {
        char buf[32];
        lv_calendar_date_t d;
        lv_calendar_get_pressed_date(e->target, &d);        // 获取当前选中的日期

        lv_snprintf(buf, sizeof(buf), "%02d.%02d.%d", d.day, d.month, d.year);
        lv_textarea_set_text(ta, buf);  // 在文本区域展示日期信息

        lv_obj_del(calendar);           // 删除对象及其所有子对象
        lv_obj_del(calendar_header);    // 删除对象及其所有子对象
        lv_obj_remove_event_cb(lv_layer_top(),calendar_event_cb);   // 删除对象的事件处理函数(lv_layer_top层)
        calendar = NULL;
        calendar_header = NULL;
        lv_obj_clear_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);   // 清除标志(lv_layer_top层)
        lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_TRANSP, 0);  // 设置透明度(lv_layer_top层)
    }
    else if ((code == LV_EVENT_CLICKED))
    {
        lv_obj_del(calendar);           // 删除对象及其所有子对象
        lv_obj_del(calendar_header);    // 删除对象及其所有子对象
        lv_obj_remove_event_cb(lv_layer_top(),calendar_event_cb);   // 删除对象的事件处理函数(lv_layer_top层)
        calendar = NULL;
        calendar_header = NULL;
        lv_obj_clear_flag(lv_layer_top(), LV_OBJ_FLAG_CLICKABLE);   // 清除标志(lv_layer_top层)
        lv_obj_set_style_bg_opa(lv_layer_top(), LV_OPA_TRANSP, 0);  // 设置透明度(lv_layer_top层)
    }
}

更近一步的改进

这里的改进是对于对话框之外的改进,不知道读者发现没有,我们布局编写的代码非常低级糟糕的,其实我们不用这么麻烦,lvgl 8.x 中我们可以使用两种布局:

  • Flex(弹性布局)
  • Grid(网格布局)

通过这两种布局我们可以很方便的管理我们的ui布局。这部分的内容超出了本文的范围,感兴趣的读者可以关注阅读我的下一篇文章:
//TODO