← 都来码← 文档索引多点触控与设备原文English

多点触控与设备

多指查询、按住判断、方向锁、全屏。

完整清单见「UI 开发」。下面每个接口都带一句用法和一个例子;签名取自 Lib/c/vml_ui.h(权威来源)。

ui_audio_is_playing(void)

后台 BGM 还在放吗 → 1/0(没放过、已放完、被停掉都是 0)。典型用法:等一首放完再进下一段。

ui_audio_play("bgm.mp3", 0);
while (ui_audio_is_playing()) { ui_msg_wait(msg, 200); }

ui_scr_set_immersive(int on)

隐藏 / 恢复状态栏与导航栏(全屏游戏)。

ui_scr_set_immersive(1);   /* 进游戏:全屏 */
ui_scr_set_immersive(0);   /* 退出前恢复 */

ui_key_is_down(int key)

某个键此刻按住没有(“持续按住左移”不必自己维护状态表)。 ⚠ 它治不了丢 KeyUp —— 手指划出按键范围、系统吃掉 CANCEL 都会让 KeyUp 永远不来,而查询会一直报“按着”。连发逻辑仍然要自带刹车(按了没动两次就停 / 总拍数上限)。

if (ui_key_is_down(VML_KEY_LEFT)) { x = x - 2; }

ui_scr_lock_orientation(int mode)

锁定屏幕方向:VML_LOCK_PORTRAIT(0) / VML_LOCK_LANDSCAPE(1) / VML_LOCK_AUTO(2)。 ⚠ 与 ui_win_open_ex 的 rotatable 是两件事:那个管“转屏时窗口跟不跟”,这个管“系统让不让转”。

ui_scr_lock_orientation(VML_LOCK_LANDSCAPE);   /* 运行中锁成横屏 */

ui_touch_read(int slot, int* out)

查第 slot 根手指(0..9):写进 out[0]=x out[1]=y out[2]=按下,返回 1 有效(槽位越界 0)。 双人同屏 / 虚拟摇杆 / 双指缩放 / 按和弦靠它。⚠ 这是轮询,要自己每帧查。

int t[3];
if (ui_touch_read(0, t) && t[2]) { /* 第 0 根手指按在 (t[0],t[1]) */ }

⚠⚠ 触摸的「事件消息」只有槽位 0 会投 —— ui_msg_wait 收到的触摸消息永远只描述第 0 根手指。所以凡是多指的程序,按下/抬起的判定必须走轮询(挂个 16~33ms 的定时器,自己用上一拍的按下状态做边沿检测)。只靠事件消息的话,程序在单指下一切正常、 一旦同时按第二根手指,第二根就完全看不见 —— 这也正是"按住琴键的另一根手指点不动按钮"那类怪现象的来源。现成写法见 examples/c/app/piano.c。

ui_touch_is_down(void)

上一次 ui_touch_fetch 缓存里的“按下”(1/0)。

if (ui_touch_is_down()) { /* … */ }

ui_touch_fetch(int slot)

拿不到指针的语言用这个:查一次并缓存,下面三个读缓存(与 ui_msg_fetch_wait + ui_msg_get_a/b 同一套分工)。

ui_touch_fetch(0);
if (ui_touch_is_down()) { x = ui_touch_get_x(); y = ui_touch_get_y(); }

ui_touch_get_x(void)

上一次 ui_touch_fetch 缓存里的 x。

int x = ui_touch_get_x();

ui_touch_get_y(void)

上一次 ui_touch_fetch 缓存里的 y。

int y = ui_touch_get_y();

传感器(歪手机控制)

ui_sensor_read(int kind, int* out)

读最新读数,写进 out[0..2]。返回 1 = 有效、0 = 这台设备没有该传感器。

kind 三选一 —— 做倾斜控制用第一个:

kind是什么单位
VML_SENS_ACCEL(0)加速度(含重力)毫克(1000 = 1g)
VML_SENS_GYRO(1)角速度(转得多快)千分之一度/秒
VML_SENS_ROTATION(2)融合姿态(俯仰/翻滚/方位)千分之一度
int a[3];
if (ui_sensor_read(VML_SENS_ACCEL, a)) {
    /* 平放时 a ≈ (0, 0, 1000);往右歪 30° 时 a[0] ≈ 500 */
    move_x = 0 - a[0] / 40;     /* ⚠ 取负:球往低处滚 */
}

⚠⚠ 为什么倾斜用加速度计而不是陀螺仪:陀螺仪给的是角速度,要"当前角度"得自己积分,而积分会漂(几十秒就偏出可用范围)。加速度计静止时读到的就是重力方向,也就是倾斜本身,不漂。

⚠⚠ 两个轴都要取负号。加速度计读的是"哪条轴朝上"—— 把右边抬起来 x 会变大,而东西该往低的那边滑。写反的症状是"歪这边、东西往那边跑",看起来完全像传感器装反了,其实就错在这一行。(这条已在真机上确认:右边抬高、球往左滚。)

⚠ 读不到 ≠ 读到 0:返回 0 才是"没有这个传感器"。混在一起程序会以为"手机放平了"。

ui_sensor_is_available(int kind)

这台设备有没有该传感器 → 1/0。开窗之前就能问。

ui_sensor_set_rate(int kind, int ms)

采样间隔(毫秒,0 = 平台默认)。传感器一开就在耗电,间隔越大越省。

ui_sensor_calibrate(int kind)

把当前姿态当成零点("校准水平")。玩家躺在沙发上玩时"水平"是错的,退出重进也还是错的 ⇒ 给一个"点一下校准"的入口。

ui_sensor_fetch(int kind) / ui_sensor_get_x/y/z(void)

拿不到指针的语言用这一套(与 ui_touch_fetch 同构):先 ui_sensor_fetch(0) 查一次并缓存,再读 ui_sensor_get_x() 等。

ok = ui_sensor_fetch(0);
if (ok) { x = ui_sensor_get_x(); y = ui_sensor_get_y(); z = ui_sensor_get_z(); }

现成例子:examples/c/app/tilt.c(水平仪 + 小球,三个传感器的读数都画在屏幕上)。

电量与省电

玩到一半没电是手机游戏最常见的"事故",而程序自己就能防 —— 电量低了少画点东西、关掉音效、提醒玩家存档。不需要任何权限。

ui_battery_read(int* out)

读电量与充电状态:out[0] = 电量 0–100、out[1] = 充电中(1)/没充电(0)。返回 1 = 有效、0 = 这台设备没有电池(桌面、一直插电的机器)。

int b[2];
if (ui_battery_read(b) && b[0] < 15 && b[1] == 0) {
    low_power_mode();      /* 没在充电、又是低电量 ⇒ 该省了 */
}

⚠ 「没有电池」与「电量 0%」是两回事:返回 0 才是前者。拿 100 或 0 冒充,程序会以为"电很足"或"电耗尽了",而真相是"这个问题在这里没有意义"。

⚠ 电量变得很慢(几分钟才动一格)⇒ 在"每拍跑一次"的逻辑里顺手看一眼就够, 不必每帧查。

ui_power_is_saving(void)

系统的省电模式开着吗 → 1/0。

⚠ 与"电量低"不是一回事:那是用户明确要求省电(也可能在电量还有 40% 时手动开的)。程序据此降帧是照用户的意思办,而不是你在猜。两者互相独立 —— 满电也可能省电模式开着。

ui_battery_fetch(void) / ui_battery_get_level(void) / ui_battery_is_charging(void)

拿不到指针的语言用这一套(与 ui_touch_fetch 同构):先 ui_battery_fetch() 查一次并缓存,再读 ui_battery_get_level() 等。

现成例子:examples/c/app/tilt.c 底部就画着电量那一行。

剪贴板与分享

手机游戏的标配:分享成绩、存取档码(把整局状态编成一小段文本,玩家自己存到备忘录里,换设备接着玩)。都不需要权限。

char code[64];
ui_clipboard_set("SAVE-1a2b3c");                 /* 把存档码交给玩家 */
if (ui_clipboard_get(code, 64) > 0) { /* 读回来接着玩 */ }
ui_share_text("我在拧螺丝里拧了 8 圈!", "分享战绩");

ui_clipboard_set(char* text) / ui_clipboard_get(char* buf, int cap)

写 / 读系统剪贴板。set 返回 1=已交给系统、0=这一端没有剪贴板; get 返回写入的 UTF-8 字节数、-1=剪贴板是空的。

⚠ 返回值 1 只表示"已经交给系统",不表示"玩家已经存上了" —— 写剪贴板在平台上是异步的,宿主不等结果。 ⚠ 缓冲区不够时 get 返回的是实际写入的字节数(不是原文长度)—— **拿它和 strlen 比一比就知道有没有被截断**。 ⚠ 传空串给 set 不会清空剪贴板(返回 0)—— 那是"没算出来",不是你想要的"清空"。

ui_share_text(char* text, char* title)

弹系统分享面板(标题传空串 = 不带)。1=已弹出、0=这一端没有分享面板。 ⚠ 不阻塞 —— 只负责把面板弹出来,程序继续跑。

ui_url_open(char* url)

用系统浏览器打开链接。**只认 http:// 和 https://**,别的 scheme 一律返回 0 (放行任意 scheme 等于让程序借系统去打开任意东西)。

⚠ 它会离开这个应用 —— 调之前先存档。但别指望宿主替你暂停:那是策略,不是机制。

⚠ 返回 0 是"这一端没有这个能力",不是失败 —— 桌面就没有分享面板。游戏该照常继续(与"设备没有陀螺仪"同一条处置)。

错误码(上一次调用为什么失败)

相机 / 相册、文件选择器、定位这几条失败时一律返回 0 —— 而 0 分不清三件事: 用户点了取消 / 没有权限 / 这一端压根没有这个能力。这两个接口给它们分家:

char img[256];
if (ui_camera_capture(img, sizeof(img)) > 0) {
    ui_2d_image(0, 0, img, 320, 240);
} else if (ui_error_get_code() != VML_E_CANCELLED) {
    char why[64];
    ui_error_get_message(why, sizeof(why));   /* 人话,例如「没有权限」 */
    ui_dlg_msg("取图失败", why, VML_DLG_WARN);
}
值名字含义
0VML_E_OK没有错误(上一次调用成功了)
1VML_E_CANCELLED用户取消了 —— 不是故障
2VML_E_PERMISSION没有权限(被拒 / "不再询问")
3VML_E_UNSUPPORTED这一端没有这个能力(没相机 / 没 GPS / 没这个传感器)
4VML_E_TIMEOUT超时
5VML_E_FAILED其它失败(宿主内部错误 / 异常 / 文件不存在)

ui_error_get_code(void)

最近一次失败的错误码;没有错误就是 0。

⚠ 它描述的是「刚才那一次宿主调用」—— 中间再调任何别的 ui_*,槽就被清回 0,所以要在失败之后立刻读。这两个读口自己不清槽,先问码再问消息读的是同一次失败。

if (ui_gps_fetch(5000) == 0) {
    if (ui_error_get_code() == VML_E_PERMISSION) { /* 引导用户去系统设置里开权限 */ }
}

ui_error_get_message(char* buf, int cap)

把人话描述写进 buf(最多 cap 字节,含结尾 0),返回写入的字节数;没有错误就写空串、返回 0。cap 传 0 = 只问长度(一个字节都不写)。描述跟随界面语言(中文端中文、英文端英文),可以直接拿去给玩家看。

⚠ VML_E_CANCELLED 是玩家的正常操作,别当故障终止程序。