# -*- coding: utf-8 -*- """手表屏幕适配校验。 分两部分: 1. **真机参数**(来自用户实测的诊断报告)—— 这是最该保证的 2. 一组常见 density 的覆盖,防止别的设备上按钮被挤爆 把 MainActivity.adaptGridToScreen() 的布局计算原样搬过来核算。 注意:这是**布局数学核算**,不是真机渲染测试。 没有可用设备/模拟器时,它能提前发现"screensize 一变按钮就挤爆"这类问题。 """ from __future__ import annotations import sys PANEL_PADDING_DP = 8.0 # controlPanel 左右 padding CELL_MARGIN_DP = 3.0 # 每个按钮的外边距 BUTTON_PADDING_DP = 4.0 # 按钮内部左右 padding MIN_TOUCH_DP = 48.0 # Android 无障碍最小点击高度 # 现在手表端分 6 页,每页按钮数不一样。 # 关键指标变成「每一页都能一屏放下、不用滚动」。 # 现在分 10 页:功能页已删除,14 个分组散到各页。 # 括号里只统计**固定快捷按钮**;功能分组是滚动区,会显示在下面。 # 现在分 10 页:功能页已删除,14 个分组散到各页。 # 括号里只统计**固定快捷按钮**;功能分组是滚动区,会显示在下面。 # 每页最多 3 行(9 个),超了会滚动。 TABS = { "媒体": ["播放/暂停", "停止", "上一曲", "下一曲", "音量 +", "音量 -", "静音"], "屏幕": ["关掉画面", "屏幕镜像", "截屏", "投图到电脑", "放音到电脑"], "文件": ["发送文件", "接收文件", "剪贴板→电脑", "取电脑剪贴板"], "语音": ["语音播报", "音色", "朗读剪贴板", "电脑音乐", "弹通知"], "电脑": ["快捷命令", "打开网址", "进程管理", "性能", "磁盘", "电量"], "网络": ["本机监听端口", "确认能连上"], "系统": ["回退上一步", "回退全部", "看能回退什么", "锁屏", "睡眠", "关机", "重启", "取消定时关机", "定时关机"], "教室": ["屏蔽游戏网站", "结束游戏进程", "一键恢复全部", "看有哪些限制"], "效率": ["加待办", "看所有待办"], "娱乐": ["掷骰子", "抽奖", "抛硬币"], } # 每页还挂着哪些功能分组(这些在滚动区里) TAB_GROUPS = { "媒体": ["音量", "媒体"], "屏幕": ["显示"], "文件": ["文件"], "语音": ["静默"], "电脑": ["查看", "工具"], "网络": ["网络", "极客"], "系统": ["系统"], "教室": ["教室", "教学"], "效率": ["效率"], "娱乐": ["娱乐"], } LABELS = [label for group in TABS.values() for label in group] # 用户实测的设备(来自遥控诊断报告) REAL_DEVICE = { "name": "M60T (hesc, Android 9 / API 28)", "width_px": 492, "height_px": 602, "density": 1.4125, "density_dpi": 226, } PASSED = 0 FAILED = 0 def check(name: str, ok: bool, detail: str = "") -> None: global PASSED, FAILED if ok: PASSED += 1 print(f" [PASS] {name}") else: FAILED += 1 print(f" [FAIL] {name} {detail}") def adapt(width_dp: float, height_dp: float): """与 Java 端 adaptGridToScreen() 一致。""" if width_dp < 200: return 1, 64.0, 16.0 if width_dp < 300: columns, height, text = 2, 60.0, 14.0 elif width_dp < 330: columns, height, text = 2, 64.0, 15.0 else: columns, height, text = 3, 58.0, 13.0 if height_dp < 320 and columns == 2: height = 56.0 return columns, height, text def label_width_dp(label: str, text_sp: float) -> float: """估算文字宽度:中日韩字符约 1em,其余约 0.55em。""" total = 0.0 for ch in label: total += 1.0 if ord(ch) > 0x2E80 else 0.55 return total * text_sp def report_one(tag: str, width_px: float, height_px: float, density: float) -> float: """核算一台设备,返回最紧的文字余量。""" width_dp = width_px / density height_dp = height_px / density columns, cell_h, text_sp = adapt(width_dp, height_dp) content_dp = width_dp - PANEL_PADDING_DP * 2 # 每行 columns 个按钮,每个按钮左右各一个 3dp 外边距 cell_w = (content_dp - CELL_MARGIN_DP * 2 * columns) / columns text_area = cell_w - BUTTON_PADDING_DP * 2 print(f"\n{tag}") print(f" {width_px:.0f}x{height_px:.0f}px density={density} -> " f"{width_dp:.0f}x{height_dp:.0f} dp") print(f" 列数={columns} 按钮高={cell_h:.0f}dp 字号={text_sp:.0f}sp " f"按钮宽={cell_w:.1f}dp 可用文字宽={text_area:.1f}dp") check(f"{tag} 按钮高度 >= {MIN_TOUCH_DP:.0f}dp", cell_h >= MIN_TOUCH_DP, f"实际 {cell_h}dp") longest = max(LABELS, key=lambda s: label_width_dp(s, text_sp)) need = label_width_dp(longest, text_sp) lines = 1 if need <= text_area else 2 needed_h = lines * text_sp * 1.3 + 6 check(f"{tag} 最长文字「{longest}」放得下", needed_h <= cell_h, f"需 {need:.0f}dp 宽 / {needed_h:.0f}dp 高,可用 {text_area:.0f}dp / {cell_h:.0f}dp") print(f" 「{longest}」{need:.1f}dp -> {lines} 行,需高 {needed_h:.1f}dp") margin = text_area / need check(f"{tag} 文字余量 >= 95%", margin >= 0.95, f"余量 {margin * 100:.0f}%") print(f" 文字余量 {margin * 100:.0f}%") rows = (len(LABELS) + columns - 1) // columns scroll_dp = rows * (cell_h + CELL_MARGIN_DP * 2) print(f" 按钮 {len(LABELS)} 个 -> {rows} 行,总高约 {scroll_dp:.0f}dp" f"(屏高 {height_dp:.0f}dp,需滚动 {max(0, scroll_dp - height_dp + 60):.0f}dp)") return margin def main() -> int: print("=" * 74) print("一、真机参数(来自用户实测的遥控诊断报告)") print("=" * 74) dev = REAL_DEVICE report_one(f"实测设备: {dev['name']}", dev["width_px"], dev["height_px"], dev["density"]) # ---- 分页:每页都要能一屏放下 ---- print() print("=" * 74) print("三、分页布局(每页不超过 7 个按钮)") print("=" * 74) width_dp = dev["width_px"] / dev["density"] height_dp = dev["height_px"] / dev["density"] columns, cell_h, text_sp = adapt(width_dp, height_dp) # 顶栏 42dp + 标签栏 42dp + 日志约 60dp + 边距 chrome_dp = 42 + 42 + 62 + 20 available = height_dp - chrome_dp for name, labels in TABS.items(): rows = (len(labels) + columns - 1) // columns need = rows * (cell_h + 6) fits = need <= available check(f"「{name}」{len(labels)} 个按钮 / {rows} 行,一屏放得下", fits, f"需要 {need:.0f}dp,可用 {available:.0f}dp") print(f" {name}: {len(labels)} 个 / {rows} 行 / {need:.0f}dp" f"(可用 {available:.0f}dp)") real_cols = adapt(dev["width_px"] / dev["density"], dev["height_px"] / dev["density"])[0] check(f"真机走 {real_cols} 列布局(348dp 宽应走三列以减少滚动)", real_cols == 3, f"实际 {real_cols} 列") # 报告里的 densityDpi 与 density 换算是否自洽 implied = dev["density"] * 160 check(f"density 与 densityDpi 自洽({dev['density']} x 160 = {implied:.0f}," f"报告值 {dev['density_dpi']})", abs(implied - dev["density_dpi"]) <= 2, f"差 {abs(implied - dev['density_dpi']):.1f}") print("\n" + "=" * 74) print("二、同一分辨率下不同 density 的覆盖") print("=" * 74) worst = 1e9 two_col = 0 for density in [1.0, 1.25, 1.4125, 1.5, 1.75, 2.0, 2.5, 3.0]: margin = report_one(f"density={density}", dev["width_px"], dev["height_px"], density) worst = min(worst, margin) if adapt(dev["width_px"] / density, dev["height_px"] / density)[0] >= 2: two_col += 1 print("\n" + "=" * 74) print(f"最紧的一档文字余量: {worst * 100:.0f}%") check("常见 density 下不会退化成单列", two_col >= 6, f"多列档数 {two_col}") print("\n" + "=" * 74) print(f"通过 {PASSED} 项,失败 {FAILED} 项") return 1 if FAILED else 0 if __name__ == "__main__": sys.exit(main())