如何用传感器采样获得的数值驱动指针指向对应的刻度 换句话说,如何把任意数值转化为指针旋转的角度,并用这一角度信息来旋转表征指针的图片素材; 如何模拟出类似实体仪表指针那种物理特性? 也就是说,当传感器采样结果具有较大变化时,指针不是直接就指向对应角度,而是有一个类似超调、回调——在目标值附近来回摆动几下才逐渐停止的物理质感。 如何在资源受限、频率较低的MCU上让指针的运动显得流畅而自然?
从v1.2.0版本开始,Arm-2D引入了一个全新的辅助控件:meter_pointer_t,以极其简单直接的接口,一口气同时解决了上述三个问题。借助该控件,我们可以轻松的实现下面视频所展示的效果:
值得说明的是,上述例子中为了展示指针在跳变时刻的“摆动”效果,特意用下面的代码每隔3秒钟就随机产生一个0~200之间的随机整数,并以此来驱动指针的运动:
do {
/* 产生一个3000ms的定时 */
if (arm_2d_helper_is_time_out(3000,
&this.lTimestamp[1])) {
/* 复位软件定时器 */
this.lTimestamp[1] = 0;
/* 初始化随机数发生器 */
srand(arm_2d_helper_get_system_timestamp());
/* 产生一个 0 到 200 之间的随机整数 */
this.iTargetNumber = rand() % 200;
}
/* 更新 指针 */
meter_pointer_on_frame_start(
&this.tMeterPointer, /* 指针控件 */
this.iTargetNumber, /* 指针的值 */
1.0f); /* 指针的缩放比例 */
} while(0);
那么,这一切是如何做到的呢?我们又如何使用 meter_pointer_t 来解决上述三个问题呢?不着急,请听我娓娓道来。
在介绍 meter_pointer_t 控件的使用之前,我需要假设您:
已经通过前面的文章《入门和移植从未如此简单》将Arm-2D移植到了你本地的(硬件)平台上; 通过文章《Arm-2D应用开发入门》了解运用Arm-2D进行GUI应用开发的基础框架——知道场景播放器(Scene Player)、知道如何创建自己的场景(Scene); 至少阅读过文章《零基础Arm-2D API绘图入门无忧》了解了Arm-2D绘图API的基本使用套路,知道Mask的意义和作用。 基本掌握了文章《还在手算坐标?试试Layout Assistant吧!》所介绍的界面布局辅助工具的使用。
如果您还没来得及阅读上述内容,不妨先“单击这里”进入文章列表吧。
#include "arm_2d_example_controls.h"
/*!
* \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 */
...
meter_pointer_t tMeterPointer;
)
/* place your public member here */
...
};
在“场景载入(on-load)”事件处理程序 __on_scene_xxxx_load 中添加 meter_pointer_on_load() 处理程序,例如:
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);
meter_pointer_on_load(&this.tMeterPointer);
}
在“场景卸载(on-depose)”事件处理程序 __on_scene_xxxx_depose 中添加 meter_pointer_depose() 处理程序,例如:
static void __on_scene_meter_depose(arm_2d_scene_t *ptScene)
{
user_scene_meter_t *ptThis = (user_scene_meter_t *)ptScene;
ARM_2D_UNUSED(ptThis);
...
meter_pointer_depose(&this.tMeterPointer);
if (!this.bUserAllocated) {
__arm_2d_free_scratch_memory(ARM_2D_MEM_TYPE_UNSPECIFIED, ptScene);
}
}
在“当前帧刷新完毕(on-frame-complete)”事件处理程序 __on_scene_xxxx_frame_complete 中添加 meter_pointer_on_frame_complete() 处理程序,例如:
static void __on_scene_meter_frame_complete(arm_2d_scene_t *ptScene)
{
user_scene_meter_t *ptThis = (user_scene_meter_t *)ptScene;
ARM_2D_UNUSED(ptThis);
...
meter_pointer_on_frame_complete(&this.tMeterPointer);
...
}
meter_pointer_t 的初始化和配置; 当我们获得传感器返回值时,如何借助 meter_pointer_t 来更新指针的位置; 如何用 meter_pointer_t 来绘制指针
ARM_NONNULL(1)
user_scene_meter_t *__arm_2d_scene_meter_init( arm_2d_scene_player_t *ptDispAdapter,
user_scene_meter_t *ptThis)
{
bool bUserAllocated = false;
assert(NULL != ptDispAdapter);
...
*ptThis = (user_scene_meter_t){
.use_as__arm_2d_scene_t = {
...
.bUseDirtyRegionHelper = true,
},
.bUserAllocated = bUserAllocated,
};
do {
meter_pointer_cfg_t tCFG = {
.tSpinZoom = {
...
.ptScene = (arm_2d_scene_t *)ptThis,
},
...
};
meter_pointer_init(&this.tMeterPointer, &tCFG);
} while(0);
...
arm_2d_scene_player_append_scenes( ptDispAdapter,
&this.use_as__arm_2d_scene_t,
1);
return ptThis;
}
针对 meter_pointer_t 的初始化要放在“针对当前场景(scene)的初始化”之后、“将当前场景添加到场景播放器(scene player)”之前进行;
meter_pointer_t 的初始化是通过调用方法函数 meter_pointer_init() 来实现的。
ARM_NONNULL(1,2)
arm_2d_err_t meter_pointer_init(meter_pointer_t *ptThis,
meter_pointer_cfg_t *ptCFG)
do {
meter_pointer_cfg_t tCFG = {
.tSpinZoom = {
...
},
...
};
meter_pointer_init(&this.tMeterPointer, &tCFG);
} while(0);
为了使用 meter_pointer_t 内置的动态脏矩阵功能,我们需要在“当前场景初始化”中将“UseDirtyRegionHelper”开关设置为"true";在配置结构体中将 tCFG.tSpinZoom.ptScene 赋值为 "(arm_2d_scene_t *)ptThis"。
针对 meter_pointer_t 的配置,集中体现在 meter_pointer_cfg_t 结构体中:
typedef struct meter_pointer_cfg_t {
implement_ex(spin_zoom_widget_cfg_t, tSpinZoom);
struct {
bool bIsSourceHorizontal;
int16_t iRadius;
} Pointer;
arm_2d_helper_pi_slider_cfg_t tPISliderCFG;
int32_t nPISliderStartPosition;
} meter_pointer_cfg_t;
typedef struct spin_zoom_widget_cfg_t {
arm_2d_scene_t *ptScene;
struct {
const arm_2d_tile_t *ptMask;
const arm_2d_tile_t *ptSource;
arm_2d_location_t tCentre;
union {
COLOUR_INT_TYPE tColourForKeying;
COLOUR_INT_TYPE tColourToFill;
};
} Source;
spin_zoom_widget_mode_t *ptTransformMode;
struct {
struct {
float fAngleInDegree;
int32_t nValue;
} LowerLimit;
struct {
float fAngleInDegree;
int32_t nValue;
} UpperLimit;
struct {
float fAngle;
float fScale;
} Step;
} Indicator;
} spin_zoom_widget_cfg_t;
该结构体看似复杂,其实很简单。
它的配置主要分成两个重要组成部分:
“对于指针资源的描述(.Source)”和我们要使用的“Transform模式(.ptTransformMode)”;
这里所谓的 “Transform模式” 是通过 ptTransformMode 来指定的。可选项有:
好奇的小伙伴可能会奇怪 “.Source.tCentre” 的作用。它其实并不复杂。Arm-2D中的旋转操作与我们日常生活中往墙上钉图片类似——如果只用一个图钉,那么图片就可以绕着图钉为圆心进行旋转。如果我们再将图钉取下来,会发现墙上有一个洞——我们称之为目标画布上的“圆心(Pivot)”;图片上也会有一个洞——我们称之为素材上的“旋转中心(Centre)”。
聪明的你一定猜到了——这里的“.Source.tCentre”就是用来设置素材上的旋转中心的。
值得强调的是,由于 meter_pointer_t 用了更加友好的方法来设置指针素材的旋转中心,因此,这里的“.Source.tCentre”不用专门设置(即便你设置了,后面也会被覆盖)。
在 meter_pointer_cfg_t 中,我们是通过下面的结构来描述指针素材的:
typedef struct meter_pointer_cfg_t {
...
struct {
bool bIsSourceHorizontal;
int16_t iRadius;
} Pointer;
...
} meter_pointer_cfg_t;
接着,根据你屏幕上指针旋转中心到指针末梢的距离(也就是指针的旋转半径)来设置 “.Pointer.iRadius” 。
需要特别强调的是:iRadius 的值“可以”与指针素材的尺寸无关。比如,对于例子视频中的指针,显然指针的高度(height)是小于指针的旋转半径的,此时我们只需要根据需要将实际的旋转半径赋值给“.Pointer.iRadius” 就能实现视频中“悬空指针”的效果。
对仪表盘中角度和仪表数值线性映射关系的设置(Indicator)
在Indicator中,我们需要分别设置:
指针的“起始角度(.LowerLimit.fAngleInDegree)”——顾名思义,指针起始的角度(注意不是弧度),这是一个float型数据; 指针指向“起始角度”时对应的“仪表数值下限(LowerLimit.nValue)”,这是一个int32_t型数据; 指针的“终止角度(.UpperLimit.fAngleInDegree)”——顾名思义,指针终止的角度; 指针指向“终止角度”时对应的“仪表数值上线(UpperLimit.nValue)”,这是一个int32_t型数据;
meter_pointer_cfg_t tCFG = {
.tSpinZoom = {
.Indicator = {
.LowerLimit = {
.fAngleInDegree = -120.0f,
.nValue = 0,
},
.UpperLimit = {
.fAngleInDegree = 100.0f,
.nValue = 200,
},
},
.ptTransformMode = &SPIN_ZOOM_MODE_FILL_COLOUR,
.Source = {
.ptMask = &c_tilePointerMask,
.tColourToFill = GLCD_COLOR_RED,
},
...
},
...
};
指针的物理效果是通过 tPISliderCFG 来配置的。
typedef struct meter_pointer_cfg_t {
...
arm_2d_helper_pi_slider_cfg_t tPISliderCFG;
int32_t nPISliderStartPosition;
} meter_pointer_cfg_t;
值得说明的是:对普通应用来说,请忽略“tPISliderCFG”的配置——meter_pointer_t会采用一个适合大部分应用的默认值。一些有兴趣、且有特殊要求的小伙伴可以去自行调参,希望你们能找回大学时上自控原理PID实验课时候的乐趣——虽然这里只用到了PI而已。
至此,我们获得了一个完整的 meter_pointer_t 初始化和配置代码:
do {
meter_pointer_cfg_t tCFG = {
.tSpinZoom = {
/* 设置角度与数值的映射关系 */
.Indicator = {
.LowerLimit = {
.fAngleInDegree = -120.0f,
.nValue = 0,
},
.UpperLimit = {
.fAngleInDegree = 100.0f,
.nValue = 200,
},
},
/* 设置指针的绘制模式 */
.ptTransformMode = &SPIN_ZOOM_MODE_FILL_COLOUR,
.Source = {
.ptMask = &c_tilePointerMask,
.tColourToFill = GLCD_COLOR_RED,
},
/* 开启脏矩阵的必要步骤之一 */
.ptScene = (arm_2d_scene_t *)ptThis,
},
/* 设置指针 */
.Pointer = {
.bIsSourceHorizontal = false, /* 素材是竖着的 */
.iRadius = 100, /* 旋转半径 */
},
};
meter_pointer_init(&this.tMeterPointer, &tCFG);
} while(0);
static
IMPL_PFB_ON_DRAW(__pfb_draw_scene_meter_handler)
{
user_scene_meter_t *ptThis = (user_scene_meter_t *)pTarget;
ARM_2D_UNUSED(ptTile);
ARM_2D_UNUSED(bIsNewFrame);
arm_2d_canvas(ptTile, __canvas) {
/* 将表盘居中 */
arm_2d_align_centre(__canvas, c_tileMeterPanel.tRegion.tSize) {
/* 绘制仪表表盘 */
arm_2d_tile_copy_only( &c_tileMeterPanel,
ptTile,
&__centre_region);
ARM_2D_OP_WAIT_ASYNC();
/* 绘制指针 */
meter_pointer_show(
&this.tMeterPointer,
ptTile, /* 目标画布 */
&__centre_region, /* 指针在画布中的位置 */
NULL, /* 指针在画布上的旋转中心 */
255); /* Opacity */
}
...
}
ARM_2D_OP_WAIT_ASYNC();
return arm_fsm_rt_cpl;
}
extern
ARM_NONNULL(1, 2)
void meter_pointer_show(meter_pointer_t *ptThis,
const arm_2d_tile_t *ptTile,
const arm_2d_region_t *ptRegion,
const arm_2d_location_t *ptPivot,
uint8_t chOpacity);
这里,我们需要借助函数 meter_pointer_on_frame_start(),它的函数原型如下:
extern
ARM_NONNULL(1)
bool meter_pointer_on_frame_start(
meter_pointer_t *ptThis,
int32_t nTargetValue,
float fScale);
static void __on_scene_meter_frame_start(arm_2d_scene_t *ptScene)
{
user_scene_meter_t *ptThis = (user_scene_meter_t *)ptScene;
ARM_2D_UNUSED(ptThis);
meter_pointer_on_frame_start(
&this.tMeterPointer,
<你的传感器采样结果>,
1.0f);
...
}
2、选择Add New Item to Group。
3、在窗口中找到User Code Template。展开Acceleration,选中Arm-2D Helper:PFB的Scene Template Meter,并在Location后面的"..." 按钮中将例子模板添加到工程目录中。
4、在MDK工程配置中,添加针对头文件的搜索路径,使其指向 arm_2d_scene_meter.h 所在的目录。
5、在main.c 中添加头文件:
#include "arm_2d_scene_meter.h"
...
arm_2d_scene_meter_init(&DISP_ADAPTER0);
arm_2d_scene_player_switch_to_next_scene(&DISP0_ADAPTER);
...