【说在前面的话】
你想知道这个仪表界面是怎么实现的吗?
哈哈,下面我们就从原理讲起一步一步实现这个酷炫的界面。在讲制作原理之前,有必要先说一下这个板子的软硬件配置,如下
单片机 | syd8810(m0内核)主频64M |
屏幕 | 240*240像素、spi接口 |
Arm-2D的PFB大小 | 240*30 |
MDK编译优化等级 | -Omax |
配置讲完之后,就来看看我们是怎么实现这个酷炫的界面。
【表针的旋转】
首先,仪表盘中有一个表针,看到表针那肯定就需要旋转了,Arm-2D的旋转我们之前已经讲过了,不清楚的可以看下面这篇文章
值得一提的就是之前我们讲的旋转圆心的坐标都是在素材里面,其实圆心也可以在素材的外面,如下图所示
当我们把表针的旋转圆心设置成(3,50)即在素材的外面,它就会沿着半径为50的圆进行旋转,和视频中看到的效果一样。
哈哈,当然没有这么简单。如果只让表针旋转,这样就可以了,但是,如果在资源紧张的单片机(比如m0的内核)中,实现表针的流畅旋转,还需要一些特殊的技巧,那就是我们之前讲的脏矩阵(即局部刷新)。
【脏矩阵的使用】
视频中,我们很容易发现表针的旋转区域为橙色半圆环的区域,如下图所示
所以我们只要刷新这片区域就可以了,不需要整个屏幕全刷。
那怎么把旋转区域设置成半圆环呢?
哈哈,这个是办不到的,因为Arm-2D的脏矩阵只能设置成矩形区域。
不过,有了这个矩形区域,我们就可以实现表针的流畅旋转了,因为我们可以动态地修改这个区域,如下图所示:
图中,表针从位置1运动到位置2,我们只需要把脏矩阵的区域设置成位置1和位置2这两个矩形区域就可以了。位置1的区域是为了擦除原来的表针,而位置2的区域就是绘制表针。这个就是视频中表针旋转非常流畅的原因,因为我们每次只刷了两块小区域。
那么,问题又来了,位置2的区域该怎么计算呢?
这个也简单,这个动态更新的脏矩阵区域Arm-2D已经为我们计算好了,我只要简单地使用就可以了,下面就讲一下怎么用Arm-2D来动态更新旋转区域,让我们的旋转更流畅。
【从Arm-2D提供的例子模板开始】
使用Arm-2D来实现视频中的旋转表针也很简单(官方已经提供了模板),如下图
然后添加Meter模板就可以,如下图所示
此时,我们的工程就会出现这两个文件,如下图
这时我们就把模板添加成功了,在主函数中调用就可以,如下
int main(){
...
arm_2d_scene_meter_init(&DISP0_ADAPTER);
...
}
因为meter模板也是一个scene,所以和scene的使用是一样的。
到这里,视频中表针的旋转就实现了,是不是很简单。
【transform helper的使用】
模板中让指针流畅运动的关键是一个名为transform helper的服务,它以极傻瓜的方式实现了前面文中介绍过的“跟着指针走的动态脏矩阵”。关于这个Transform Helper 几点注意事项需要说一下:
一、这里用 transform 的时候,一定要有一个独立的 OP,我们可以根据具体使用的transform类型在场景类中添加:
/*!
* \brief a user class for scene meter
*/
typedef struct user_scene_meter_t user_scene_meter_t;
struct user_scene_meter_t {
...
ARM_PRIVATE(
/* place your private member here, following two are examples */
...
struct {
/* transform 的 OP */
arm_2d_op_fill_cl_msk_opa_trans_t tOP;
arm_2d_helper_dirty_region_transform_t tHelper;
} Pointer;
)
/* place your public member here */
};
且一定要用 ARM_2D_OP_INIT 初始化一下,如下所示
/* initialize op */
ARM_2D_OP_INIT(this.Pointer.tOP);
二、初始化完OP后,接着就是初始化transform helper。
首先,我们要确保打开场景自带的 Dirty Region Helper 服务。在场景初始化函数中,我们可以找到以下的内容:
ARM_NONNULL(1)
user_scene_meter_t *__arm_2d_scene_meter_init( arm_2d_scene_player_t *ptDispAdapter,
user_scene_meter_t *ptThis)
{
...
*ptThis = (user_scene_meter_t){
.use_as__arm_2d_scene_t = {
...
/* Please uncommon the callbacks if you need them
*/
.fnOnLoad = &__on_scene_meter_load,
.fnScene = &__pfb_draw_scene_meter_handler,
//.fnOnBGStart = &__on_scene_meter_background_start,
//.fnOnBGComplete = &__on_scene_meter_background_complete,
.fnOnFrameStart = &__on_scene_meter_frame_start,
.fnDepose = &__on_scene_meter_depose,
...
.bUseDirtyRegionHelper = true,
},
...
};
...
return ptThis;
}
这里的 bUseDirtyRegionHelper 就是我们的目标,请将其设置为 true。
接下来在“场景载入事件处理程序(fnOnLoad)”中,添加transform helper的初始化代码:
static void __on_scene_meter_load(arm_2d_scene_t *ptScene)
{
user_scene_meter_t *ptThis = (user_scene_meter_t *)ptScene;
ARM_2D_UNUSED(ptThis);
/* initialize transform helper */
arm_2d_helper_dirty_region_transform_init(
&this.Pointer.tHelper,
&ptScene->tDirtyRegionHelper,
(arm_2d_op_t *)&this.Pointer.tOP,
0.01f,
0.1f);
}
其中0.01f是设置旋转角度的门限值,也就是旋转角度大于0.01f时才会重新绘制表针。这个在Arm-2D内部使用了双缓冲技术,不仅可以让用户在任意地方安全的更新角度,而且使得旋转效率进一步提升,真是为了旋转的性能无所不用其极啊(* ̄︶ ̄)
同样的,0.1f是设置图片缩放的门限值,其内部也使用了双缓冲技术
这里,我们将scene自带的Dirty Region Helper对象(ptScene->tDirtyRegionHelper)传递给了初始化函数——完成了关联。
三、当我们使用完OP后(界面切换走),记得要把它释放掉,否则会导致内存泄漏,程序如下
static void __on_scene_meter_depose(arm_2d_scene_t *ptScene){
...
/* depose op */
arm_2d_helper_dirty_region_transform_depose(
&this.Pointer.tHelper);
/* depose op */
ARM_2D_OP_DEPOSE(this.Pointer.tOP);
...
}
四、在场景模板的 __on_scene_xxxx_start() 处理程序的最后插入transform_helper 对应的时间处理程序 arm_2d_helper_dirty_region_transform_on_frame_start() 例如:
static void __on_scene_meter_frame_start(arm_2d_scene_t *ptScene)
{
user_scene_meter_t *ptThis = (user_scene_meter_t *)ptScene;
...
/* call helper's on-frame-begin event handler */
arm_2d_helper_dirty_region_transform_on_frame_start(&this.Pointer.tHelper);
}
static void __on_scene_meter_frame_start(arm_2d_scene_t *ptScene)
{
/* 你的用来更新角度的代码,保存在iResult里上 */
float fAngle = ARM_2D_ANGLE((float)iResult / 10.0f);
/* update helper with new values*/
arm_2d_helper_dirty_region_transform_update_value(
&this.Pointer.tHelper,
fAngle,
1.0f);
/* call helper's on-frame-begin event handler */
arm_2d_helper_dirty_region_transform_on_frame_start(&this.Pointer.tHelper);
}
定义一个变量fAngle 来存放要更新的角度值(注意是弧度)
然后调用arm_2d_helper_dirty_region_transform_update_value函数更新角度。注意第3个参数为缩放倍数,1.0f即原图大小,不进行缩放。
角度更新完成后,就可以调用旋转函数对表针进行旋转了,如下所示
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_meter_handler)
{
...
arm_2d_align_centre(__canvas, c_tileMeterPanel.tRegion.tSize) {
/* draw pointer */
arm_2dp_fill_colour_with_mask_opacity_and_transform(
&this.Pointer.tOP,
&c_tilePointerMask,
ptTile,
NULL, //&__centre_region,
s_tPointerCenter,
this.Pointer.tHelper.fAngle,
this.Pointer.tHelper.fScale,
GLCD_COLOR_RED,
255);
arm_2d_helper_dirty_region_transform_update(
&this.Pointer.tHelper,
&__centre_region,
bIsNewFrame);
arm_2d_op_wait_async((arm_2d_op_core_t *)&this.Pointer.tOP);
}
}
我们使用arm_2dp_fill_colour_with_mask_opacity_and_transform函数对表针进行旋转。
旋转完成后记得调用arm_2d_helper_dirty_region_transform_update()函数来更新脏矩阵,此函数必须放在自己所要服务的 transform 操作函数的后面哦。
【修改 meter scene模板的图片资源】
那该怎么修改模板例子中的背景图片呢?
具体来说,我们需要:
使用 img2c.py 将新的背景转换成 arm-2d 可以使用的 tile 对象;
按照屏幕的颜色格式把 c_tileMeterPanelGRAY8、c_tileMeterPanelRGB565或者c_tileMeterPanelCCCA8888替换成新生成的tile
下面是一个替换了背景后的效果,看起来不错吧?
答案是肯定的:我们只需要定义一个叫做c_tilePointerMask的宏, 并把它跟我们自己的指针关联起来就行:
/* 背景图片的宏 */
/* 指针Mask的宏 */
我们把c_tilePointerMask替换成我们自己的表针素材就可以了,如果要更改指针的旋转半径,可以修改__arm_2d_scene_meter_init()的代码:
ARM_NONNULL(1)
user_scene_meter_t *__arm_2d_scene_meter_init( arm_2d_scene_player_t *ptDispAdapter,
user_scene_meter_t *ptThis)
{
...
/* initialize op */
ARM_2D_OP_INIT(this.Pointer.tOP);
...
s_tPointerCenter.iX = c_tilePointerMask.tRegion.tSize.iWidth >> 1;
/* 我们在这里更新指针的旋转半径 */
s_tPointerCenter.iY = 100; /* radius */
...
}
那要是多个图片旋转效果怎么样呢?
【制作手表界面】
哈哈,上面的仪表界面只有一个表针(图片)在旋转,其实我们也可以实现多个表针的旋转,比如有时、分、秒的表盘界面。只要使用transform helper就可以很方便地实现动态脏矩阵,让我们的表针旋转更流畅。
贴心的官方也提供了表盘的显示界面给我们测试,我们只要添加watch模板就可以,如下图
在syd8810单片机上的运行效果如下
怎么样,使用了脏矩阵,运行效果还是很流畅吧。
【小结】
到此,今天的文章就讲完了,如果大家对仪表界面感兴趣,那就赶快动手试试吧,添加官方的模板文件来测试是真的非常简单哦(* ̄︶ ̄)
原创不易,如果你喜欢我的公众号、觉得我的文章对你有所启发,
请务必“点赞、收藏、转发”,这对我很重要,谢谢!
欢迎订阅 嵌入式小书虫