数码相册——设置时间间隔界面的显存管理、页面规划、输入控制
- 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
- 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
- 参考资料:《嵌入式Linux应用开发手册》、《嵌入式Linux应用开发手册第2版》
- 开发环境:Linux 3.4.2内核、arm-linux-gcc 4.3.2工具链
- 源码仓库:https://gitee.com/d_1254436976/Embedded-Linux-Phase-3
目录
一、前言
在【1.10 数码相册—main_page主界面的显存管理、页面规划、输入控制】中,实现了main_page主界面的显示与按键控制,在这篇博文中,实现setting_page
设置界面的测试,实现如下目的:
- 有效的管理内存,实现页面的快速刷新;
- 合理的规划页面空间,达到较好的显示效果;
- 有效的输入区域,实现触控的效果。
二、interval_page页面的实现
由于需要程序改动的地方过多,所以这篇博文会比较侧重于描述思想与方法。
1、关键的结构体
/* 描述图片信息 */
typedef struct PixelDatas{
int bpp; //图片的bpp
int width; //图片的宽度
int height; //图片的高度
int linebytes; //图片每行占据的字节数
int TotalBytes; //图片总大小
unsigned char *PixelDatas; //存储图片具体像素数据的首地址
}T_PixelDatas, *PT_PixelDatas;
/* 页面内存数据是否被使用 */
typedef enum {
VMS_FREE = 0, //空闲
VMS_USED_FOR_PREPARE, //子线程占用
VMS_USED_FOR_CURMAIN, //当前主线程占用
}E_VideoMemState;
/* 页面内存数据是否已准备好 */
typedef enum {
PS_BLANK = 0, //数据空白
PS_GENERATING, //数据生成中
PS_GENERATED, //数据完毕
}E_PicState;
/* 图标布局信息 */
typedef struct Layout {
char *strIconName; //图标的名字
int TopLeftX; //图标左上角x坐标
int TopLeftY; //图标左上角y坐标
int BotRightX; //图标右下角x坐标
int BotRightY; //图标右下角x坐标
}T_Layout, *PT_Layout;
/* 页面图标信息 */
typedef struct PageLayout {
int bpp; //显示页面的bpp
int MaxTotalBytes; //最大图标的大小
PT_Layout ptLayout; //指向描述该页面图标数组的指针
}T_PageLayout, *PT_PageLayout;
/* 描述页面内存块 */
typedef struct VideoMem {
int id; //页面内存的id
int isDevFB; //当前描述的内存是否为Framebuffer:1-是,2-否
E_VideoMemState eVideoMemState; //描述该页面内存数据的使用状态
E_PicState ePicState; //描述该页面内存数据是否已准备好
T_PixelDatas tPixelDatas; //描述图片信息
struct VideoMem *ptNext; //指向下一结点的指针
}T_VideoMem, *PT_VideoMem;
/* 描述页面行为 */
typedef struct PageAction {
char *name;
int (*Run)(void);
int (*GetInputEvent)(PT_PageLayout ptPageLayout, PT_InputEvent ptInputEvent);
int (*Prepare)(void);
struct PageAction *ptNext;
}T_PageAction, *PT_PageAction;
2、显存管理
2.1 思想
对于页面的显示,LCD与Framebuffer之间通过LCD控制器来进行像素信息的传递,当我们需要显示图片时,只需要在Framebuffer
中写入颜色信息就可以在LCD上显示出来。
当应用程序过大,会导致显示的很慢,此时需要优化措施:
- 在内存中开辟一个与
Framebuffer
大小相同的内存空间; - 提前把需要显示的内容写到新开辟的内存空间中;(3、页面规划与显示中介绍)
- 需要显示时,直接把新开辟的内存空间
memcpy
到Framebuffer
中。(3、页面规划与显示中介绍)
2.2 具体的实现
- 步骤描述:
/*---------------------------显存管理----------------------------------------*/
页面管理内存 = 页面描述信息 + 显存
int AllocVideoMem(int num)函数:
1. 获得LCD显示设备的分辨率以及bpp,计算得到需要开辟内存空间的大小(单位:byte)以及行宽(单位:byte)
2. 先把设备本身的Framebuffer放入链表
2.1 开辟一个大小为sizeof(T_VideoMem)内存,用来存放页面描述信息,地址放在ptNew结构体指针中
2.2 根据1中获取的信息设置该内存的描述信息,以及把Framebuffer的地址放入结构体变量中
2.3 强制设置其eVideoMemState状态位为占用状态
2.4 采用头插法插入链表
3. 后根据num分配用于页面管理的内存并设置结构体
3.1 开辟一个大小为sizeof(T_VideoMem) + VMSize内存,用来存放页面描述信息与显存,地址放在ptNew结构体指针中
3.2 根据1获得的信息,设置该内存的描述信息与显存的地址
3.3 采用头插法插入链表
- 代码实现:
int AllocVideoMem(int num)
{
int i;
int bpp;
int linebytes;
int xres, yres;
int VMSize;
PT_VideoMem ptNew;
PT_VideoMem ptTmp;
bpp = yres = xres = 0;
GetDispResolution(&xres, &yres, &bpp);
VMSize = xres * yres * bpp / 8;
linebytes = xres * bpp / 8;
/* 1、先把设备本身的Framebuffer放入链表 */
ptNew = (PT_VideoMem)malloc(sizeof(T_VideoMem));
if (ptNew == NULL) {
DebugPrint(APP_ERR"Framebuffer set fail!\n");
goto fail;
}
/* 设置该内存所描述页面的信息 */
ptNew->id = 0;
ptNew->isDevFB = 1;
ptNew->eVideoMemState = VMS_FREE;
ptNew->ePicState = PS_BLANK;
ptNew->tPixelDatas.bpp = bpp;
ptNew->tPixelDatas.height = yres;
ptNew->tPixelDatas.width = xres;
ptNew->tPixelDatas.linebytes = linebytes;
ptNew->tPixelDatas.PixelDatas = s_ptDefaultDispOpr->pDispMem;
ptNew->tPixelDatas.TotalBytes = VMSize;
/* 强制设置设备本身的Framebuffer状态为被占用 */
if (num != 0)
ptNew->eVideoMemState = VMS_USED_FOR_CURMAIN;
/* 头插法插入链表 */
ptNew->ptNext = s_ptVieoMemHead;
s_ptVieoMemHead = ptNew;
/* 2、后分配用于页面管理的内存并设置结构体,组成:页面描述 + 显存 */
for (i = 0; i < num; i++) {
ptNew = (PT_VideoMem)malloc(sizeof(T_VideoMem) + VMSize);
if (ptNew == NULL) {
DebugPrint(APP_ERR"ptNew malloc fail,already malloc num: %d!\n", i);
goto fail;
}
/* 设置该内存所描述页面的信息 */
ptNew->id = 0;
ptNew->isDevFB = 0;
ptNew->eVideoMemState = VMS_FREE;
ptNew->ePicState = PS_BLANK;
ptNew->tPixelDatas.bpp = bpp;
ptNew->tPixelDatas.height = yres;
ptNew->tPixelDatas.width = xres;
ptNew->tPixelDatas.linebytes = linebytes;
ptNew->tPixelDatas.PixelDatas = (unsigned char *)(ptNew + 1);
ptNew->tPixelDatas.TotalBytes = VMSize;
/* 头插法插入链表 */
ptNew->ptNext = s_ptVieoMemHead;
s_ptVieoMemHead = ptNew;
}
return 0;
fail:
while (s_ptVieoMemHead) {
ptTmp = s_ptVieoMemHead->ptNext;
free(s_ptVieoMemHead);
s_ptVieoMemHead = ptTmp;
}
return -1;
}
3、页面规划与显示
3.1 思想
- 页面规划:
为了适应多种尺寸的LCD设备以及良好的视觉感受,所以图标的显示如下,尺寸计算可以适当:(不通用于其他页面,每个页面都需要制定不同的方案)
注意:由于该页面的特殊性:需要在图标中的指定区域进行数字变换与长按实现快速加减,所以会使用Freetype库的字符编码和timval
结构体记录时间。
- 显示:
对于多个页面的显示与切换,需要大致做到以下步骤:
1、获得该页面的显存;
2、描画该页面内存的显示数据;
3、刷新到显示设备的Framebuffer
中。
3.2 具体的实现
- 步骤描述:
1.1 获得显存
1.2 描画数据
需要判断该页面的图标坐标信息是否已经计算完毕:
若是则表示数据已经准备好,则直接进行步骤1.3
否则进行计算各图标坐标信息如下措施:
1.2.2 获得LCD显示设备的分辨率以及bpp
1.2.3 根据事先约定的LCD上的页面规划,进行图标的尺寸计算
1.2.4 绘制图标中的数字
1.2.5 清空屏幕
1.2.6 根据TotalBytes的数据来malloc分配存储LCD显示的图片数据的内存,把内存的首地址存储到页面内存的PixelDatas
1.2.7 描绘该页面的需要显示的图标源bmp信息
1.2.7.1 调用GetPixelDatasForIcon(),获取每个图标的bmp像素信息
1.2.7.2 调用PicZoom(),根据1.2.3中获得的LCD显示的图标信息与1.2.7.1获得的源bmp文件信息,采用近邻取样插值,对LCD显示的图标的像素信息进设置
1.2.7.3 调用PicMerge(),根据1.2.7.2获得的LCD显示的图标信息,把数据整合到1.2.6中的页面内存的PixelDatas所指向的内存中
1.2.7.4 调用FreePixelDatasForIcon(),释放1.2.7.1中所开辟的内存
移动到下一个图标的在LCD中显示的位置
1.2.8 释放1.2.6中所开辟的内存,并更新该页面的ePicState页面数据状态位为数据完毕位
1.3 刷新设备并解放显存
- 改进:
1、在结构体内部定义另一结构体指针,方便管理该页面图标的信息
2、对于每个页面需要使用到的公共部分定义为函数
3、做了信息的判断,防止每个页面界面进行切换时重复计算该页面图标的坐标
/* 定义结构体数组存储该页面的每个图标 */
static T_Layout s_tIntervalPageIconLayout[] = {
{"inc.bmp", 0, 0, 0, 0},
{"time.bmp", 0, 0, 0, 0},
{"dec.bmp", 0, 0, 0, 0},
{"ok.bmp", 0, 0, 0, 0},
{"cancel.bmp", 0, 0, 0, 0},
{NULL, 0, 0, 0, 0}, //结尾标志
};
/* 定义一个新的结构体存储,成员变量指向该页面所有图标信息的结构体地址 */
static T_PageLayout s_tIntervalPageLayout = {
.MaxTotalBytes = 0,
.ptLayout = s_tIntervalPageIconLayout,
};
- 函数的实现:
static void ShowIntervalPage(PT_PageLayout ptPageLayout)
{
int error;
PT_VideoMem ptVideoMem;
PT_Layout ptLayout;
ptLayout = ptPageLayout->ptLayout;
/* 获得显存 */
ptVideoMem = GetVideoMem(ID("Interval"), 1);
if (ptVideoMem == NULL) {
DebugPrint(APP_ERR"Can not get video mem for Interval_page!\n");
return ;
}
/* 描画数据 */
/* 如果还没有计算过各图标的坐标 */
if (ptLayout[0].TopLeftX == 0)
CalcIntervalPageLayout(ptPageLayout);
/* 绘制图标中的数字 */
error = GenerateIntervalPageSpecialIcon(s_IntervalSecond, ptVideoMem);
if (error)
DBG_PRINTF(APP_ERR"GenerateIntervalPageSpecialIcon error!\n");
/* 页面数据的描绘 */
error = GeneratePage(ptPageLayout, ptVideoMem);
/* 刷新/加载到设备 */
FlushVideoMemToDev(ptVideoMem);
/* 设置页面内存为空闲状态 */
PutVideoMem(ptVideoMem);
}
4、触摸输入的设置
4.1 思想
对于页面中需要显示的图标,每个图标都有对应的有效触摸区域。当用户触摸到对应的区域时,进入不同的页面(未实现,下篇博客实现)且该图标的颜色变化。
由于这里使用到输入系统,即【1.7 数码相册—电子书(5)—多线程支持多输入】中介绍的,会使用到触摸屏子线程与tslib库。
4.2 具体实现
- 步骤描述:
2.1 IntervalPageGetInputEvent(),获得该页面的输入事件与对应图标的索引,
由于调用到之前的输入事件子线程,所以在这里程序会处于休眠,有输入事件才唤醒
2.2 若输入事件得到的压力值为0(松开) 且 pressured==1曾经按下,则调用ReleaseButton(),根据indexPressured,把图标颜色取反
2.2.1 根据按键的索引进入不同的页面
2.2.1.1 indexPressured == 0,inc按钮,对应计数值加1,把该数值显示在LCD对应图标上
2.2.1.2 indexPressured == 2,dec按钮,对应计数值减1,把该数值显示在LCD对应图标上
2.2.1.3 indexPressured == 3,ok按钮
2.2.1.4 indexPressured == 4,cancel按钮
2.2.1.5 其他则不操作
2.3 若输入事件得到的压力值为1(按下) 且 已经获得了按键的index索引 且 pressured==0未按下,则表示按键按下未松开
2.3.1 记录该事件的时间值到tInputEventPrePress.timeval结构体中,设置标志位并图标颜色取反
2.3.2 比较tInputEventPrePress与tInputEvent两次事件中的timeval结构体的时间差,小于50ms则判断为长按,数值持续加或减
并不断刷新该数值显示在LCD对应图标上
- 代码实现:
while (1) {
index = IntervalPageGetInputEvent(&s_tIntervalPageLayout, &tInputEvent);
/* 松开和按下不在同一个图标范围内 */
if (tInputEvent.pressure == 0) {
/* 曾经有按键按下 */
if (pressured) {
ReleaseButton(&s_tIntervalPageIconLayout[indexPressured]);
pressured = 0;
/* 松开与按下的按键为同一个 */
if (indexPressured == index) {
switch (indexPressured) {
case 0: /* inc按钮 */
intervalSecond++;
if (intervalSecond == 60)
intervalSecond = 0;
/* 在指定区域显示数字 */
GenerateIntervalPageSpecialIcon(intervalSecond, ptDevVideoMem);
break;
case 2: /* dec按钮 */
intervalSecond--;
if (intervalSecond == -1)
intervalSecond = 59;
/* 在指定区域显示数字 */
GenerateIntervalPageSpecialIcon(intervalSecond, ptDevVideoMem);
break;
case 3: /* ok按钮 */
return 0;
break;
case 4: /* cancel按钮 */
return 0;
break;
default:
break;
}
}
indexPressured = -1;
}
} else {
/* 松开和按下都在同一个图标范围内 */
/* 按下 */
if (index != -1) {
/* 之前未按下 */
if (!pressured && index != -1) {
pressured = 1;
indexPressured = index;
tInputEventPrePress = tInputEvent; /* 记录下来 */
PressButton(&s_tIntervalPageIconLayout[indexPressured]);
}
/* 如果按下的是"inc.bmp"或"dec.bmp"
* 连按2秒后, 飞快的递增或减小: 每50ms变化一次
*/
if ((indexPressured == 0) || (indexPressured == 2)) {
if (fast && (TimeMSBetween(tInputEventPrePress.time, tInputEvent.time) > 50))
{
intervalSecond = indexPressured ? (intervalSecond - 1) : (intervalSecond + 1);
if (intervalSecond == 60)
intervalSecond = 0;
else if (intervalSecond == -1)
intervalSecond = 59;
/* 在指定区域显示数字 */
GenerateIntervalPageSpecialIcon(intervalSecond, ptDevVideoMem);
tInputEventPrePress = tInputEvent;
}
if (TimeMSBetween(tInputEventPrePress.time, tInputEvent.time) > 2000) {
fast = 1;
tInputEventPrePress = tInputEvent;
}
}
}
}
}
5、页面的切换
对于main_page
主页面、setting_page
设置页面与interval_page设置时间间隔界面
的切换,三者通过在对应页面的GetInputEvent()
函数,获取当前按下图标的索引值,根据索引值执行Page("xxx")->Run();
进入到不同页面。
main_page
主页面:
switch (indexPressured) {
case 2: /* setting_page */
/* 设置页面 */
Page("setting")->Run();
/* 返回主页面 */
ShowMainPage(&s_tMainPageLayout);
break;
default:
break;
}
setting_page
设置页面:
switch (indexPressured) {
case 1: /* interval按钮 */
/* 设置时间间隔页面 */
Page("interval")->Run();
/* 返回设置页面 */
ShowSettingPage(&s_tSettingPageLayout);
break;
case 2: /* 返回按键 */
return 0;
default:
break;
}
interval_page
设置时间间隔界面:
switch (indexPressured) {
case 0: /* inc按钮 */
intervalSecond++;
if (intervalSecond == 60)
intervalSecond = 0;
/* 在指定区域显示数字 */
GenerateIntervalPageSpecialIcon(intervalSecond, ptDevVideoMem);
break;
case 2: /* dec按钮 */
intervalSecond--;
if (intervalSecond == -1)
intervalSecond = 59;
/* 在指定区域显示数字 */
GenerateIntervalPageSpecialIcon(intervalSecond, ptDevVideoMem);
break;
case 3: /* ok按钮 */
return 0;
break;
case 4: /* cancel按钮 */
return 0;
break;
default:
break;
}
三、编译与运行
1、编译
执行make
生成可执行文件digitpic
文件
2、运行
- 可执行文件
digitpic
放入到根文件系统 - icon目录下的图标文件放入到根文件系统的
/etc/digitpic/icons
(程序打开图标文件时需要用到) - 执行
./digitpic MSTH.TTF
,得到以下的效果:
1、程序一开始进入main_page
主界面,按下与松开3个图标的位置,图标的颜色会反转
当按下设置按钮时,进入setting_main
设置界面
按下返回按键后可以返回main_page
主页面。
2、在设置页面的时候按下与松开3个图标的位置,图标的颜色会反转
当按下设置时间间隔按钮时,进入interval_main
设置界面,点击⬆或⬇按钮实现数字加减,长按实现快速加减。