Compare commits
9 Commits
553c55026a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 00bce1b6ab | |||
| 230180febf | |||
| 696b728c78 | |||
| 22340b09fa | |||
| d9d45d983b | |||
| 4d96ab0912 | |||
| 52346bdd09 | |||
| d457f8244e | |||
| 398018b51e |
34
CHANGELOG.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 更新记录
|
||||
|
||||
## 1.1.0
|
||||
|
||||
**机型覆盖 17 → 55 款**
|
||||
|
||||
新增 38 款圆屏、Connect IQ ≥ 4.2 的机型,包括 fenix 7 Pro 全系、Venu 2/3S、
|
||||
Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、FR255S/265S/570/955 等。
|
||||
机型列表与资源路径改由 `tools/gen_devices.py` 扫描本机 SDK 生成,不再手写。
|
||||
|
||||
新增两档图标尺寸(218px、360px)。218px 上设计稿的天气图标只有 14×14,
|
||||
太阳与云之间的暗缝不足一个像素,会糊成一块 —— 该尺寸改用专画的简化标记。
|
||||
|
||||
**常亮模式收紧**
|
||||
|
||||
砍掉整个进度环,时间/日期由 55% 压暗到 38%。实测点亮像素数 10.4% → 9.7%,
|
||||
平均亮度 3.0% → 2.5%(普通模式分别是 27.6% / 14.1%)。
|
||||
|
||||
**其它**
|
||||
|
||||
- 补 LICENSE(MIT,仅覆盖代码;`design/` 下的设计稿版权归第三方,见文件内说明)
|
||||
- `UserProfile.getProfile()` 加异常保护
|
||||
- 版本号 1.0.0 → 1.1.0
|
||||
|
||||
## 1.0.0
|
||||
|
||||
按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。
|
||||
|
||||
- 156 项可选数据字段,编号对齐 watchface.io 目录
|
||||
- 七套主题,配色逐根刻度实测自设计稿
|
||||
- 常亮(AOD)模式,含防烧屏位移
|
||||
- 简体中文 / 英文双语,表盘文字亦本地化
|
||||
- OpenWeatherMap 接入(用户自备 API key,后台定时拉取)
|
||||
- 系统 Complication 接入(日历、训练状态、完赛预测、第三方 App)
|
||||
29
LICENSE
Normal file
@@ -0,0 +1,29 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 ericwyuan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
注意:本许可只覆盖**本仓库的代码**。
|
||||
|
||||
design/ 目录下的设计稿(watchface-*.svg、watchface-tokens.json、rebuild.py)
|
||||
来自第三方 watchface-kit,版权归其原作者所有,不在本许可范围内。
|
||||
表盘的视觉呈现是对那套设计的 1:1 还原 —— 若要发布到 Connect IQ Store 或以
|
||||
其它形式分发,需先取得原作者授权,或将视觉部分替换为原创设计。
|
||||
515
README.md
@@ -1,168 +1,411 @@
|
||||
# Fenix 8 V3 — Garmin 数据表盘
|
||||
|
||||
一款基于 Connect IQ 设计稿(`watchface-kit`)1:1 还原的多指标数据表盘。设计画布为 **500×500** 正方形,渲染时按屏幕宽度等比缩放,支持圆形 AMOLED(Fenix 8 / Enduro 3 / Epix 2)与圆屏 MIP(Fenix 7 / FR965 等)设备。
|
||||
按 `watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLED 与圆屏 MIP 共 **55 款**设备(Fenix 7/8 全系含 Pro、Epix 2、Enduro 3、Venu 2/3、FR 系列、Approach S50/S70、Descent、D2、Instinct 3 AMOLED 等)。
|
||||
|
||||
- **SDK**:Connect IQ SDK 9.1.0(`monkeyc` 9.1.0,API level `minSdkVersion="4.2.0"`)
|
||||
- **语言**:Monkey C
|
||||
- **SDK**:Connect IQ SDK 9.1.0(`monkeyc` 9.1.0,`minSdkVersion="4.2.0"`)
|
||||
- **语言**:Monkey C;界面文案含英文与简体中文
|
||||
- **类型**:`watchface`
|
||||
- **应用 ID**:`f8a3c1b2c3d44e5f8a9b0c1d2e3f4a5b`
|
||||
|
||||
---
|
||||
|
||||
## 1. 布局还原(与设计稿基准)
|
||||
|
||||
所有坐标以 500×500 设计画布为基准,渲染时统一乘以缩放系数 `s = screenWidth / 500.0`(Enduro 3 屏宽 416px → `s≈0.832`)。下表中 `y` 为设计稿原始像素值。
|
||||
|
||||
| 区域 | 设计坐标(500 基准) | 内容 | 说明 |
|
||||
|------|----------------------|------|------|
|
||||
| 进度环 | 外半径 `rOut=249.5`,内半径 `rIn=231.5` | 四段进度弧 + 4 个锚点圆点 | 见 §2 |
|
||||
| 电量 | `y=53.5`,电池框居中偏左 | 电池图标 + 百分比 | `System.getSystemStats().battery` |
|
||||
| 上方双复杂功能 | 图标 `y=90`,数值 `y=130.5`;左侧 `x=156`,右侧 `x=343` | 天气温度 / 卡路里 / 步数 / 距离 / 楼层 / 电量 | 见 §4 |
|
||||
| 中央时间 | `y=220`;小时 `x=211`(右对齐),分钟 `x=272`(左对齐) | `HH : MM`(12 小时制) | 冒号用两块矩形块,位于 `x=238` |
|
||||
| 日期带 | 矩形 `y=286.5`,高 `44` | 蓝牙图标 + 星期 + 月日 + AM/PM | 蓝牙图标 `x=36.5`,星期 `x=177.5`,月日 `x=297.5` |
|
||||
| 底部指标 | 数值 `y=366`,标签 `y=410.5` | 左侧距离(`DST`),右侧步数(`STEP`) | 距离单位 km |
|
||||
|
||||
进度环几何(取自设计稿 `rebuild.py`):
|
||||
|
||||
- **顶弧**:22 根刻度,角度 `TOP = [2,6,…,86]`(步距 4),线宽 `13`,渐变停止点 9 个(`8→86`)。
|
||||
- **底弧**:21 根刻度,角度 `BOT = [6,9,…,66]`(步距 3),线宽 `9`,渐变停止点 7 个(`7→47`)。
|
||||
- **锚点圆点**:位于角度 `[0.0, 93.3, 180.0, 266.7]`,圆心半径 `240.5`,圆点半径 `9.5`。
|
||||
- 刻度“点亮”数量按对应指标进度换算:`litTR/TL = progress × 22`,`litBR/BL = progress × 21`。
|
||||
|
||||
时间字体为 `FONT_NUMBER_HOT`(大号数字),其余为 `FONT_MEDIUM` / `FONT_SMALL`。
|
||||
设计稿本体(7 张 SVG + tokens + 逆向脚本)在 `design/`,是本仓库所有坐标与配色的唯一来源。
|
||||
|
||||
---
|
||||
|
||||
## 2. 七套主题(Themes)
|
||||
## 1. 目录结构
|
||||
|
||||
颜色数据由 `watchface-tokens.json` 经 `gen_themes.py` 生成到 `source/Themes.mc`(**请勿手改**,改 tokens 后重新生成)。每套主题包含:顶/底弧渐变停止点、未点亮刻度色 `tickOff`、锚点色 `anchorDot`、日期带填充 `bandFill` / 文字 `bandText`、强调色 `accent`、主文字 `textPrimary`、冒号色 `colon`、小时 `hours`、分钟 `minutes`。
|
||||
```
|
||||
fenix8v3-watchface/
|
||||
├── manifest.xml # 应用清单(ID / 支持设备 / 语言)
|
||||
├── monkey.jungle # 构建配置
|
||||
├── design/ # ⭐ 设计稿原件(唯一事实来源)
|
||||
│ ├── watchface-0N-*.svg # 7 套主题的矢量还原稿
|
||||
│ ├── watchface-tokens.json # 配色 token
|
||||
│ └── rebuild.py # 原作者从位图逆向出 SVG 的脚本(仅存档,不参与构建)
|
||||
├── tools/
|
||||
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc
|
||||
│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
|
||||
│ ├── gen_devices.py # 扫描 SDK -> manifest 机型列表 + monkey.jungle
|
||||
│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
|
||||
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
|
||||
├── source/
|
||||
│ ├── Fenix8V3App.mc # 应用入口(12 行壳子)
|
||||
│ ├── Fenix8V3View.mc # 绘制顺序:进度环 / 各元素 / 常亮模式
|
||||
│ ├── Layout.mc # 500 画布常量、缩放、字号选择、文字绘制
|
||||
│ ├── Icons.mc # 图标:位图三件套 + 其余图元
|
||||
│ ├── Fields.mc # 125 项数据字段的取值与格式化
|
||||
│ ├── Solar.mc # 日出/晨昏蒙影/黄金蓝调时刻的太阳高度角计算
|
||||
│ ├── Owm.mc # OpenWeatherMap 缓存读取
|
||||
│ ├── Comp.mc # 系统 / 第三方 Complication 桥接
|
||||
│ ├── Fenix8V3Background.mc # 后台服务:拉取 OWM
|
||||
│ ├── Settings.mc # 所有设置项的读取与默认值
|
||||
│ ├── FieldTable.mc # ⚠️ 自动生成:字段 id 表
|
||||
│ └── Themes.mc # ⚠️ 自动生成:七套主题配色
|
||||
├── resources/ # 英文文案 + 设置项定义(部分自动生成)
|
||||
├── resources-chn/ # 简体中文文案
|
||||
├── resources-icons-<宽度>/ # ⚠️ 自动生成:按屏幕尺寸光栅化的图标
|
||||
├── resources-launcher-<尺寸>/ # ⚠️ 自动生成:各尺寸启动图标
|
||||
├── docs/ # 上架素材:商店文案、图标、各机型截图
|
||||
└── bin/ # 编译产物(已 gitignore)
|
||||
```
|
||||
|
||||
| # | 主题 | 主色调 | 分钟数字 | 日期带 |
|
||||
|---|------|--------|----------|--------|
|
||||
| 1 | Ember | 橙红渐变(黄→红) | `0xFCA901` | 橙底黑字 |
|
||||
| 2 | Aurora | 青绿渐变(黄绿→青) | `0xAAF701` | 深灰底白字 |
|
||||
| 3 | Brass | 金铜渐变 | `0xECCC93` | 深棕底白字 |
|
||||
| 4 | Voltage | 蓝→黄渐变 | `0x52A8FD` | 蓝底黑字 |
|
||||
| 5 | Reef | 橙→青渐变 | `0xFC7801` | 青底黑字 |
|
||||
| 6 | Acid | 黄绿(近单色) | `0xC4F900` | 深灰底黑字 |
|
||||
| 7 | Kelp | 橙→绿渐变 | `0xF4880A` | 深灰底黑字 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 可配置设置(Garmin Connect / 表盘设置)
|
||||
|
||||
通过 `resources/settings/` 暴露,键名与默认值见 `properties.xml`:
|
||||
|
||||
| 设置 | 键 | 可选值 | 默认 |
|
||||
|------|----|--------|------|
|
||||
| 主题 | `Theme` | 1–7(Ember…Kelp) | 1 |
|
||||
| 顶右弧指标 | `RingTR` | 0 步数 / 1 卡路里 / 2 距离 / 3 楼层 / 4 动动条 | 0 |
|
||||
| 顶左弧指标 | `RingTL` | 同上 | 1 |
|
||||
| 底右弧指标 | `RingBR` | 同上 | 4 |
|
||||
| 底左弧指标 | `RingBL` | 同上 | 3 |
|
||||
| 左上复杂功能 | `LeftTop` | 0 天气 / 1 卡路里 / 2 步数 / 3 距离 / 4 楼层 / 5 电量 | 1 |
|
||||
| 右上复杂功能 | `RightTop` | 同上 | 0 |
|
||||
| 显示顶行 | `ShowTop` | 布尔 | true |
|
||||
| 显示日期带 | `ShowBand` | 布尔 | true |
|
||||
| 显示底行 | `ShowBottom` | 布尔 | true |
|
||||
|
||||
> 指标默认值对应设计稿:**顶右=步数、顶左=卡路里、底右=动动条、底左=楼层**;左上=卡路里、右上=天气。
|
||||
|
||||
### 指标进度换算规则(`metricProgress`)
|
||||
|
||||
| idx | 指标 | 进度分母 |
|
||||
|-----|------|----------|
|
||||
| 0 | 步数 Steps | `stepGoal`(目标步数) |
|
||||
| 1 | 卡路里 Calories | 固定 2000 kcal |
|
||||
| 2 | 距离 Distance | 10 km(1,000,000 cm) |
|
||||
| 3 | 楼层 Floors | `floorsClimbedGoal`(默认 10) |
|
||||
| 4 | 动动条 Move Bar | `moveBarLevel`(归一化 0–1) |
|
||||
|
||||
进度超出 1 会被钳制,弧长即“点亮”刻度数。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据源与已知偏差
|
||||
|
||||
| 元素 | 数据来源 | 备注 |
|
||||
|------|----------|------|
|
||||
| 步数 / 卡路里 / 距离 / 楼层 / 动动条 | `ActivityMonitor.getInfo()` | SDK 9.1.0 字段:`steps/stepGoal/calories/distance/floorsClimbed/floorsClimbedGoal/moveBarLevel` |
|
||||
| 天气温度 | `Weather.getCurrentConditions().temperature` | 需手表已配对天气 |
|
||||
| 电量 | `System.getSystemStats().battery` | 0–1 浮点,×100 显示 |
|
||||
| 日期 / 时间 | `Time.Gregorian` | 12 小时制,带 AM/PM |
|
||||
| 蓝牙图标 | `System.getDeviceSettings().phoneConnected`(仅取状态) | 见下方偏差 |
|
||||
|
||||
### ⚠️ 与设计稿的关键偏差
|
||||
|
||||
1. **“日出(sunrise)”元素 → 实时天气温度。** 设计稿顶部复杂功能中有一个日出图标及时间,但 `Weather.CurrentConditions` 在 SDK 9.1.0 **不暴露 sunrise 字段**。因此该位置改为显示**实时天气温度**(`NN°`),图标保留太阳造型。
|
||||
2. **进度环为实时数据**,非设计稿中的静态填充演示值;四段弧指标可在设置中自由分配。
|
||||
3. **蓝牙图标**目前按日期带文字色常显(未根据 `phoneConnected` 切换亮/暗),如需“未连接变暗”可后续在 `drawDateBand` 中据 `ds.phoneConnected` 调色。
|
||||
|
||||
---
|
||||
|
||||
## 5. 编译 / 构建
|
||||
|
||||
需安装 Connect IQ SDK(本文基于 9.1.0)并把 `monkeyc` 加入 PATH,且准备开发者密钥 `developer_key.der`(已在仓库内)。
|
||||
重新生成全部派生文件:
|
||||
|
||||
```bash
|
||||
# 单设备(Enduro 3 主产物)
|
||||
monkeyc -o bin/Fenix8V3.prg -f monkey.jungle -y developer_key.der -d enduro3
|
||||
python3 tools/gen_devices.py \
|
||||
&& python3 tools/gen_themes.py && python3 tools/gen_fields.py \
|
||||
&& python3 tools/gen_icons.py && python3 tools/gen_preview_svg.py
|
||||
```
|
||||
|
||||
# 导出调试资源(含 -settings.json,便于模拟器载入设置)
|
||||
monkeyc -o bin/Fenix8V3.prg -f monkey.jungle -y developer_key.der -d enduro3 -x
|
||||
`gen_devices.py` 要先跑 —— 它决定了需要哪些图标尺寸,`gen_icons.py` 的
|
||||
`WIDTHS` / `LAUNCHER` 两张表必须跟它对齐,少一档就会有机型编译不过。
|
||||
|
||||
# 多设备批量(已验证可编译)
|
||||
for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm \
|
||||
fenix8solar51mm epix2 epix2pro47mm epix2pro51mm fenix7 fenix7x \
|
||||
fenix7s fr965 fr265 fr255 venu3 vivoactive5; do
|
||||
三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**:`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。
|
||||
|
||||
---
|
||||
|
||||
## 1.5 代码结构
|
||||
|
||||
一句话:**位置归 `Layout`,内容归 `Fields`,配色归 `Themes`,设置归 `Settings`,`Fenix8V3View` 只剩画的顺序。**
|
||||
|
||||
| 模块 | 负责 | 不负责 |
|
||||
|------|------|--------|
|
||||
| `Layout` | 500 基准画布的全部坐标常量、`px()` 缩放、极坐标换算、字号选择与自动缩字、渐变文字的分带绘制 | 具体画什么 |
|
||||
| `Icons` | 三个位图图标的加载与染色、电池、蓝牙、其余数据项的图元 | 图标该出现在哪个数据位(由字段 id 决定,但查表在这里) |
|
||||
| `Fields` | 75 项字段的取值、单位换算、本地化文案(星期/月份/标签) | 排版 |
|
||||
| `Settings` | 每个设置项的读取与默认值,包含空值回退 | 设置项的 UI 定义(在 `settings.xml`) |
|
||||
| `Fenix8V3View` | `onUpdate` 的绘制顺序、进度环、常亮模式 | 以上任何一项的细节 |
|
||||
|
||||
`Fields.value()` 按分类拆成 7 个函数(`dateTime` / `activity` / `body` / `system` / `environment` / `weather` / `custom`),每个在不认识 id 时返回 `null`,`value()` 依次尝试。id 是交错的(1、2、3 是活动,8、11、12 是身体,14 又是活动),所以按区间派发行不通。
|
||||
|
||||
`Fields.Ctx` 每次绘制构造一个,把 `ActivityMonitor` / `Weather` / `UserProfile` / `SensorHistory` 的查询**懒加载并缓存**——四个数据位可能都要天气,但只查一次;没有数据位用到 `UserProfile` 时就完全不查。
|
||||
|
||||
|
||||
## 2. 布局(500 基准,取自设计稿 SVG 路径的墨迹包围盒)
|
||||
|
||||
| 元素 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| 进度环 | 内半径 `231.09`,外半径 `249.10` | 见 §3 |
|
||||
| 电量图标 | `x=192.70`,`cy=53.20`,`26.20×17.81` | 圆角外框 + 按电量填充 + 右侧触点 |
|
||||
| 电量数值 | 左对齐 `x=232.00` | `NN%` |
|
||||
| 上方双数据 | 图标 `cy=90`,数值 `cy=131.06`;左 `x=157.00`,右 `x=343.70` | 居中对齐 |
|
||||
| 时间 | `cy=221.10`;小时右对齐 `x=211.40`,分钟左对齐 `x=276.02` | 竖向渐变,见 §4 |
|
||||
| 冒号 | `x=237.88`,`24.24×21.30`,`y=183.68 / 234.57` | 两块矩形 |
|
||||
| 日期带 | `y=286.30`,高 `44.19`,文字 `cy=308.10` | 蓝牙 `cx=36.88`、星期 `cx=178.05`、月日 `cx=298.28`、AM/PM 右对齐 `465.89` |
|
||||
| 底部数值 | `cy=366.76`;左格右对齐 `234.30`,右格左对齐 `267.40` | 过宽时自动降一档字号 |
|
||||
| 底部标签 | `cy=410.88`,同上对齐 | 跟随所选数据(`DST` / `STEP` / `距离` …) |
|
||||
|
||||
字号按设计稿的**字面高度(cap height)**选取:日期带与电量 `25.34`、上方数据与底部行 `31.50`、时间 `98.70`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 进度环
|
||||
|
||||
刻度是**等角宽的环形扇区**(`fillPolygon` 四点),不是径向线段。
|
||||
|
||||
- **顶弧**:21 根,角度 `6.5 + 4.0k`(k=0..20),角宽 `3.108°`
|
||||
- **底弧**:21 根,角度 `6.005 + 3.0k`,角宽 `2.086°`
|
||||
- **四象限**:右上 `+a`、左上 `-a`、左下 `180+a`、右下 `180-a`(角度为自 12 点起顺时针)
|
||||
- **锚点圆点**:半径 `9.00`,位于圆心距 `240.10`、角度 `0 / 93.15 / 180 / 266.85`
|
||||
|
||||
**点亮方向**(与设计稿一致):两条顶弧从 9 点 / 3 点一侧朝 12 点填充;两条底弧从 6 点朝两侧填充。
|
||||
|
||||
每根刻度的颜色是设计稿里**逐根取样**的实测值(`Themes.RINGTOP[21]` / `RINGBOTTOM[21]`),不是插值近似。
|
||||
|
||||
> **已知近似**:设计稿中底弧最多只点亮 14/21 根,因此 `RINGBOTTOM` 的第 15–21 项无法实测,由 `gen_themes.py` 对最后 8 根做最小二乘线性外推得到。只有当指标超过约 67% 时才会显示这几根。
|
||||
|
||||
### 指标进度换算
|
||||
|
||||
| idx | 指标 | 分母 |
|
||||
|-----|------|------|
|
||||
| 0 | 步数 | `stepGoal` |
|
||||
| 1 | 卡路里 | 固定 2000 kcal |
|
||||
| 2 | 距离 | 10 km |
|
||||
| 3 | 楼层 | `floorsClimbedGoal`(缺省 10) |
|
||||
| 4 | 动动条 | `MOVE_BAR_LEVEL_MAX`(=5) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 时间数字的竖向渐变
|
||||
|
||||
设计稿的小时 / 分钟数字带竖向多段渐变(如 Ember 分钟由 `#FEAA00` 渐变到 `#FF2600`)。Monkey C 无法给文字直接填渐变,实现方式是**同一串数字画 11 遍,每遍用 `dc.setClip` 限制在一条横向色带内**,色带颜色取自 `Themes.HOURS` / `Themes.MINUTES`(各 11 个停止点,实测自设计稿的 `linearGradient`)。首尾两条色带向外延伸,保证字形不会有未绘制的缝隙。
|
||||
|
||||
---
|
||||
|
||||
## 5. 字号选择
|
||||
|
||||
Connect IQ 只暴露 `getFontAscent` / `getFontDescent` / `getFontHeight`,**没有字面高度接口**,而 ascent 里含有数字和大写字母用不到的变音符空间。因此代码用两个实测比例把 ascent 折算成 cap height:
|
||||
|
||||
```
|
||||
文本字体(Roboto 系) cap ≈ 0.76 × ascent
|
||||
数字字体(Bionic 系) cap ≈ 0.71 × ascent
|
||||
```
|
||||
|
||||
比例是在模拟器里渲染后、从截图上量出墨迹得到的(fenix847mm:`FONT_XTINY` ascent 29 → 大写高 22px;`FONT_NUMBER_MILD` ascent 82 → 数字高 58px)。`pickFont` 取折算后**最接近**目标 cap height 的字体,而不是"最大且不超过"——后者会在设计尺寸落在两档字体之间时白白小一号。
|
||||
|
||||
各机型内置字体不完全相同(Roboto-Regular / RobotoCondensed-Bold / Yantramanav / 各机型位图字体),比例会有几个百分点误差。设计稿使用的是更粗更方的第三方字体,不随包分发,字形差异无法消除。
|
||||
|
||||
---
|
||||
|
||||
## 6. 七套主题
|
||||
|
||||
由 `tools/gen_themes.py` 从 `design/watchface-0N-*.svg` 生成。每套包含:顶/底弧各 21 根刻度色、未点亮刻度色、锚点色、日期带底/字色、冒号色、小时/分钟各 11 段渐变、强调色(图标与 DST/STEP 标签)、主文字色(各项数值)。
|
||||
|
||||
| # | 主题 | 主色调 | 强调色 | 日期带 |
|
||||
|---|------|--------|--------|--------|
|
||||
| 1 | Ember | 黄 → 红 | `#FEAA05` | 橙底黑字 |
|
||||
| 2 | Aurora | 黄绿 → 青 | `#ADABAE` | 深灰底白字 |
|
||||
| 3 | Brass | 金铜 | `#B57E4B` | 深棕底白字 |
|
||||
| 4 | Voltage | 蓝 → 黄 | `#ADABAE` | 蓝底黑字 |
|
||||
| 5 | Reef | 橙 → 青 | `#FEAA05` | 青底黑字 |
|
||||
| 6 | Acid | 黄绿(近单色) | `#C6FA03` | 深灰底黑字 |
|
||||
| 7 | Kelp | 橙 → 绿 | `#34C294` | 深灰底黑字 |
|
||||
|
||||
改配色:编辑 / 替换 `design/` 里的 SVG,然后
|
||||
|
||||
```bash
|
||||
python3 tools/gen_themes.py && python3 tools/gen_preview_svg.py
|
||||
```
|
||||
|
||||
新增主题需同步在 `settings.xml` 的 `Theme` 列表与 `strings.xml`(含 `resources-chn/`)中登记。
|
||||
|
||||
---
|
||||
|
||||
## 7. 可配置设置与数据字段
|
||||
|
||||
19 项设置,在 Garmin Connect 手机 App 的「表盘设置」里改(定义在 `resources/settings/settings.xml`,默认值在 `properties.xml`,两者由 `tools/gen_fields.py` 生成)。
|
||||
|
||||
| 设置 | 键 | 默认 |
|
||||
|------|----|------|
|
||||
| 主题 | `Theme` | 1 (Ember) |
|
||||
| 四段进度环指标 | `RingTR/TL/BR/BL` | 步数 / 卡路里 / 动动条 / 楼层 |
|
||||
| 四个数据位 | `LeftTop` `RightTop` `BottomLeft` `BottomRight` | 日出 / 高低温 / 距离 / 步数 |
|
||||
| 温度单位 | `TempUnit` | 跟随手表 |
|
||||
| 第二时区 1 / 2 偏移(分钟) | `AltOffset1` `AltOffset2` | 0 |
|
||||
| 自定义文字 1 / 2 / 3 | `Custom1/2/3` | 空 |
|
||||
| 显示顶行 / 日期带 / 底行 | `ShowTop` `ShowBand` `ShowBottom` | true |
|
||||
|
||||
进度环指标:步数、卡路里、距离、楼层、动动条、活动分钟(对周目标)、身体电量。
|
||||
|
||||
### 数据字段
|
||||
|
||||
四个数据位共用一张 **156 项** 的字段表,编号沿用 [watchface.io/docs/datafields](https://watchface.io/docs/datafields),两边可以直接对照。底部两格的小标签跟随所选字段自动变化,不需要单独配置。
|
||||
|
||||
| 分类 | 字段 |
|
||||
|------|------|
|
||||
| 日期时间 | 秒(前导零两种)、时间、UTC 时间、第二时区 1/2、星期月日、星期日、月日、日、星期(简写/全称)、月份(名称/数字)、上午下午、周数(ISO / 日历) |
|
||||
| 活动 | 步数、步数取整、剩余步数、卡路里、活动卡路里、活动分钟(总/中强度/高强度/本周)、距离、上楼层数、下楼层数、爬升高度、推行距离、推行次数、**本周 / 近 7 天 / 近 28 天累计**(步数/卡路里/距离/楼层/活动分钟) |
|
||||
| 身体 | 心率、静息心率、压力指数、血氧、呼吸频率、身体电量、活动条(正/反向)、体重、BMI、恢复时间、最大摄氧量(跑/骑) |
|
||||
| 系统 | 电量、剩余天数、电量+天数、闹钟、通知、勿扰、蓝牙、太阳能强度 |
|
||||
| 环境 | 海拔、海平面气压、环境气压、日出、日落、下一太阳事件、距太阳事件、月龄、月球照明、民用/航海/天文晨昏蒙影(各 2 项)、黄金时刻(晨/昏)、蓝调时刻(晨/昏) |
|
||||
| 天气 | 气温、最高/最低、体感、最高、最低、湿度、降水概率、风速、风向角、风向、更新时间、海平面气压、紫外线、能见度、露点 |
|
||||
| OpenWeatherMap | 气温、体感、最高/最低、天气描述(简/详)、湿度、气压、能见度、风速、风向角、风向、阵风、1h 降雨、1h 降雪、云量、降水概率、露点、明日与后日气温及高低温、城市名、更新时间、**空气质量(AQI 等级 + 描述 + PM2.5 + PM10 + CO)**(共 30 项) |
|
||||
| 系统 Complication | 下一个日程、训练状态、完赛预测(5k/10k/半马/全马)、配速预测(同上四项)、最近高尔夫得分、本周跑步距离、本周骑行距离、第三方 App 1–5 |
|
||||
| 自定义 | 自定义文字 1 / 2 / 3、自定义时间格式、日期格式 1 / 2、事件倒计时 |
|
||||
|
||||
加字段:在 `tools/gen_fields.py` 的 `FIELDS` 表里加一行,跑一遍生成器,再到 `source/Fields.mc` 的 `value()` 里补一个分支。设置项、两种语言的文案、表盘标签会自动跟上。
|
||||
|
||||
### 系统 Complication
|
||||
|
||||
`Toybox.Complications`(CIQ 4.2+,17 款设备全都有)让表盘可以订阅系统和**其它 App 发布**的复杂功能。这条路解锁了一批原本以为做不到的字段:
|
||||
|
||||
- **日历事件** —— CIQ 确实没有日历 API,但系统把「下一个日程」作为 complication 发布了出来
|
||||
- **训练状态、完赛/配速预测、最近高尔夫得分** —— `ActivityMonitor.Info` 和 `UserProfile` 上都没有,这里有
|
||||
- **本周跑步 / 骑行距离** —— 这是**按运动分类**的,`ActivityMonitor` 的按天历史做不出来
|
||||
- **第三方 App 1–5** —— CGM、补水、行情这类 App 发布的复杂功能,按序号取
|
||||
|
||||
需要 `ComplicationSubscriber` 权限。第三方 App 那五个槽位按「有值且有短标签」过滤后按序号取,避免序号随手表上装了什么 App 而漂移。
|
||||
|
||||
### 自定义格式与倒计时
|
||||
|
||||
时间/日期格式串用单字母占位符(设置项能填的长度有限,这样中英文都好写):
|
||||
|
||||
| 占位符 | 含义 | 占位符 | 含义 |
|
||||
|--------|------|--------|------|
|
||||
| `h` / `H` | 小时(跟随 12/24 制)/ 补零 | `D` | 日 |
|
||||
| `m` / `s` | 分钟 / 秒(补零) | `M` / `N` | 月(数字 / 名称) |
|
||||
| `a` | 上午/下午(24 小时制下为空) | `Y` / `y` | 四位年 / 两位年 |
|
||||
| | | `w` | 星期 |
|
||||
|
||||
其它字符原样输出,所以 `D/M` → `10/9`,`w N D` → `周四 9月 10`。
|
||||
|
||||
事件倒计时填 `YYYY-MM-DD`,显示还剩几天。
|
||||
|
||||
### 秒针与后台天气
|
||||
|
||||
**秒**(850/869)通过 `onPartialUpdate` 每秒重绘:只有当某个数据位真的选了秒时才请求这个回调,并且只在该格的裁剪区内重绘,不动整个表盘。常亮的 AMOLED 机型不参与——那时数据位本来就不显示。
|
||||
|
||||
**OpenWeatherMap** 需要用户在设置里填自己的 API key(免费档即可)。没填就完全不联网,25 个 OWM 字段都显示 `--`。填了之后由后台服务每 30 分钟拉一次(当前天气 + `cnt=16` 的短预报,够算出明日/后日高低温),结果压平成一个小字典存进 `Application.Storage`,表盘只读不联网。
|
||||
|
||||
位置来自天气观测点或最近一次 GPS 定位,并缓存在 Storage 里——两个来源都会间歇性为空,缓存让日出和晨昏蒙影不会一会儿有一会儿没有。
|
||||
|
||||
### ⚠️ 仍然没有实现的,以及原因
|
||||
|
||||
| 没做的 | 原因 |
|
||||
|--------|------|
|
||||
| 按运动细分的多数汇总(游泳、自定义运动 1/2 的距离/时长/次数,约 60 项) | `ActivityMonitor.getHistory()` 只有按天总计,没有按运动分类的历史。**跑步和骑行的本周距离例外**——系统把这两项作为 complication 发布了,已经做了;游泳和自定义运动没有对应的 complication 类型 |
|
||||
| 日历事件、计时器 | 日历已通过 Complication 做了。**计时器**仍然做不了:表盘拿不到触摸输入,没法启停 |
|
||||
| StormGlass.io 潮汐(5 项) | 需要额外的付费 API key,且与本表盘的定位无关 |
|
||||
| 第三方 App 复杂功能(CGM、Hydration、QuoteGlance 等 12 项) | 需要用户装了对应的 App,并通过 Complications API 订阅 |
|
||||
| 睡眠得分 | `ActivityMonitor.Info` 上没有,也没有对应的 complication 类型(SDK 9.1.0 实测)。训练状态与完赛/配速预测已改由 Complication 提供 |
|
||||
| 高尔夫成绩 | 无对应 API |
|
||||
| 标记类字段(Marker,约 35 项) | 那是给刻度型表盘做环形标记用的,本表盘的进度环用的是自己的指标体系 |
|
||||
| OWM 的 UVI | 要 One Call 3.0 订阅。**AQI 已经做了**——空气质量走 `/data/2.5/air_pollution`,那个接口在免费档就开放。露点用 Magnus 公式从温湿度算出 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 数据来源与权限
|
||||
|
||||
| 元素 | API |
|
||||
|------|-----|
|
||||
| 步数 / 卡路里 / 距离 / 楼层 / 动动条 / 活动分钟 / 呼吸 / 压力 / 恢复时间 | `ActivityMonitor.getInfo()` |
|
||||
| 心率 | `Activity.getActivityInfo().currentHeartRate`,回退 `ActivityMonitor.getHeartRateHistory()` |
|
||||
| 血氧 / 身体电量 | `SensorHistory`(需 `SensorHistory` 权限) |
|
||||
| 静息心率 / 体重 / 身高 / 最大摄氧量 | `UserProfile.getProfile()`(需 `UserProfile` 权限) |
|
||||
| 海拔 / 环境气压 / 海平面气压 | `Activity.getActivityInfo()` |
|
||||
| 日出 / 日落 | `Weather.getSunrise/getSunset(location, time)`,位置取 `CurrentConditions.observationLocationPosition`,回退 `Activity.currentLocation` |
|
||||
| 气温 / 体感 / 湿度 / 降水 / 风 | `Weather.getCurrentConditions()` |
|
||||
| 最高 / 最低气温 | `Weather.getDailyForecast()[0]` |
|
||||
| 电量 / 剩余天数 / 太阳能 | `System.getSystemStats()`(`battery` 是 0–100 的百分比,不是 0–1) |
|
||||
| 闹钟 / 通知 / 勿扰 / 蓝牙 / 单位制 | `System.getDeviceSettings()` |
|
||||
| 月龄 / 月球照明 | 自算(儒略日 + 朔望月 29.530588853 天) |
|
||||
| 周数 | 自算(ISO 8601 与日历周两种) |
|
||||
|
||||
### 权限
|
||||
|
||||
`manifest.xml` 声明五项:`UserProfile`(静息心率、体重、VO2max)、`SensorHistory`(血氧、身体电量)、`Communications` 与 `Background`(OpenWeatherMap)、`ComplicationSubscriber`(日历、训练状态、第三方 App)。用户安装时会看到授权提示。
|
||||
|
||||
> `Sensor` 权限**不能**用于 `type="watchface"`(除非同时申请 `Background`),编译器会直接拒绝。所以海拔与气压走 `Activity.Info` 而不是 `Sensor.getInfo()`。
|
||||
|
||||
单位与制式跟随手表:`is24Hour`、`distanceUnits`、`temperatureUnits`(温度可用 `TempUnit` 覆盖)。天气与日出需手表已配对并同步过数据,否则显示 `--`。
|
||||
|
||||
---
|
||||
|
||||
## 8.2 常亮显示(AOD)
|
||||
|
||||
AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有烧屏风险,Garmin 上架审核也会卡。`onEnterSleep` / `onExitSleep` 切换到 `drawAmbient()`:
|
||||
|
||||
- 不画日期带底色(整条亮橙色横幅是最费电的元素)
|
||||
- 进度环只画**点亮的**刻度,且整体压暗到 45%
|
||||
- 时间与日期压暗到 55%,其余数据位全部隐藏
|
||||
- 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮
|
||||
|
||||
只在 `DeviceSettings.requiresBurnInProtection` 为真时启用;MIP 机型不受影响,照常全量绘制。
|
||||
|
||||
实测(fenix847mm,取表盘内区):
|
||||
|
||||
| | 点亮像素数 | 平均亮度(功耗代理) |
|
||||
|---|---|---|
|
||||
| 普通模式 | 27.6% | 14.1% |
|
||||
| 常亮模式 | **9.7%** | **2.5%** |
|
||||
|
||||
常亮下砍掉的东西:整条日期带底色、**整个进度环**、四个数据位与电池;保留的时间、冒号、日期压暗到 38%。
|
||||
|
||||
两个口径要分清:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,对大号数字表盘天然不友好;AMOLED 的功耗更接近各子像素亮度之和,也就是表里的「平均亮度」。本实现两个口径都留了余量。
|
||||
|
||||
---
|
||||
|
||||
## 8.5 多语言
|
||||
|
||||
`manifest.xml` 声明了 `eng` 与 `chn`,两套文案分别在 `resources/strings/` 与 `resources-chn/strings/`,由 `monkey.jungle` 的 `base.resourcePath = resources;resources-chn` 同时纳入编译(目录名的 `-chn` 后缀就是语言标记)。手表会按系统语言自动选择。
|
||||
|
||||
**表盘上的文字也走资源**,不是硬编码:
|
||||
|
||||
| 资源 | 英文 | 中文 |
|
||||
|------|------|------|
|
||||
| `DowNames` | `SUN,MON,…,SAT` | `周日,周一,…,周六` |
|
||||
| `MonNames` | `JAN,FEB,…,DEC` | `1月,2月,…,12月` |
|
||||
| `DateFormat` | `$1$ $2$` → `SEP 9` | `$1$$2$日` → `9月9日` |
|
||||
| `Meridiem` | `AM,PM` | `上午,下午` |
|
||||
| `FieldLabels` | `TEMP,KCAL,STEP,DST,…` | `气温,卡路里,步数,距离,…` |
|
||||
|
||||
逗号分隔的列表在 `loadStrings()` 里一次性读入并拆分(`loadResource` 每次调用开销不低,不宜放进 `onUpdate`)。
|
||||
|
||||
> 中文字形依赖手表固件里的 APAC 字体包(`fontSet: apac_twcn` / `apac_chn`)。国行机型自带;国际版若未安装中文字体包,会退回英文或显示方块。模拟器可用 `Settings → Language → Chinese (Simplified)` 验证。
|
||||
|
||||
---
|
||||
|
||||
## 9. 构建
|
||||
|
||||
需安装 Connect IQ SDK 9.1.0,`monkeyc` 在 PATH 中,并备好 `developer_key.der`(**不在仓库内**,需自行生成或提供)。
|
||||
|
||||
```bash
|
||||
monkeyc -o bin/Fenix8V3.prg -f monkey.jungle -y developer_key.der -d fenix847mm
|
||||
```
|
||||
|
||||
全部 17 款设备批量编译(已验证通过):
|
||||
|
||||
```bash
|
||||
for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm fenix8solar51mm \
|
||||
epix2 epix2pro47mm epix2pro51mm fenix7 fenix7x fenix7s fr965 fr265 fr255 \
|
||||
venu3 vivoactive5; do
|
||||
monkeyc -o "bin/Fenix8V3_${d}.prg" -f monkey.jungle -y developer_key.der -d "$d" \
|
||||
&& echo "[$d] OK" || echo "[$d] FAIL"
|
||||
done
|
||||
```
|
||||
|
||||
已验证编译通过的设备:`enduro3, fenix843mm, fenix847mm, fenix8pro47mm, fenix8solar47mm, fenix8solar51mm, epix2, epix2pro47mm, epix2pro51mm, fenix7, fenix7x, fenix7s, fr965, fr265, fr255, venu3, vivoactive5`。产物位于 `bin/`,每个设备对应一个 `.prg` 及 `.prg.debug.xml`(调试版含资源镜像)。
|
||||
屏幕分辨率跨度较大(FR255S 218×218 至 454×454 共 8 档),全部按 `s = width/500` 缩放。
|
||||
|
||||
机型列表和 `monkey.jungle` 由 `tools/gen_devices.py` 扫描本机已安装的 SDK 设备生成:筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」(Complications 需要 4.2)。**注意结果取决于本机装了哪些设备**,跑之前先确认 SDK 设备装全了,脚本会打印找到的数量。
|
||||
|
||||
---
|
||||
|
||||
## 6. 模拟器与侧载
|
||||
## 10. 模拟器与侧载
|
||||
|
||||
1. **Connect IQ Simulator**(Garmin 自带):`File → Open App Project` 选本目录,选设备后 `Run`。在 `bin/*.prg.debug.xml` 旁会生成 `-settings.json`,可在模拟器菜单 `View → Settings` 载入并实时切换主题/指标验证。
|
||||
2. **真机侧载(Garmin Connect / Garmin Express)**:
|
||||
- 用 Garmin Express 或 Connect 手机 App 的“侧载(sideload)”功能导入对应设备的 `bin/Fenix8V3_<device>.prg`;
|
||||
- 或把 `.prg` 拷入手表的 `/GARMIN/Apps/` 目录(需开发者模式 / WiFi 文件传输)。
|
||||
3. 安装后在手表“表盘”列表选择 **Fenix 8 V3**,长按进入设置即可调主题与指标。
|
||||
|
||||
---
|
||||
|
||||
## 7. 目录结构
|
||||
|
||||
```
|
||||
fenix8v3-watchface/
|
||||
├── manifest.xml # 应用清单(ID / 支持设备 / 语言)
|
||||
├── monkey.jungle # 构建配置(manifest + 源码/资源路径)
|
||||
├── developer_key.der # Connect IQ 开发者签名密钥
|
||||
├── source/
|
||||
│ ├── Fenix8V3App.mc # 应用入口:getInitialView / onSettingsChanged
|
||||
│ ├── Fenix8V3View.mc # 表盘渲染主逻辑(onUpdate + 各 draw*)
|
||||
│ └── Themes.mc # ⚠️ 自动生成:7 套主题颜色(勿手改)
|
||||
├── resources/
|
||||
│ ├── strings/strings.xml # AppName / 设置项 / 主题 / 指标 / 字段文案
|
||||
│ └── settings/
|
||||
│ ├── properties.xml # 设置默认值
|
||||
│ └── settings.xml # 设置 UI 定义(列表/布尔)
|
||||
└── bin/ # 编译产物(.prg + 调试 xml + settings.json)
|
||||
```bash
|
||||
connectiq & # 启动模拟器
|
||||
monkeydo bin/Fenix8V3_fenix847mm.prg fenix847mm # 载入表盘
|
||||
```
|
||||
|
||||
---
|
||||
非 release 编译会同时产出 `bin/*-settings.json`,可在模拟器 `View → Settings` 载入以实时切换主题与指标。注意模拟器会**持久化**已保存的设置,改 `properties.xml` 的默认值对已运行过的实例不生效,需先重置应用数据。
|
||||
|
||||
## 8. 二次开发与扩展
|
||||
### 侧载到真机
|
||||
|
||||
- **改主题配色**:编辑 `watchface-tokens.json`(设计稿目录下),运行 `gen_themes.py` 重新生成 `source/Themes.mc`。新增主题时同步在 `Themes.mc` 的 `ORDER` 数组、`settings.xml` 的 `Theme` 列表、`strings.xml` 的 `ThemeXX` 文案中登记。
|
||||
- **加指标**:在 `metricProgress`(进度换算)与 `complicationValue`(复杂功能文本)两个方法里按 `idx` 分支扩展,并在 `settings.xml` / `properties.xml` 的列表项补充。
|
||||
- **调布局**:所有坐标集中在 `Fenix8V3View.mc` 的 `draw*` 方法里,均为 “设计像素 × `s`” 形式,改一处即可全局缩放生效。
|
||||
```bash
|
||||
tools/install.sh # 默认 enduro3
|
||||
tools/install.sh fenix847mm # 指定机型,id 见 manifest.xml
|
||||
```
|
||||
|
||||
脚本会编译 release 版并自动找已挂载的 GARMIN 卷复制过去。找不到卷时会打印
|
||||
`.prg` 的路径和排查提示。
|
||||
|
||||
手动做的话就是:编译出 `bin/Fenix8V3_<机型>.prg`,拷进手表的 `GARMIN/APPS/`,
|
||||
推出后拔线,表上长按 UP/MENU → 表盘 → 选 Fenix 8 V3。再长按 MENU 进表盘设置。
|
||||
|
||||
> macOS 上 fenix 7 之后的机型多数走 MTP,Finder 看不到这个盘。装
|
||||
> [OpenMTP](https://openmtp.ganeshrvel.com)(免费)或 Android File Transfer
|
||||
> 就能拖文件了。
|
||||
|
||||
---
|
||||
|
||||
## 9. 设计稿来源
|
||||
## 11. 预览
|
||||
|
||||
还原基准为 `watchface-kit.zip` 内的 8 张 SVG 变体 + `watchface-tokens.json`(配色)+ `watchface-spec.html`(像素测量规范)+ `rebuild.py`(几何常量)。本仓库为按该规范落地的 Monkey C 实现,设计版权归原作者所有。
|
||||
```bash
|
||||
python3 tools/gen_preview_svg.py
|
||||
```
|
||||
|
||||
产出 `preview-themes.svg`(七套主题一览)与 `preview.html`(可切换主题的单页)。两者用网页字体渲染文字,**几何与配色可信,字形不可信** —— 真机字形以模拟器截图为准。
|
||||
|
||||
---
|
||||
|
||||
## 12. 与设计稿的差异
|
||||
|
||||
1. **字体**。设计稿用的是一款更粗、更方的第三方字体;Garmin 只提供内置 Roboto / Bionic 系列,且各机型不同。字号按 cap height 对齐,字形无法一致。
|
||||
2. **图标**。设计稿里的图标是位图描出的密集矢量路径(数百个点),不适合逐帧绘制;代码用圆、多边形、线段按实测的子形状位置重建了电量、日出/日落、多云三个图标。其余数据项(卡路里 / 步数 / 距离 / 楼层)设计稿未提供图标,为自绘。
|
||||
3. **底弧高段颜色**为外推值,见 §3。
|
||||
4. **进度环是实时数据**,设计稿中的是固定演示状态(右上 21/21、左上 10/21、右下 9/21、左下 14/21)。
|
||||
|
||||
---
|
||||
|
||||
## 12.5 上架素材
|
||||
|
||||
商店后台要填的文案、图标和截图都在 `docs/`,见 `docs/README.md`。
|
||||
|
||||
截图是模拟器的真实渲染(Garmin 内置字体),八个屏宽各一张,外加七套主题各一张。
|
||||
其中**进度环是摆拍的** —— 模拟器灌不进步数和距离,环会全灰,具体说明在
|
||||
`docs/README.md` 里写清楚了。
|
||||
|
||||
---
|
||||
|
||||
## 13. 设计稿版权
|
||||
|
||||
`design/` 内的 SVG、tokens 与 `rebuild.py` 来自 `watchface-kit`,版权归原作者所有。本仓库是按该设计稿实现的 Monkey C 表盘。
|
||||
|
||||
329
design/rebuild.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Watch face vector reconstruction, v2.
|
||||
|
||||
Changes over v1:
|
||||
* ring ticks are annular sectors (constant angular width) instead of rects
|
||||
* every tick's colour is sampled directly from the source, no LUT interpolation
|
||||
* glyph alpha is normalised against the *local* fill colour, so vertically
|
||||
graded digits keep their true weight in the dark rows
|
||||
* time digits get multi-stop gradients sampled every 8 px
|
||||
* sub-pixel geometry throughout
|
||||
"""
|
||||
import json, math, os
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from skimage import measure
|
||||
|
||||
SRC = "/mnt/user-data/uploads"
|
||||
OUT = "/mnt/user-data/outputs"
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
CX_IX, CY_IX = 250.038, 250.049 # dial centre, pixel-index space
|
||||
CX, CY = CX_IX + 0.5, CY_IX + 0.5 # same point in SVG user space
|
||||
PX = 0.5 # index -> SVG offset
|
||||
|
||||
THEMES = [
|
||||
("0fbde6715a76f357bcc5c20cff74c0bf2f156e89.png", "01", "Ember"),
|
||||
("304a904845dbc44aa0c87eecad2dd3a2621d2ffc.png", "02", "Aurora"),
|
||||
("a61e76acdc7f17edfda3b11d36ba5d1c25e0739b.png", "03", "Brass"),
|
||||
("f43ad1228487aa0261183e7268277f0e9ab328f4.png", "04", "Voltage"),
|
||||
("8fd4a2811dc0227b004a5832479cc0a5d4e26a9f.png", "05", "Reef"),
|
||||
("f087f3345e5036bfd406719e8d7517c13109d998.png", "06", "Acid"),
|
||||
("d3a77eea386551ff09b352a77f522bb3c68e925d.png", "07", "Kelp"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------- ring geometry ---
|
||||
R_IN, R_OUT = 231.09, 249.10
|
||||
|
||||
TOP_PHASE, TOP_PITCH, TOP_W, TOP_N = 6.500, 4.000, 3.108, 21
|
||||
BOT_PHASE, BOT_PITCH, BOT_W, BOT_N = 6.005, 3.000, 2.086, 21
|
||||
|
||||
DOT_R, DOT_RAD = 240.10, 9.00
|
||||
DOT_ANGLES = [0.0, 93.15, 180.0, 266.85]
|
||||
|
||||
BAND_Y, BAND_H = 286.30, 44.19
|
||||
COLON = [(237.88, 183.68, 24.24, 21.30), (237.88, 234.57, 24.24, 21.30)]
|
||||
|
||||
|
||||
def tick_angles():
|
||||
"""(group, signed centre angle) for all 86 ticks."""
|
||||
out = []
|
||||
for k in range(TOP_N):
|
||||
a = TOP_PHASE + TOP_PITCH * k
|
||||
out.append(("top-right", a, TOP_W))
|
||||
out.append(("top-left", -a, TOP_W))
|
||||
for k in range(BOT_N):
|
||||
a = BOT_PHASE + BOT_PITCH * k
|
||||
out.append(("bottom-left", 180.0 + a, BOT_W))
|
||||
out.append(("bottom-right", 180.0 - a, BOT_W))
|
||||
return out
|
||||
|
||||
|
||||
def polar(ang, r):
|
||||
t = math.radians(ang - 90.0)
|
||||
return CX + math.cos(t) * r, CY + math.sin(t) * r
|
||||
|
||||
|
||||
def polar_ix(ang, r):
|
||||
t = math.radians(ang - 90.0)
|
||||
return CX_IX + math.cos(t) * r, CY_IX + math.sin(t) * r
|
||||
|
||||
|
||||
def sector_path(centre, width, r0=R_IN, r1=R_OUT):
|
||||
a0, a1 = centre - width / 2, centre + width / 2
|
||||
x1, y1 = polar(a0, r1)
|
||||
x2, y2 = polar(a1, r1)
|
||||
x3, y3 = polar(a1, r0)
|
||||
x4, y4 = polar(a0, r0)
|
||||
return (f"M{x1:.2f},{y1:.2f} A{r1:.2f},{r1:.2f} 0 0 1 {x2:.2f},{y2:.2f} "
|
||||
f"L{x3:.2f},{y3:.2f} A{r0:.2f},{r0:.2f} 0 0 0 {x4:.2f},{y4:.2f} Z")
|
||||
|
||||
|
||||
def sample_tick(arr, centre, width):
|
||||
"""Median colour of a tick's interior (AA edges excluded)."""
|
||||
px = []
|
||||
for r in np.arange(R_IN + 3.5, R_OUT - 3.5, 1.0):
|
||||
for d in np.arange(-width * 0.32, width * 0.32 + 1e-9, width * 0.16):
|
||||
x, y = polar_ix(centre + d, r)
|
||||
px.append(arr[int(round(y)), int(round(x))])
|
||||
return np.median(np.array(px), axis=0)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- tracing ---
|
||||
def rdp(pts, eps):
|
||||
if len(pts) < 3:
|
||||
return pts
|
||||
a, b = pts[0], pts[-1]
|
||||
ab = b - a
|
||||
n = math.hypot(ab[0], ab[1])
|
||||
if n < 1e-9:
|
||||
d = np.hypot(*(pts - a).T)
|
||||
else:
|
||||
d = np.abs(ab[0] * (pts[:, 1] - a[1]) - ab[1] * (pts[:, 0] - a[0])) / n
|
||||
i = int(np.argmax(d))
|
||||
if d[i] > eps:
|
||||
return np.vstack([rdp(pts[: i + 1], eps)[:-1], rdp(pts[i:], eps)])
|
||||
return np.vstack([a, b])
|
||||
|
||||
|
||||
def trace(alpha, x0, y0, eps=0.07, ss=6):
|
||||
half = 0.5 / ss # supersampled index -> source continuous coordinate
|
||||
up = np.asarray(Image.fromarray((np.clip(alpha, 0, 1) * 255).astype(np.uint8))
|
||||
.resize((alpha.shape[1] * ss, alpha.shape[0] * ss), Image.BICUBIC)) / 255.0
|
||||
up = np.pad(up, 1, mode="constant")
|
||||
parts = []
|
||||
for c in measure.find_contours(up, 0.5):
|
||||
c = (c - 1) / ss
|
||||
pts = np.column_stack([c[:, 1] + x0 + half, c[:, 0] + y0 + half])
|
||||
if len(pts) < 10:
|
||||
continue
|
||||
pts = rdp(pts, eps)
|
||||
if len(pts) < 4:
|
||||
continue
|
||||
parts.append("M" + " ".join(f"{x:.2f},{y:.2f}" for x, y in pts[:-1]) + "Z")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def full_colour(reg, q=0.97):
|
||||
"""Colour of a fully-opaque pixel in a solid-filled region."""
|
||||
flat = reg.reshape(-1, 3)
|
||||
l = flat.mean(axis=1)
|
||||
cut = np.quantile(l, q)
|
||||
sel = flat[l >= cut]
|
||||
return sel.mean(axis=0)
|
||||
|
||||
|
||||
def alpha_solid(arr, box):
|
||||
x0, y0, x1, y1 = box
|
||||
reg = arr[y0:y1, x0:x1].astype(float)
|
||||
e = full_colour(reg)
|
||||
a = reg.mean(axis=2) / max(e.mean(), 1e-6)
|
||||
return np.clip(a, 0, 1), e, x0, y0
|
||||
|
||||
|
||||
def row_ramp(reg):
|
||||
"""Per-row fully-opaque colour for a vertically graded glyph."""
|
||||
h = reg.shape[0]
|
||||
ys, cols = [], []
|
||||
for y in range(h):
|
||||
row = reg[y]
|
||||
l = row.mean(axis=1)
|
||||
if l.max() < 30:
|
||||
continue
|
||||
sel = row[l >= l.max() * 0.92]
|
||||
if len(sel) < 3:
|
||||
continue
|
||||
ys.append(y)
|
||||
cols.append(sel.mean(axis=0))
|
||||
if len(ys) < 4:
|
||||
return None, None
|
||||
ys = np.array(ys)
|
||||
cols = np.array(cols)
|
||||
# smooth then extend to every row
|
||||
out = np.zeros((h, 3))
|
||||
for c in range(3):
|
||||
out[:, c] = np.interp(np.arange(h), ys, np.convolve(
|
||||
cols[:, c], np.ones(5) / 5, mode="same") if len(ys) > 8 else cols[:, c])
|
||||
out[: ys[0], c] = cols[0, c]
|
||||
out[ys[-1] + 1:, c] = cols[-1, c]
|
||||
return out, (ys[0], ys[-1])
|
||||
|
||||
|
||||
def alpha_graded(arr, box):
|
||||
x0, y0, x1, y1 = box
|
||||
reg = arr[y0:y1, x0:x1].astype(float)
|
||||
ramp, span = row_ramp(reg)
|
||||
if ramp is None:
|
||||
return alpha_solid(arr, box) + (None,)
|
||||
denom = np.maximum(ramp.mean(axis=1), 1e-6)[:, None]
|
||||
a = np.clip(reg.mean(axis=2) / denom, 0, 1)
|
||||
return a, ramp, x0, y0, span
|
||||
|
||||
|
||||
def alpha_on_fill(arr, box, fill):
|
||||
x0, y0, x1, y1 = box
|
||||
reg = arr[y0:y1, x0:x1].astype(float)
|
||||
fill = np.array(fill, float)
|
||||
d = reg - fill
|
||||
far = d.reshape(-1, 3)
|
||||
mag = np.linalg.norm(far, axis=1)
|
||||
e = far[mag >= np.quantile(mag, 0.97)].mean(axis=0)
|
||||
a = (d @ e) / max(e @ e, 1e-6)
|
||||
return np.clip(a, 0, 1), fill + e, x0, y0
|
||||
|
||||
|
||||
def hx(c):
|
||||
return "#%02X%02X%02X" % tuple(int(round(min(255, max(0, v)))) for v in c)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- elements ---
|
||||
SOLID = [
|
||||
("battery-icon", (188, 38, 228, 68)),
|
||||
("battery-value", (228, 38, 312, 70)),
|
||||
("sunrise-icon", (136, 72, 178, 108)),
|
||||
("sunrise-value", (106, 110, 208, 152)),
|
||||
("weather-icon", (322, 70, 364, 108)),
|
||||
("weather-value", (258, 110, 428, 152)),
|
||||
("dist-value", (166, 345, 240, 388)),
|
||||
("dist-label", (143, 390, 238, 432)),
|
||||
("steps-value", (261, 345, 366, 388)),
|
||||
("steps-label", (263, 390, 386, 432)),
|
||||
]
|
||||
BAND_EL = [
|
||||
("bt-icon", (20, 289, 54, 326)),
|
||||
("date-day", (132, 290, 223, 326)),
|
||||
("date-md", (225, 290, 370, 326)),
|
||||
("meridiem", (404, 290, 472, 326)),
|
||||
]
|
||||
TIME_L = (76, 162, 218, 278)
|
||||
TIME_R = (264, 162, 434, 278)
|
||||
|
||||
|
||||
def grad_def(gid, ramp, y0, span):
|
||||
ya, yb = y0 + span[0] + 0.5, y0 + span[1] + 0.5
|
||||
n = max(2, int((span[1] - span[0]) // 8))
|
||||
stops = []
|
||||
for i in range(n + 1):
|
||||
f = i / n
|
||||
yy = int(round(span[0] + f * (span[1] - span[0])))
|
||||
stops.append(f'<stop offset="{f:.3f}" stop-color="{hx(ramp[yy])}"/>')
|
||||
return (f'<linearGradient id="{gid}" x1="0" y1="{ya}" x2="0" y2="{yb}" '
|
||||
f'gradientUnits="userSpaceOnUse">{"".join(stops)}</linearGradient>')
|
||||
|
||||
|
||||
def build(path, tid, tname):
|
||||
arr = np.array(Image.open(os.path.join(SRC, path)).convert("RGB")).astype(int)
|
||||
defs, parts, tok = [], [], {}
|
||||
|
||||
parts.append('<rect id="background" width="500" height="500" fill="#000000"/>')
|
||||
|
||||
# ---- ring: every tick measured individually
|
||||
groups = {}
|
||||
for gname, centre, width in tick_angles():
|
||||
raw = sample_tick(arr, centre, width)
|
||||
if raw.mean() < 12: # slot occupied by an anchor dot / not drawn
|
||||
continue
|
||||
col = hx(raw)
|
||||
groups.setdefault(gname, []).append(
|
||||
f'<path d="{sector_path(centre, width)}" fill="{col}"/>')
|
||||
ring = []
|
||||
for g in ("top-right", "top-left", "bottom-left", "bottom-right"):
|
||||
ring.append(f'<g id="ring-{g}">\n ' + "\n ".join(groups[g]) + "\n </g>")
|
||||
dot_col = hx(np.median(arr[4:13, 245:255].reshape(-1, 3), axis=0))
|
||||
dots = []
|
||||
for a in DOT_ANGLES:
|
||||
x, y = polar(a, DOT_R)
|
||||
dots.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{DOT_RAD}" fill="{dot_col}"/>')
|
||||
ring.append('<g id="ring-anchors">\n ' + "\n ".join(dots) + "\n </g>")
|
||||
parts.append('<g id="ring">\n ' + "\n ".join(ring) + "\n</g>")
|
||||
tok["anchorDot"] = dot_col
|
||||
|
||||
# ---- date band
|
||||
fill = arr[300, 110].astype(float)
|
||||
band = [f'<rect x="0" y="{BAND_Y}" width="500" height="{BAND_H}" fill="{hx(fill)}"/>']
|
||||
band_text = None
|
||||
for name, box in BAND_EL:
|
||||
a, e, x0, y0 = alpha_on_fill(arr, box, fill)
|
||||
d = trace(a, x0, y0)
|
||||
if not d:
|
||||
continue
|
||||
band_text = hx(e)
|
||||
band.append(f'<path id="{name}" d="{d}" fill="{band_text}" fill-rule="evenodd"/>')
|
||||
parts.append('<g id="date-band">\n ' + "\n ".join(band) + "\n</g>")
|
||||
tok["bandFill"], tok["bandText"] = hx(fill), band_text
|
||||
|
||||
# ---- time
|
||||
tg = []
|
||||
for gid, name, box in (("gHours", "time-hours", TIME_L),
|
||||
("gMinutes", "time-minutes", TIME_R)):
|
||||
a, ramp, x0, y0, span = alpha_graded(arr, box)
|
||||
if span is None:
|
||||
continue
|
||||
defs.append(grad_def(gid, ramp, y0, span))
|
||||
tok[gid] = [hx(ramp[span[0]]), hx(ramp[span[1]])]
|
||||
tg.append((name, f'<path id="{name}" d="{trace(a, x0, y0)}" '
|
||||
f'fill="url(#{gid})" fill-rule="evenodd"/>'))
|
||||
colon_col = hx(np.median(arr[188:200, 242:258].reshape(-1, 3), axis=0))
|
||||
tok["colon"] = colon_col
|
||||
colon = '<g id="time-colon">' + "".join(
|
||||
f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="{colon_col}"/>'
|
||||
for x, y, w, h in COLON) + "</g>"
|
||||
parts.append('<g id="time">\n ' + tg[0][1] + "\n " + colon + "\n " + tg[1][1] + "\n</g>")
|
||||
|
||||
# ---- complications
|
||||
comp = []
|
||||
for name, box in SOLID:
|
||||
a, e, x0, y0 = alpha_solid(arr, box)
|
||||
d = trace(a, x0, y0)
|
||||
if not d:
|
||||
continue
|
||||
comp.append(f'<path id="{name}" d="{d}" fill="{hx(e)}" fill-rule="evenodd"/>')
|
||||
tok[name] = hx(e)
|
||||
parts.append('<g id="complications">\n ' + "\n ".join(comp) + "\n</g>")
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" '
|
||||
'height="500" shape-rendering="geometricPrecision">\n'
|
||||
f'<title>Watch face {tid} {tname}</title>\n'
|
||||
f'<defs>{"".join(defs)}</defs>\n' + "\n".join(parts) + "\n</svg>\n")
|
||||
p = os.path.join(OUT, f"watchface-{tid}-{tname.lower()}.svg")
|
||||
open(p, "w").write(svg)
|
||||
|
||||
# ring colour table for the spec
|
||||
tok["ringTop"] = {f"{TOP_PHASE + TOP_PITCH * k:.2f}":
|
||||
hx(sample_tick(arr, TOP_PHASE + TOP_PITCH * k, TOP_W))
|
||||
for k in range(TOP_N)}
|
||||
tok["ringBottom"] = {f"{BOT_PHASE + BOT_PITCH * k:.2f}":
|
||||
hx(sample_tick(arr, 180 + BOT_PHASE + BOT_PITCH * k, BOT_W))
|
||||
for k in range(BOT_N)}
|
||||
return p, tok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tokens = {}
|
||||
for p, tid, tname in THEMES:
|
||||
out, tok = build(p, tid, tname)
|
||||
tok["name"] = tname
|
||||
tokens[tid] = tok
|
||||
print("wrote", out)
|
||||
json.dump(tokens, open(os.path.join(OUT, "watchface-tokens.json"), "w"), indent=2)
|
||||
129
design/watchface-01-ember.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
129
design/watchface-02-aurora.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
129
design/watchface-03-brass.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
129
design/watchface-04-voltage.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
129
design/watchface-05-reef.svg
Normal file
|
After Width: | Height: | Size: 60 KiB |
129
design/watchface-06-acid.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
129
design/watchface-07-kelp.svg
Normal file
|
After Width: | Height: | Size: 59 KiB |
499
design/watchface-tokens.json
Normal file
@@ -0,0 +1,499 @@
|
||||
{
|
||||
"01": {
|
||||
"anchorDot": "#525551",
|
||||
"bandFill": "#FEAA00",
|
||||
"bandText": "#000002",
|
||||
"gHours": [
|
||||
"#6F6F6F",
|
||||
"#4A4A4B"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#7E5501",
|
||||
"#931500"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#FEAA03",
|
||||
"battery-value": "#FFFFFF",
|
||||
"sunrise-icon": "#FEAA01",
|
||||
"sunrise-value": "#FFFFFF",
|
||||
"weather-icon": "#FEAA05",
|
||||
"weather-value": "#FFFFFF",
|
||||
"dist-value": "#FFFFFF",
|
||||
"dist-label": "#FEAB05",
|
||||
"steps-value": "#FFFFFF",
|
||||
"steps-label": "#FEAA05",
|
||||
"ringTop": {
|
||||
"6.50": "#FFF200",
|
||||
"10.50": "#FFE601",
|
||||
"14.50": "#FFDB01",
|
||||
"18.50": "#FFCE00",
|
||||
"22.50": "#FFC200",
|
||||
"26.50": "#FEB600",
|
||||
"30.50": "#FEAA00",
|
||||
"34.50": "#FF9D01",
|
||||
"38.50": "#FF9100",
|
||||
"42.50": "#FE8600",
|
||||
"46.50": "#FE7900",
|
||||
"50.50": "#FE6E00",
|
||||
"54.50": "#FF6100",
|
||||
"58.50": "#FF5500",
|
||||
"62.50": "#FF4801",
|
||||
"66.50": "#FF3C01",
|
||||
"70.50": "#FF3002",
|
||||
"74.50": "#FF2600",
|
||||
"78.50": "#FF2600",
|
||||
"82.50": "#FF2600",
|
||||
"86.50": "#FF2600"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#AD1602",
|
||||
"9.00": "#AD1602",
|
||||
"12.00": "#B21902",
|
||||
"15.00": "#B32301",
|
||||
"18.00": "#B43102",
|
||||
"21.00": "#BC3C01",
|
||||
"24.00": "#C54801",
|
||||
"27.00": "#C45501",
|
||||
"30.00": "#CE6100",
|
||||
"33.01": "#CC6D00",
|
||||
"36.01": "#D57900",
|
||||
"39.01": "#D58401",
|
||||
"42.01": "#DD9201",
|
||||
"45.01": "#DE9D02",
|
||||
"48.01": "#525551",
|
||||
"51.01": "#525551",
|
||||
"54.01": "#525551",
|
||||
"57.01": "#525551",
|
||||
"60.01": "#525551",
|
||||
"63.01": "#525551",
|
||||
"66.00": "#525551"
|
||||
},
|
||||
"name": "Ember"
|
||||
},
|
||||
"02": {
|
||||
"anchorDot": "#0F3C82",
|
||||
"bandFill": "#525551",
|
||||
"bandText": "#FFFFFF",
|
||||
"gHours": [
|
||||
"#6F6F6F",
|
||||
"#2F312F"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#567D01",
|
||||
"#036E6F"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#ACAAAD",
|
||||
"battery-value": "#FFFFFF",
|
||||
"sunrise-icon": "#ACAAAD",
|
||||
"sunrise-value": "#FFFFFF",
|
||||
"weather-icon": "#ADABAE",
|
||||
"weather-value": "#FFFFFF",
|
||||
"dist-value": "#FFFFFF",
|
||||
"dist-label": "#ADABAE",
|
||||
"steps-value": "#FFFFFF",
|
||||
"steps-label": "#ADABAE",
|
||||
"ringTop": {
|
||||
"6.50": "#A4F909",
|
||||
"10.50": "#A4F918",
|
||||
"14.50": "#9DF91E",
|
||||
"18.50": "#9EFA31",
|
||||
"22.50": "#95FA39",
|
||||
"26.50": "#94F94A",
|
||||
"30.50": "#8AFA52",
|
||||
"34.50": "#8BFA61",
|
||||
"38.50": "#84FA6A",
|
||||
"42.50": "#85FA7C",
|
||||
"46.50": "#7BFA82",
|
||||
"50.50": "#7BFA94",
|
||||
"54.50": "#73FB9B",
|
||||
"58.50": "#74FBAB",
|
||||
"62.50": "#6BFBB4",
|
||||
"66.50": "#6AFCC5",
|
||||
"70.50": "#63FCCD",
|
||||
"74.50": "#62FCDE",
|
||||
"78.50": "#5AFCE6",
|
||||
"82.50": "#59FDF6",
|
||||
"86.50": "#52FDFF"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#52FDFF",
|
||||
"9.00": "#59FCF6",
|
||||
"12.00": "#5AFCE6",
|
||||
"15.00": "#62FCDE",
|
||||
"18.00": "#62FCCD",
|
||||
"21.00": "#6AFCC5",
|
||||
"24.00": "#6BFBB4",
|
||||
"27.00": "#74FBAB",
|
||||
"30.00": "#73FB9B",
|
||||
"33.01": "#7BFA94",
|
||||
"36.01": "#7BFA83",
|
||||
"39.01": "#85FA7C",
|
||||
"42.01": "#84FA6A",
|
||||
"45.01": "#8BFA61",
|
||||
"48.01": "#525551",
|
||||
"51.01": "#525551",
|
||||
"54.01": "#525551",
|
||||
"57.01": "#525551",
|
||||
"60.01": "#525551",
|
||||
"63.01": "#525551",
|
||||
"66.00": "#525551"
|
||||
},
|
||||
"name": "Aurora"
|
||||
},
|
||||
"03": {
|
||||
"anchorDot": "#62441E",
|
||||
"bandFill": "#392810",
|
||||
"bandText": "#FFFFFF",
|
||||
"gHours": [
|
||||
"#6F6F6F",
|
||||
"#2F312F"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#78674B",
|
||||
"#4D3620"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#B67E4B",
|
||||
"battery-value": "#F0CF98",
|
||||
"sunrise-icon": "#B57D4A",
|
||||
"sunrise-value": "#F0CF97",
|
||||
"weather-icon": "#B57E4B",
|
||||
"weather-value": "#F0CF97",
|
||||
"dist-value": "#F0CF97",
|
||||
"dist-label": "#B57E4C",
|
||||
"steps-value": "#F0CF97",
|
||||
"steps-label": "#B57E4C",
|
||||
"ringTop": {
|
||||
"6.50": "#B3814A",
|
||||
"10.50": "#B5854B",
|
||||
"14.50": "#B48951",
|
||||
"18.50": "#BD8E52",
|
||||
"22.50": "#BD915B",
|
||||
"26.50": "#C5955B",
|
||||
"30.50": "#C59962",
|
||||
"34.50": "#C59D62",
|
||||
"38.50": "#CD9D6B",
|
||||
"42.50": "#CCA169",
|
||||
"46.50": "#CCA573",
|
||||
"50.50": "#D6AA73",
|
||||
"54.50": "#D5AE7B",
|
||||
"58.50": "#D5B37A",
|
||||
"62.50": "#DEB682",
|
||||
"66.50": "#DEBA83",
|
||||
"70.50": "#DEBD8B",
|
||||
"74.50": "#E5C28B",
|
||||
"78.50": "#E6C693",
|
||||
"82.50": "#E6CA94",
|
||||
"86.50": "#EFCE95"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#B47D4A",
|
||||
"9.00": "#B3814A",
|
||||
"12.00": "#B4844A",
|
||||
"15.00": "#B48951",
|
||||
"18.00": "#BD8E52",
|
||||
"21.00": "#BD915B",
|
||||
"24.00": "#C5955B",
|
||||
"27.00": "#C49962",
|
||||
"30.00": "#C59962",
|
||||
"33.01": "#CD9D6A",
|
||||
"36.01": "#CDA169",
|
||||
"39.01": "#CCA572",
|
||||
"42.01": "#D6AA73",
|
||||
"45.01": "#D5AE7B",
|
||||
"48.01": "#392810",
|
||||
"51.01": "#392710",
|
||||
"54.01": "#392810",
|
||||
"57.01": "#392810",
|
||||
"60.01": "#392810",
|
||||
"63.01": "#392810",
|
||||
"66.00": "#392810"
|
||||
},
|
||||
"name": "Brass"
|
||||
},
|
||||
"04": {
|
||||
"anchorDot": "#53AAFF",
|
||||
"bandFill": "#53AAFF",
|
||||
"bandText": "#000000",
|
||||
"gHours": [
|
||||
"#6F6F6F",
|
||||
"#2F312F"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#29557F",
|
||||
"#848301"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#ACAAAD",
|
||||
"battery-value": "#55ABFF",
|
||||
"sunrise-icon": "#ACAAAD",
|
||||
"sunrise-value": "#55ABFF",
|
||||
"weather-icon": "#ADABAE",
|
||||
"weather-value": "#55ABFF",
|
||||
"dist-value": "#55ABFF",
|
||||
"dist-label": "#ADABAE",
|
||||
"steps-value": "#55ABFF",
|
||||
"steps-label": "#ADABAE",
|
||||
"ringTop": {
|
||||
"6.50": "#5AAEEE",
|
||||
"10.50": "#62B2E5",
|
||||
"14.50": "#6AB6DF",
|
||||
"18.50": "#72BACC",
|
||||
"22.50": "#7CBEC5",
|
||||
"26.50": "#83C1B4",
|
||||
"30.50": "#8CC6AC",
|
||||
"34.50": "#94C99C",
|
||||
"38.50": "#9DCE94",
|
||||
"42.50": "#A3D283",
|
||||
"46.50": "#ACD67A",
|
||||
"50.50": "#B5DA6A",
|
||||
"54.50": "#BEDD62",
|
||||
"58.50": "#C5E252",
|
||||
"62.50": "#CDE64B",
|
||||
"66.50": "#D5EA3A",
|
||||
"70.50": "#DEED30",
|
||||
"74.50": "#E7F221",
|
||||
"78.50": "#EDF617",
|
||||
"82.50": "#F6FA08",
|
||||
"86.50": "#FFFB01"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#FFFB01",
|
||||
"9.00": "#F6FA08",
|
||||
"12.00": "#EDF617",
|
||||
"15.00": "#E7F221",
|
||||
"18.00": "#DEED30",
|
||||
"21.00": "#D5EA38",
|
||||
"24.00": "#CDE64B",
|
||||
"27.00": "#C5E252",
|
||||
"30.00": "#BEDE62",
|
||||
"33.01": "#B5DA6A",
|
||||
"36.01": "#ACD67A",
|
||||
"39.01": "#A3D182",
|
||||
"42.01": "#9CCE94",
|
||||
"45.01": "#94C99C",
|
||||
"48.01": "#525551",
|
||||
"51.01": "#525551",
|
||||
"54.01": "#525551",
|
||||
"57.01": "#525551",
|
||||
"60.01": "#525551",
|
||||
"63.01": "#525551",
|
||||
"66.00": "#525551"
|
||||
},
|
||||
"name": "Voltage"
|
||||
},
|
||||
"05": {
|
||||
"anchorDot": "#FE7900",
|
||||
"bandFill": "#52FDFF",
|
||||
"bandText": "#000000",
|
||||
"gHours": [
|
||||
"#6F6F6F",
|
||||
"#2F312F"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#7F3C01",
|
||||
"#2B8384"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#FEAA03",
|
||||
"battery-value": "#FFFFFF",
|
||||
"sunrise-icon": "#FEAA01",
|
||||
"sunrise-value": "#FFFFFF",
|
||||
"weather-icon": "#FEAA05",
|
||||
"weather-value": "#FFFFFF",
|
||||
"dist-value": "#FFFFFF",
|
||||
"dist-label": "#FEAB05",
|
||||
"steps-value": "#FFFFFF",
|
||||
"steps-label": "#FEAA05",
|
||||
"ringTop": {
|
||||
"6.50": "#F57D07",
|
||||
"10.50": "#ED8618",
|
||||
"14.50": "#E68D20",
|
||||
"18.50": "#DE9130",
|
||||
"22.50": "#D49939",
|
||||
"26.50": "#CC9D49",
|
||||
"30.50": "#C5A552",
|
||||
"34.50": "#BCAA61",
|
||||
"38.50": "#B3B26A",
|
||||
"42.50": "#ADB67C",
|
||||
"46.50": "#A4BE83",
|
||||
"50.50": "#9CC694",
|
||||
"54.50": "#94CA9C",
|
||||
"58.50": "#8CD2AB",
|
||||
"62.50": "#83D6B5",
|
||||
"66.50": "#7BDEC5",
|
||||
"70.50": "#74E6CE",
|
||||
"74.50": "#6AEADF",
|
||||
"78.50": "#63F2E7",
|
||||
"82.50": "#5BF6F6",
|
||||
"86.50": "#52FDFF"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#FE7900",
|
||||
"9.00": "#F57C07",
|
||||
"12.00": "#ED8618",
|
||||
"15.00": "#E68920",
|
||||
"18.00": "#DE9130",
|
||||
"21.00": "#D49939",
|
||||
"24.00": "#CD9D49",
|
||||
"27.00": "#C5A552",
|
||||
"30.00": "#BCAA61",
|
||||
"33.01": "#B3B26A",
|
||||
"36.01": "#ADB67C",
|
||||
"39.01": "#A4BE83",
|
||||
"42.01": "#9CC694",
|
||||
"45.01": "#94C99C",
|
||||
"48.01": "#525551",
|
||||
"51.01": "#525551",
|
||||
"54.01": "#525551",
|
||||
"57.01": "#525551",
|
||||
"60.01": "#525551",
|
||||
"63.01": "#525551",
|
||||
"66.00": "#525551"
|
||||
},
|
||||
"name": "Reef"
|
||||
},
|
||||
"06": {
|
||||
"anchorDot": "#F58909",
|
||||
"bandFill": "#525551",
|
||||
"bandText": "#000000",
|
||||
"gHours": [
|
||||
"#85961E",
|
||||
"#556D01"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#769600",
|
||||
"#556D01"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#C6FA03",
|
||||
"battery-value": "#FFFFFF",
|
||||
"sunrise-icon": "#C5FA01",
|
||||
"sunrise-value": "#FFFFFF",
|
||||
"weather-icon": "#C7FA03",
|
||||
"weather-value": "#FFFFFF",
|
||||
"dist-value": "#FFFFFF",
|
||||
"dist-label": "#C6FA04",
|
||||
"steps-value": "#FFFFFF",
|
||||
"steps-label": "#C7FA03",
|
||||
"ringTop": {
|
||||
"6.50": "#9CCA0B",
|
||||
"10.50": "#9CCF08",
|
||||
"14.50": "#A4CE08",
|
||||
"18.50": "#A4D208",
|
||||
"22.50": "#A4D60A",
|
||||
"26.50": "#A4D60A",
|
||||
"30.50": "#ABDA06",
|
||||
"34.50": "#ADDA00",
|
||||
"38.50": "#ADDE02",
|
||||
"42.50": "#ADDE02",
|
||||
"46.50": "#B4E200",
|
||||
"50.50": "#B4E600",
|
||||
"54.50": "#B4E600",
|
||||
"58.50": "#B4EA00",
|
||||
"62.50": "#B4EA00",
|
||||
"66.50": "#BDEE00",
|
||||
"70.50": "#BDF101",
|
||||
"74.50": "#BDF101",
|
||||
"78.50": "#BEF600",
|
||||
"82.50": "#C6F600",
|
||||
"86.50": "#C5FA00"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#C5FA00",
|
||||
"9.00": "#C6F600",
|
||||
"12.00": "#BEF600",
|
||||
"15.00": "#BDF101",
|
||||
"18.00": "#BDF101",
|
||||
"21.00": "#BDEE00",
|
||||
"24.00": "#B4EA00",
|
||||
"27.00": "#B4EA00",
|
||||
"30.00": "#B4E600",
|
||||
"33.01": "#B4E600",
|
||||
"36.01": "#B4E100",
|
||||
"39.01": "#ACDE01",
|
||||
"42.01": "#ADDE02",
|
||||
"45.01": "#ADD900",
|
||||
"48.01": "#525551",
|
||||
"51.01": "#525551",
|
||||
"54.01": "#525551",
|
||||
"57.01": "#525551",
|
||||
"60.01": "#525551",
|
||||
"63.01": "#525551",
|
||||
"66.00": "#525551"
|
||||
},
|
||||
"name": "Acid"
|
||||
},
|
||||
"07": {
|
||||
"anchorDot": "#F58909",
|
||||
"bandFill": "#525551",
|
||||
"bandText": "#000000",
|
||||
"gHours": [
|
||||
"#935205",
|
||||
"#844103"
|
||||
],
|
||||
"gMinutes": [
|
||||
"#935205",
|
||||
"#762E05"
|
||||
],
|
||||
"colon": "#525551",
|
||||
"battery-icon": "#34C294",
|
||||
"battery-value": "#FFFFFF",
|
||||
"sunrise-icon": "#32C294",
|
||||
"sunrise-value": "#FFFFFF",
|
||||
"weather-icon": "#34C294",
|
||||
"weather-value": "#FFFFFF",
|
||||
"dist-value": "#FFFFFF",
|
||||
"dist-label": "#34C294",
|
||||
"steps-value": "#FFFFFF",
|
||||
"steps-label": "#34C294",
|
||||
"ringTop": {
|
||||
"6.50": "#EE8910",
|
||||
"10.50": "#DD8D17",
|
||||
"14.50": "#D49117",
|
||||
"18.50": "#CD911F",
|
||||
"22.50": "#C59528",
|
||||
"26.50": "#BE9931",
|
||||
"30.50": "#B49D3A",
|
||||
"34.50": "#AB9D39",
|
||||
"38.50": "#A3A141",
|
||||
"42.50": "#94A54A",
|
||||
"46.50": "#8BA552",
|
||||
"50.50": "#83AA5A",
|
||||
"54.50": "#7BAA5A",
|
||||
"58.50": "#73AE63",
|
||||
"62.50": "#69B269",
|
||||
"66.50": "#63B273",
|
||||
"70.50": "#52B67C",
|
||||
"74.50": "#4BBA82",
|
||||
"78.50": "#42B983",
|
||||
"82.50": "#39BD8B",
|
||||
"86.50": "#31C293"
|
||||
},
|
||||
"ringBottom": {
|
||||
"6.00": "#CD5008",
|
||||
"9.00": "#C5550E",
|
||||
"12.00": "#BD5A1A",
|
||||
"15.00": "#B46118",
|
||||
"18.00": "#AC651F",
|
||||
"21.00": "#A46A29",
|
||||
"24.00": "#9B712F",
|
||||
"27.00": "#9C763A",
|
||||
"30.00": "#94793A",
|
||||
"33.01": "#8B8241",
|
||||
"36.01": "#83854A",
|
||||
"39.01": "#7A8952",
|
||||
"42.01": "#74915A",
|
||||
"45.01": "#6A955A",
|
||||
"48.01": "#525551",
|
||||
"51.01": "#525551",
|
||||
"54.01": "#525551",
|
||||
"57.01": "#525551",
|
||||
"60.01": "#525551",
|
||||
"63.01": "#525551",
|
||||
"66.00": "#525551"
|
||||
},
|
||||
"name": "Kelp"
|
||||
}
|
||||
}
|
||||
72
docs/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 上架素材
|
||||
|
||||
Connect IQ Store 后台要填的东西都在这个目录里。代码不依赖这里的任何文件。
|
||||
|
||||
```
|
||||
docs/
|
||||
├── store-listing-zh.md 中文商店文案(名称 / 简介 / 详细描述 / 关键词 / 权限说明)
|
||||
├── store-listing-en.md 英文商店文案
|
||||
├── store-icon-1024.png 商店图标(大)
|
||||
├── store-icon-512.png 商店图标(小)
|
||||
└── screenshots/
|
||||
├── 218x218.png ~ 454x454.png 八个屏宽各一张
|
||||
└── theme-01-ember.png ~ theme-07-kelp.png 七套主题(454px)
|
||||
```
|
||||
|
||||
## 截图是怎么来的
|
||||
|
||||
全部是 **Connect IQ 模拟器的真实渲染**,不是网页预览图 —— 字体是 Garmin 内置的
|
||||
Roboto / Bionic,和真机一致。`preview-themes.svg` 那张用的是网页字体,**不要**拿去
|
||||
当商店截图。
|
||||
|
||||
每个屏宽各挑了一台代表机型:
|
||||
|
||||
| 尺寸 | 机型 | 尺寸 | 机型 |
|
||||
|------|------|------|------|
|
||||
| 218 | FR255S | 360 | Venu 2S |
|
||||
| 240 | fenix 7S | 390 | Vivoactive 5 |
|
||||
| 260 | fenix 7 | 416 | fenix 8 43mm |
|
||||
| 280 | Enduro 3 | 454 | fenix 8 47mm |
|
||||
|
||||
截图里显示的数据位是 **日出 / 最高最低气温 / 心率 / 身体电量** ——
|
||||
这四项在模拟器里有真实取值,不是默认配置(默认是日出 / 高低温 / 距离 / 步数)。
|
||||
换掉后两项是因为模拟器灌不进步数和距离,默认配置截出来底部是 `0.0` 和 `0`。
|
||||
|
||||
### ⚠️ 进度环是摆拍的
|
||||
|
||||
模拟器的「Edit Activity Monitor Info」对话框只能改活动分钟、恢复时间、呼吸和
|
||||
压力,**改不了步数 / 距离 / 卡路里**(那几个输入框是 Java 自绘的表格,
|
||||
脚本也够不着)。所以进度环在模拟器里永远是全灰的,截图没法看。
|
||||
|
||||
截图构建把四段弧固定成了设计稿的填充状态(右上 21/21、左上 10/21、
|
||||
右下 9/21、左下 14/21)。**除进度环外,画面上每个数字都是模拟器的真实取值**,
|
||||
没有伪造。
|
||||
|
||||
如果要出完全真实的截图,把 `.prg` 侧载到真机戴一天再拍。
|
||||
|
||||
## 重新生成
|
||||
|
||||
图标:
|
||||
|
||||
```bash
|
||||
python3 tools/gen_icons.py # 顺带产出 store-icon 用的同款视觉
|
||||
```
|
||||
|
||||
截图:本目录下的 PNG 是用模拟器抓的,流程见提交记录里的 `shot.sh` / `theme.sh`
|
||||
(脚本在临时目录,没有入库 —— 它们依赖具体的窗口位置,换台机器就要重调)。
|
||||
手工重做的话:编译 → `monkeydo` 载入 → 截模拟器窗口 → 按日期带定位裁出表盘圆形区。
|
||||
|
||||
## 后台还要填但不在这里的
|
||||
|
||||
- **分类**:Watch Faces
|
||||
- **支持的机型**:55 款,以 `manifest.xml` 的 `<iq:products>` 为准
|
||||
- **价格**:免费 / 付费自行决定
|
||||
- **隐私政策**:如果启用 OpenWeatherMap,需要说明会把**坐标**发给
|
||||
`api.openweathermap.org`。不填 key 时完全不联网,这一点建议在描述里写清楚
|
||||
- **联系邮箱 / 支持链接**
|
||||
|
||||
## ⚠️ 发布前必须先解决
|
||||
|
||||
表盘的视觉是对第三方 `watchface-kit` 设计稿的 1:1 还原(见仓库根目录的
|
||||
`LICENSE` 与 `README.md` 第 13 节)。**以自己的名义上架前,需要先取得原作者
|
||||
授权,或把视觉部分替换成原创设计。** 这是法律问题,不是技术问题。
|
||||
BIN
docs/screenshots/218x218.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/screenshots/240x240.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/screenshots/260x260.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/screenshots/280x280.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/screenshots/360x360.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/screenshots/390x390.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/screenshots/416x416.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
docs/screenshots/454x454.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/screenshots/theme-01-ember.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/screenshots/theme-02-aurora.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/screenshots/theme-03-brass.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/screenshots/theme-04-voltage.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/screenshots/theme-05-reef.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/screenshots/theme-06-acid.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/screenshots/theme-07-kelp.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/store-icon-1024.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/store-icon-512.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
90
docs/store-listing-en.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Connect IQ Store listing (English)
|
||||
|
||||
## App name
|
||||
|
||||
```
|
||||
Fenix 8 V3
|
||||
```
|
||||
|
||||
## Short description
|
||||
|
||||
```
|
||||
A dense data watch face: four progress arcs, four configurable slots, 156 data
|
||||
fields to choose from, seven colour themes.
|
||||
```
|
||||
|
||||
## Full description
|
||||
|
||||
```
|
||||
Fenix 8 V3 packs a lot of information onto the dial without making it feel
|
||||
crowded.
|
||||
|
||||
■ Four progress arcs
|
||||
The rim is split into four independent arcs. Each one tracks a metric of your
|
||||
choosing - steps, calories, distance, floors, move bar, active minutes or body
|
||||
battery - so a glance tells you where the day stands. Every tick is
|
||||
individually coloured, so the arc shifts hue as it fills.
|
||||
|
||||
■ Four data slots, 156 fields
|
||||
Top-left, top-right, bottom-left and bottom-right. Each can show any of:
|
||||
|
||||
· Date & time seconds, UTC, two alternate time zones, week number
|
||||
(ISO or calendar), custom time and date formats
|
||||
· Activity steps, calories, distance, floors, ascent, active minutes,
|
||||
plus week-to-date, 7-day and 28-day totals
|
||||
· Body heart rate, resting HR, stress, pulse ox, respiration,
|
||||
body battery, weight, BMI, recovery time, VO2 max
|
||||
· System battery, battery in days, alarms, notifications, do not
|
||||
disturb, bluetooth, solar intensity
|
||||
· Environment altitude, pressure, sunrise/sunset, next sun event, moon age
|
||||
and illumination, civil/nautical/astronomical twilight,
|
||||
golden hour, blue hour
|
||||
· Weather temperature, feels-like, high/low, humidity, precipitation,
|
||||
wind speed and direction, UV index, visibility, dew point
|
||||
· From watch next calendar event, training status, race and pace
|
||||
predictors (5k/10k/half/marathon), weekly run and bike
|
||||
distance, and complications published by your other apps
|
||||
· Custom three custom text lines, event countdown
|
||||
|
||||
The captions under the bottom two slots follow whatever you pick - no separate
|
||||
setting to keep in sync.
|
||||
|
||||
■ Seven themes
|
||||
Ember, Aurora, Brass, Voltage, Reef, Acid and Kelp. Each carries its own arc
|
||||
gradient, date band colours and time-digit gradient.
|
||||
|
||||
■ Always-on display
|
||||
On AMOLED watches the face switches to a low-power layout while asleep: the
|
||||
date band fill and the progress ring are dropped, the time and date are dimmed,
|
||||
and the whole layout drifts a few pixels each minute to avoid burn-in.
|
||||
|
||||
■ OpenWeatherMap (optional)
|
||||
Add your own OpenWeatherMap API key for 30 further weather fields, including
|
||||
air quality (AQI, PM2.5, PM10, CO) and the next two days' forecast. Without a
|
||||
key the watch face never touches the network.
|
||||
|
||||
■ Also
|
||||
· English and Simplified Chinese - weekday, month and field captions all follow
|
||||
the watch language
|
||||
· Temperature unit can be forced to Celsius or Fahrenheit; distance and
|
||||
12/24-hour follow the watch
|
||||
· The bluetooth glyph dims when the phone is out of range
|
||||
```
|
||||
|
||||
## Keywords / tags
|
||||
|
||||
```
|
||||
data field, progress ring, complications, always-on, AMOLED, weather,
|
||||
heart rate, body battery, sunrise sunset, chinese
|
||||
```
|
||||
|
||||
## Permissions to explain to users
|
||||
|
||||
```
|
||||
· UserProfile - resting heart rate, weight, VO2 max
|
||||
· SensorHistory - pulse ox, body battery
|
||||
· Communications + Background - only used to fetch weather, and only when an
|
||||
OpenWeatherMap API key has been entered
|
||||
· ComplicationSubscriber - calendar, training status, and data published by
|
||||
other apps
|
||||
```
|
||||
75
docs/store-listing-zh.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Connect IQ Store 上架文案(简体中文)
|
||||
|
||||
## 应用名称
|
||||
|
||||
```
|
||||
Fenix 8 V3
|
||||
```
|
||||
|
||||
## 一句话简介(短描述)
|
||||
|
||||
```
|
||||
多指标数据表盘:四段进度环 + 四个可自由配置的数据位,156 项数据任选,七套配色。
|
||||
```
|
||||
|
||||
## 详细描述
|
||||
|
||||
```
|
||||
Fenix 8 V3 是一款信息密度高、但不拥挤的数据表盘。
|
||||
|
||||
■ 四段进度环
|
||||
表盘外圈是四段独立的进度弧,各自绑定一项指标(步数、卡路里、距离、楼层、
|
||||
动动条、活动分钟、身体电量),一眼就能看出今天的完成度。每根刻度的颜色都
|
||||
是渐变的,进度越高颜色越靠近弧段末端的色相。
|
||||
|
||||
■ 四个数据位,156 项数据任选
|
||||
左上、右上、左下、右下四格,每格都能从 156 项数据里任选:
|
||||
|
||||
· 时间日期 秒、UTC、第二时区 ×2、周数(ISO / 日历)、自定义时间与日期格式
|
||||
· 活动 步数、卡路里、距离、楼层、爬升、活动分钟,以及本周 / 近 7 天 /
|
||||
近 28 天累计
|
||||
· 身体 心率、静息心率、压力、血氧、呼吸、身体电量、体重、BMI、
|
||||
恢复时间、最大摄氧量
|
||||
· 系统 电量、剩余天数、闹钟、通知、勿扰、蓝牙、太阳能强度
|
||||
· 环境 海拔、气压、日出日落、下一太阳事件、月龄月相,
|
||||
以及民用 / 航海 / 天文晨昏蒙影、黄金时刻、蓝调时刻
|
||||
· 天气 气温、体感、最高最低、湿度、降水概率、风速风向、紫外线、
|
||||
能见度、露点
|
||||
· 手表数据 下一个日程、训练状态、完赛与配速预测(5k/10k/半马/全马)、
|
||||
本周跑步与骑行距离、以及其它 App 发布的复杂功能
|
||||
· 自定义 三段自定义文字、事件倒计时
|
||||
|
||||
底部两格的小标签会跟着所选数据自动变化,不用另外设置。
|
||||
|
||||
■ 七套配色
|
||||
余烬、极光、黄铜、电压、珊瑚礁、酸绿、海藻。每套都有自己的进度环渐变、
|
||||
日期带配色和时间数字渐变。
|
||||
|
||||
■ 常亮显示
|
||||
AMOLED 机型支持常亮。常亮时自动切换到低功耗版面:去掉日期带底色和进度环,
|
||||
时间与日期压暗,整个画面每分钟在几个像素内轻微游走以避免烧屏。
|
||||
|
||||
■ OpenWeatherMap(选用)
|
||||
填入自己的 OpenWeatherMap API key 后,可额外获得 30 项天气数据,包括空气质量
|
||||
(AQI、PM2.5、PM10、CO)与未来两日预报。不填则完全不联网。
|
||||
|
||||
■ 其它
|
||||
· 简体中文 / 英文双语,表盘上的星期、月份、数据标签都会跟随手表语言
|
||||
· 温度单位可强制摄氏或华氏,距离与时间制式跟随手表设置
|
||||
· 手机断连时蓝牙图标自动压暗
|
||||
```
|
||||
|
||||
## 关键词 / 标签
|
||||
|
||||
```
|
||||
数据表盘, 进度环, 数据字段, 常亮, AMOLED, 天气, 心率, 身体电量, 日出日落, 中文
|
||||
```
|
||||
|
||||
## 需要向用户说明的权限
|
||||
|
||||
```
|
||||
· 用户档案 —— 用于静息心率、体重、最大摄氧量
|
||||
· 传感器历史 —— 用于血氧、身体电量
|
||||
· 通信与后台 —— 仅在填写了 OpenWeatherMap API key 时用于拉取天气
|
||||
· 复杂功能订阅 —— 用于日程、训练状态、以及其它 App 发布的数据
|
||||
```
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 纯标准库生成表盘运行效果 SVG(复用 Fenix8V3View.mc 的坐标与主题色)
|
||||
import math
|
||||
|
||||
THEMES = {
|
||||
"01":{"name":"Ember","ringTop":[[8,0xFFF203],[15,0xFDDB0D],[25,0xFFC203],[35,0xFFA00A],[45,0xFF8705],[55,0xFF610C],[65,0xFF4904],[75,0xFF2602],[86,0xFF2604]],
|
||||
"ringBottom":[[7,0xAE1706],[14,0xB32504],[21,0xC03B03],[28,0xCD6206],[35,0xD57805],[42,0xDE9208],[47,0xC98D00]],
|
||||
"tickOff":0x545655,"anchorDot":0x525551,"bandFill":0xFEAA00,"bandText":0x000002,"accent":0xFCA901,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xFCA901},
|
||||
"02":{"name":"Aurora","ringTop":[[8,0xA5FA11],[15,0x9EFA23],[25,0x97F84B],[35,0x8CF965],[45,0x89F87F],[55,0x74FB9F],[65,0x6DFCC6],[75,0x64FCDF],[86,0x57FDFF]],
|
||||
"ringBottom":[[7,0x55FDFF],[14,0x64FCDF],[21,0x6FFCC6],[28,0x74FBAE],[35,0x7EFA85],[42,0x88F96D],[47,0x81E654]],
|
||||
"tickOff":0x545655,"anchorDot":0x0F3C82,"bandFill":0x525551,"bandText":0xFFFFFF,"accent":0xABA9AC,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xAAF701},
|
||||
"03":{"name":"Brass","ringTop":[[8,0xB4814F],[15,0xB88954],[25,0xC5975C],[35,0xC59E65],[45,0xCBA775],[55,0xD7AE7F],[65,0xDEBC84],[75,0xE7C48D],[86,0xF0D097]],
|
||||
"ringBottom":[[7,0xB97E4A],[14,0xB68954],[21,0xBD925E],[28,0xC69B67],[35,0xCEA26C],[42,0xD9AB74],[47,0xC59E70]],
|
||||
"tickOff":0x3A2815,"anchorDot":0x62441E,"bandFill":0x392810,"bandText":0xFFFFFF,"accent":0xB37C4A,"textPrimary":0xEBCB93,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xECCC93},
|
||||
"04":{"name":"Voltage","ringTop":[[8,0x5CAFF0],[15,0x6EB6E0],[25,0x7FBEC5],[35,0x96CA9D],[45,0xAAD87F],[55,0xBFDF66],[65,0xD6EA40],[75,0xE8F227],[86,0xFFFB0A]],
|
||||
"ringBottom":[[7,0xF7FA11],[14,0xE9F225],[21,0xD6EB3F],[28,0xC0DE65],[35,0xADD880],[42,0xA0CF98],[47,0x88B98E]],
|
||||
"tickOff":0x545655,"anchorDot":0x53AAFF,"bandFill":0x53AAFF,"bandText":0x000000,"accent":0xABA9AC,"textPrimary":0x51A7FA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0x52A8FD},
|
||||
"05":{"name":"Reef","ringTop":[[8,0xF87E0D],[15,0xE78E25],[25,0xCE9B4D],[35,0xBDAC65],[45,0xA5BE88],[55,0x95CA9E],[65,0x7EDEC7],[75,0x6DEBE1],[86,0x57FDFF]],
|
||||
"ringBottom":[[7,0xFF7905],[14,0xE78B29],[21,0xD59C3E],[28,0xBEAC63],[35,0xADB77F],[42,0xA0C796],[47,0x88B98E]],
|
||||
"tickOff":0x545655,"anchorDot":0xFE7900,"bandFill":0x52FDFF,"bandText":0x000000,"accent":0xFCA901,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xFBFBFB,"minutes":0xFC7801},
|
||||
"06":{"name":"Acid","ringTop":[[8,0x9CCC14],[15,0xA9CE0C],[25,0xA4D610],[35,0xACDB08],[45,0xB4E107],[55,0xB5E603],[65,0xBFEE04],[75,0xBDF108],[86,0xC7F808]],
|
||||
"ringBottom":[[7,0xC7FA02],[14,0xBEF20A],[21,0xC0ED04],[28,0xB4E905],[35,0xB4E107],[42,0xADDD0C],[47,0x9BC603]],
|
||||
"tickOff":0x545655,"anchorDot":0xF58909,"bandFill":0x525551,"bandText":0x000000,"accent":0xC4F801,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xDBF731,"minutes":0xC4F900},
|
||||
"07":{"name":"Kelp","ringTop":[[8,0xEF8917],[15,0xD8901C],[25,0xBD9A34],[35,0xAD9E3C],[45,0x96A54E],[55,0x7DAE5D],[65,0x61B474],[75,0x4EBB84],[86,0x36C294]],
|
||||
"ringBottom":[[7,0xCE520F],[14,0xB1631D],[21,0xA26A2F],[28,0x9D763E],[35,0x85874B],[42,0x76915D],[47,0x5E8651]],
|
||||
"tickOff":0x545655,"anchorDot":0xF58909,"bandFill":0x525551,"bandText":0x000000,"accent":0x31C192,"textPrimary":0xFAFAFA,"colon":0x525551,"hours":0xF28708,"minutes":0xF4880A},
|
||||
}
|
||||
ORDER=["01","02","03","04","05","06","07"]
|
||||
TOP=[a for a in range(2,87,4)]; BOT=[a for a in range(6,67,3)]
|
||||
DOW=["SUN","MON","TUE","WED","THU","FRI","SAT"]; MON=["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]
|
||||
|
||||
def hx(c): return "#%06x"%(c&0xffffff)
|
||||
def lerp(c1,c2,f):
|
||||
r1=(c1>>16)&255;g1=(c1>>8)&255;b1=c1&255
|
||||
r2=(c2>>16)&255;g2=(c2>>8)&255;b2=c2&255
|
||||
return ((round(r1+(r2-r1)*f))<<16)|((round(g1+(g2-g1)*f))<<8)|round(b1+(b2-b1)*f)
|
||||
def lerp_stops(stops,t):
|
||||
n=len(stops)
|
||||
if not n: return 0x888888
|
||||
if t<=stops[0][0]: return stops[0][1]
|
||||
if t>=stops[-1][0]: return stops[-1][1]
|
||||
for i in range(n-1):
|
||||
a,b=stops[i],stops[i+1]
|
||||
if a[0]<=t<=b[0]:
|
||||
return lerp(a[1],b[1],(t-a[0])/(b[0]-a[0]))
|
||||
return stops[-1][1]
|
||||
|
||||
def tick(cx,cy,rIn,rOut,w,ang,col,mirror):
|
||||
eff=-ang if mirror else ang; rad=math.radians(eff); s=math.sin(rad); c=math.cos(rad)
|
||||
return f'<line x1="{cx+s*rIn:.2f}" y1="{cy-c*rIn:.2f}" x2="{cx+s*rOut:.2f}" y2="{cy-c*rOut:.2f}" stroke="{hx(col)}" stroke-width="{w}" stroke-linecap="round"/>'
|
||||
|
||||
def ring(theme,pTR,pTL,pBR,pBL):
|
||||
out=[]
|
||||
rOut=249.5;rIn=231.5;off=theme["tickOff"];ts=theme["ringTop"];bs=theme["ringBottom"]
|
||||
topW=13;botW=9
|
||||
litTR=round(pTR*22);litTL=round(pTL*22);litBR=round(pBR*21);litBL=round(pBL*21)
|
||||
# TR anchorOuter, mirror=false
|
||||
for i,a in enumerate(TOP):
|
||||
on=i>=(22-litTR); col=lerp_stops(ts,a) if on else off
|
||||
out.append(tick(250,250,rIn,rOut,topW,a,col,False))
|
||||
# TL anchorOuter, mirror=true
|
||||
for i,a in enumerate(TOP):
|
||||
on=i>=(22-litTL); col=lerp_stops(ts,a) if on else off
|
||||
out.append(tick(250,250,rIn,rOut,topW,a,col,True))
|
||||
# BL mirror=true
|
||||
for i,a in enumerate(BOT):
|
||||
col=lerp_stops(bs,a) if i<litBL else off
|
||||
out.append(tick(250,250,rIn,rOut,botW,180-a,col,True))
|
||||
# BR mirror=false
|
||||
for i,a in enumerate(BOT):
|
||||
col=lerp_stops(bs,a) if i<litBR else off
|
||||
out.append(tick(250,250,rIn,rOut,botW,180-a,col,False))
|
||||
# anchor dots
|
||||
dc=theme["anchorDot"];dr=9.5;drad=240.5
|
||||
for da in [0,93.3,180,266.7]:
|
||||
rad=math.radians(da)
|
||||
out.append(f'<circle cx="{250+math.sin(rad)*drad:.2f}" cy="{250-math.cos(rad)*drad:.2f}" r="{dr}" fill="{hx(dc)}"/>')
|
||||
return "".join(out)
|
||||
|
||||
def batt(theme,pct):
|
||||
y=53.5;gw=30;gh=16;gx=250-52;gy=y-gh/2;ac=theme["accent"];tp=theme["textPrimary"]
|
||||
fill_w=(gw-4)*pct/100
|
||||
return (f'<rect x="{gx}" y="{gy}" width="{gw}" height="{gh}" fill="none" stroke="{hx(ac)}" stroke-width="2"/>'
|
||||
f'<rect x="{gx+gw}" y="{gy+gh*0.28}" width="3" height="{gh*0.44}" fill="{hx(ac)}"/>'
|
||||
f'<rect x="{gx+2}" y="{gy+2}" width="{fill_w:.2f}" height="{gh-4}" fill="{hx(ac)}"/>'
|
||||
f'<text x="{250+16}" y="{y}" fill="{hx(tp)}" font-size="16" font-family="sans-serif" text-anchor="start" dominant-baseline="central">{round(pct)}%</text>')
|
||||
|
||||
def complic_icon(x,y,r,col,t):
|
||||
c=hx(col)
|
||||
if t==0: # sun
|
||||
s=f'<circle cx="{x}" cy="{y}" r="{r*0.45}" fill="{c}"/>'
|
||||
for i in range(8):
|
||||
rad=math.radians(i*45)
|
||||
s+=f'<line x1="{x+math.sin(rad)*r*0.6:.2f}" y1="{y-math.cos(rad)*r*0.6:.2f}" x2="{x+math.sin(rad)*r:.2f}" y2="{y-math.cos(rad)*r:.2f}" stroke="{c}" stroke-width="2"/>'
|
||||
return s
|
||||
if t==1: # flame
|
||||
return f'<polygon points="{x},{y-r} {x+r*0.8:.2f},{y+r*0.6:.2f} {x-r*0.8:.2f},{y+r*0.6:.2f}" fill="{c}"/>'
|
||||
if t==2: # steps
|
||||
return (f'<line x1="{x-r*0.5:.2f}" y1="{y-r*0.3:.2f}" x2="{x+r*0.5:.2f}" y2="{y-r*0.3:.2f}" stroke="{c}" stroke-width="2"/>'
|
||||
f'<line x1="{x-r*0.3:.2f}" y1="{y}" x2="{x+r*0.6:.2f}" y2="{y}" stroke="{c}" stroke-width="2"/>')
|
||||
if t==3: # dot
|
||||
return f'<circle cx="{x}" cy="{y}" r="{r*0.5}" fill="{c}"/>'
|
||||
if t==4: # chevron
|
||||
return f'<polygon points="{x},{y-r} {x+r*0.7:.2f},{y+r*0.2:.2f} {x-r*0.7:.2f},{y+r*0.2:.2f}" fill="{c}"/>'
|
||||
return f'<rect x="{x-r*0.6:.2f}" y="{y-r*0.4:.2f}" width="{r*1.2:.2f}" height="{r*0.8:.2f}" fill="none" stroke="{c}" stroke-width="2"/>'
|
||||
|
||||
def top_comp(theme,t,isLeft,val):
|
||||
sx=156 if isLeft else 343; iy=90; vy=130.5
|
||||
return (f'<text x="{sx}" y="{vy}" fill="{hx(theme["textPrimary"])}" font-size="22" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">{val}</text>'
|
||||
+complic_icon(sx,iy,13,theme["accent"],t))
|
||||
|
||||
def bluetooth(x,y,r,col):
|
||||
c=hx(col)
|
||||
return (f'<path d="M{x},{y-r} L{x},{y+r} M{x},{y-r} L{x+r*0.7:.2f},{y-r*0.3:.2f} L{x},{y} '
|
||||
f'L{x+r*0.7:.2f},{y+r*0.3:.2f} L{x},{y+r}" fill="none" stroke="{c}" stroke-width="3" stroke-linejoin="round"/>')
|
||||
|
||||
def date_band(theme,gi):
|
||||
by=286.5;bh=44;yc=308.5;bf=theme["bandFill"];bt=theme["bandText"]
|
||||
dow=DOW[gi["dow"]-1]; md=MON[gi["m"]-1]+" "+str(gi["day"])
|
||||
return (f'<rect x="0" y="{by}" width="500" height="{bh}" fill="{hx(bf)}"/>'
|
||||
+bluetooth(36.5,yc,10,bt)
|
||||
+f'<text x="177.5" y="{yc}" fill="{hx(bt)}" font-size="16" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">{dow}</text>'
|
||||
+f'<text x="297.5" y="{yc}" fill="{hx(bt)}" font-size="16" font-family="sans-serif" text-anchor="middle" dominant-baseline="central">{md}</text>'
|
||||
+f'<text x="465" y="{yc}" fill="{hx(bt)}" font-size="16" font-family="sans-serif" text-anchor="end" dominant-baseline="central">{"PM" if gi["h"]>=12 else "AM"}</text>')
|
||||
|
||||
def time(theme,gi):
|
||||
h=gi["h"]; h=12 if h==0 else (h-12 if h>12 else h)
|
||||
hs=str(h); ms="%02d"%gi["min"]; ty=220
|
||||
return (f'<text x="211" y="{ty}" fill="{hx(theme["hours"])}" font-size="66" font-weight="700" font-family="sans-serif" text-anchor="end" dominant-baseline="central">{hs}</text>'
|
||||
f'<text x="272" y="{ty}" fill="{hx(theme["minutes"])}" font-size="66" font-weight="700" font-family="sans-serif" text-anchor="start" dominant-baseline="central">{ms}</text>'
|
||||
f'<rect x="238" y="184" width="24" height="20" fill="{hx(theme["colon"])}"/>'
|
||||
f'<rect x="238" y="235" width="24" height="20" fill="{hx(theme["colon"])}"/>')
|
||||
|
||||
def bottom(theme,dist,steps):
|
||||
vy=366;ly=410.5;lx=234;rx=266;tp=theme["textPrimary"];ac=theme["accent"]
|
||||
return (f'<text x="{lx}" y="{vy}" fill="{hx(tp)}" font-size="22" font-family="sans-serif" text-anchor="end" dominant-baseline="central">{dist}</text>'
|
||||
f'<text x="{lx}" y="{ly}" fill="{hx(ac)}" font-size="16" font-family="sans-serif" text-anchor="end" dominant-baseline="central">DST</text>'
|
||||
f'<text x="{rx}" y="{vy}" fill="{hx(tp)}" font-size="22" font-family="sans-serif" text-anchor="start" dominant-baseline="central">{steps}</text>'
|
||||
f'<text x="{rx}" y="{ly}" fill="{hx(ac)}" font-size="16" font-family="sans-serif" text-anchor="start" dominant-baseline="central">STEP</text>')
|
||||
|
||||
def face(theme_id,m,battery,temp,gi):
|
||||
th=THEMES[theme_id]
|
||||
p=[m[0],m[1],m[2],m[3],m[4]]
|
||||
idxTR,idxTL,idxBR,idxBL=0,1,4,3
|
||||
s=[]
|
||||
s.append(f'<rect x="0" y="0" width="500" height="500" fill="#000"/>')
|
||||
s.append(ring(th,p[idxTR],p[idxTL],p[idxBR],p[idxBL]))
|
||||
s.append(batt(th,battery))
|
||||
s.append(top_comp(th,1,True,f'{round(m[1]*2000):,}')) # 左上 卡路里
|
||||
s.append(top_comp(th,0,False,f'{temp}\u00b0')) # 右上 天气
|
||||
s.append(time(th,gi))
|
||||
s.append(date_band(th,gi))
|
||||
s.append(bottom(th,f'{m[2]*10:.1f}',f'{round(m[0]*10000):,}'))
|
||||
return "".join(s)
|
||||
|
||||
# 七主题合成图:单排行,每格 230px,圆形裁切
|
||||
m=[0.84,0.64,0.65,0.80,0.40]; battery=78; temp=23
|
||||
gi={"h":10,"min":8,"day":9,"m":9,"dow":3}
|
||||
cell=230; gap=24; n=len(ORDER); W=n*cell+(n+1)*gap; H=cell+70
|
||||
parts=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}">']
|
||||
parts.append(f'<rect width="{W}" height="{H}" fill="#0a0a0a"/>')
|
||||
for i,tid in enumerate(ORDER):
|
||||
ox=gap+i*(cell+gap); oy=24
|
||||
sc=cell/500.0
|
||||
parts.append(f'<defs><clipPath id="c{i}"><circle cx="{ox+cell/2}" cy="{oy+cell/2}" r="{cell/2-2}"/></clipPath></defs>')
|
||||
parts.append(f'<g clip-path="url(#c{i})"><g transform="translate({ox},{oy}) scale({sc:.4f})">'+face(tid,m,battery,temp,gi)+'</g></g>')
|
||||
parts.append(f'<text x="{ox+cell/2}" y="{oy+cell+22}" fill="#cfccc4" font-size="15" font-family="sans-serif" text-anchor="middle">{i+1} · {THEMES[tid]["name"]}</text>')
|
||||
parts.append('</svg>')
|
||||
svg="\n".join(parts)
|
||||
with open("/Users/ericwyuan/Desktop/Work/fenix8v3-watchface/preview-themes.svg","w") as f:
|
||||
f.write(svg)
|
||||
print("written preview-themes.svg", len(svg), "bytes; size", W,"x",H)
|
||||
66
manifest.xml
@@ -7,26 +7,64 @@
|
||||
minSdkVersion="4.2.0"
|
||||
name="@Strings.AppName"
|
||||
type="watchface"
|
||||
version="1.0.0">
|
||||
version="1.1.0">
|
||||
|
||||
<iq:products>
|
||||
<iq:product id="approachs50"/>
|
||||
<iq:product id="approachs7042mm"/>
|
||||
<iq:product id="approachs7047mm"/>
|
||||
<iq:product id="d2airx10"/>
|
||||
<iq:product id="d2mach1"/>
|
||||
<iq:product id="d2mach2"/>
|
||||
<iq:product id="descentg2"/>
|
||||
<iq:product id="descentmk343mm"/>
|
||||
<iq:product id="descentmk351mm"/>
|
||||
<iq:product id="enduro3"/>
|
||||
<iq:product id="epix2"/>
|
||||
<iq:product id="epix2pro42mm"/>
|
||||
<iq:product id="epix2pro47mm"/>
|
||||
<iq:product id="epix2pro51mm"/>
|
||||
<iq:product id="fenix7"/>
|
||||
<iq:product id="fenix7pro"/>
|
||||
<iq:product id="fenix7pronowifi"/>
|
||||
<iq:product id="fenix7s"/>
|
||||
<iq:product id="fenix7spro"/>
|
||||
<iq:product id="fenix7x"/>
|
||||
<iq:product id="fenix7xpro"/>
|
||||
<iq:product id="fenix7xpronowifi"/>
|
||||
<iq:product id="fenix843mm"/>
|
||||
<iq:product id="fenix847mm"/>
|
||||
<iq:product id="fenix8pro47mm"/>
|
||||
<iq:product id="fenix8solar47mm"/>
|
||||
<iq:product id="fenix8solar51mm"/>
|
||||
<iq:product id="epix2"/>
|
||||
<iq:product id="epix2pro47mm"/>
|
||||
<iq:product id="epix2pro51mm"/>
|
||||
<iq:product id="fenix7"/>
|
||||
<iq:product id="fenix7x"/>
|
||||
<iq:product id="fenix7s"/>
|
||||
<iq:product id="fr965"/>
|
||||
<iq:product id="fr265"/>
|
||||
<iq:product id="fenixe"/>
|
||||
<iq:product id="fr165"/>
|
||||
<iq:product id="fr165m"/>
|
||||
<iq:product id="fr255"/>
|
||||
<iq:product id="fr255m"/>
|
||||
<iq:product id="fr255s"/>
|
||||
<iq:product id="fr255sm"/>
|
||||
<iq:product id="fr265"/>
|
||||
<iq:product id="fr265s"/>
|
||||
<iq:product id="fr57042mm"/>
|
||||
<iq:product id="fr57047mm"/>
|
||||
<iq:product id="fr955"/>
|
||||
<iq:product id="fr965"/>
|
||||
<iq:product id="fr970"/>
|
||||
<iq:product id="instinct3amoled45mm"/>
|
||||
<iq:product id="instinct3amoled50mm"/>
|
||||
<iq:product id="instinctcrossoveramoled"/>
|
||||
<iq:product id="marq2"/>
|
||||
<iq:product id="marq2aviator"/>
|
||||
<iq:product id="venu2"/>
|
||||
<iq:product id="venu2plus"/>
|
||||
<iq:product id="venu2s"/>
|
||||
<iq:product id="venu3"/>
|
||||
<iq:product id="venu3s"/>
|
||||
<iq:product id="venu441mm"/>
|
||||
<iq:product id="venu445mm"/>
|
||||
<iq:product id="vivoactive5"/>
|
||||
<iq:product id="vivoactive6"/>
|
||||
</iq:products>
|
||||
|
||||
<iq:languages>
|
||||
@@ -34,5 +72,15 @@
|
||||
<iq:language>eng</iq:language>
|
||||
</iq:languages>
|
||||
|
||||
<!-- Sensor is rejected for type="watchface" unless Background is also
|
||||
requested, so altitude and pressure come from Activity.Info instead. -->
|
||||
<iq:permissions>
|
||||
<iq:uses-permission id="UserProfile"/>
|
||||
<iq:uses-permission id="Communications"/>
|
||||
<iq:uses-permission id="Background"/>
|
||||
<iq:uses-permission id="ComplicationSubscriber"/>
|
||||
<iq:uses-permission id="SensorHistory"/>
|
||||
</iq:permissions>
|
||||
|
||||
</iq:application>
|
||||
</iq:manifest>
|
||||
|
||||
@@ -1,9 +1,69 @@
|
||||
project.manifest = manifest.xml
|
||||
|
||||
base.sourcePath = source
|
||||
base.resourcePath = resources
|
||||
base.excludeAnnotations = release
|
||||
|
||||
# Uncomment and adjust once you have SDK devices installed, e.g.:
|
||||
# fenix8.resourcePath = resources;resources-round-amoled
|
||||
# fenix7.resourcePath = resources;resources-round-mip
|
||||
# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。
|
||||
base.resourcePath = resources;resources-chn
|
||||
|
||||
# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:
|
||||
# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标
|
||||
# resources-launcher-<尺寸> 该机型要求尺寸的启动图标
|
||||
# 这一整段由 tools/gen_devices.py 生成,不要手改。
|
||||
|
||||
approachs50.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
|
||||
approachs7042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
approachs7047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
|
||||
d2airx10.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
|
||||
d2mach1.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
d2mach2.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
descentg2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
descentmk343mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
descentmk351mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
|
||||
enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
|
||||
epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
epix2pro42mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
|
||||
fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fenix7pro.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fenix7pronowifi.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fenix7s.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
|
||||
fenix7spro.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
|
||||
fenix7x.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
|
||||
fenix7xpro.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
|
||||
fenix7xpronowifi.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
|
||||
fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
fenix8pro47mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
|
||||
fenixe.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
fr165.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
|
||||
fr165m.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
|
||||
fr255.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fr255m.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fr255s.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40
|
||||
fr255sm.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40
|
||||
fr265.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
fr265s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-60
|
||||
fr57042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
|
||||
fr57047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
fr955.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
|
||||
fr965.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
fr970.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
instinct3amoled45mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
instinct3amoled50mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
|
||||
instinctcrossoveramoled.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-38
|
||||
marq2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
marq2aviator.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
|
||||
venu2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
|
||||
venu2plus.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
|
||||
venu2s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-61
|
||||
venu3.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
|
||||
venu3s.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-70
|
||||
venu441mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
|
||||
venu445mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
|
||||
vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
|
||||
vivoactive6.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
|
||||
|
||||
base.excludeAnnotations = release
|
||||
|
||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 96 KiB |
350
preview.html
170
resources-chn/strings/fields.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<strings>
|
||||
<!-- Field names shown in the settings picker. -->
|
||||
<string id="FldOff">关闭</string>
|
||||
<string id="FldTime">时间</string>
|
||||
<string id="FldUtcTime">UTC 时间</string>
|
||||
<string id="FldAltTime1">第二时区 1</string>
|
||||
<string id="FldAltTime2">第二时区 2</string>
|
||||
<string id="FldDayMonthDay">星期 月 日</string>
|
||||
<string id="FldDayDate">星期 日</string>
|
||||
<string id="FldMonthDay">月 日</string>
|
||||
<string id="FldDayOfMonth">日</string>
|
||||
<string id="FldDayShort">星期简写</string>
|
||||
<string id="FldDayFull">星期全称</string>
|
||||
<string id="FldMonthName">月份</string>
|
||||
<string id="FldMonthNum">月份数字</string>
|
||||
<string id="FldMeridiem">上午/下午</string>
|
||||
<string id="FldWeekIso">周数 (ISO)</string>
|
||||
<string id="FldSeconds">秒</string>
|
||||
<string id="FldSeconds0">秒 (前导零)</string>
|
||||
<string id="FldWeekCal">周数</string>
|
||||
<string id="FldCustomTime">自定义时间格式</string>
|
||||
<string id="FldDateFmt1">日期格式 1</string>
|
||||
<string id="FldDateFmt2">日期格式 2</string>
|
||||
<string id="FldCountdown">事件倒计时</string>
|
||||
<string id="FldSteps">步数</string>
|
||||
<string id="FldStepsRound">步数 (取整)</string>
|
||||
<string id="FldStepsToGo">剩余步数</string>
|
||||
<string id="FldCalories">卡路里</string>
|
||||
<string id="FldActiveCal">活动卡路里</string>
|
||||
<string id="FldActiveMin">活动分钟</string>
|
||||
<string id="FldActiveMod">中强度分钟</string>
|
||||
<string id="FldActiveVig">高强度分钟</string>
|
||||
<string id="FldActiveWeek">本周活动分钟</string>
|
||||
<string id="FldDistance">距离</string>
|
||||
<string id="FldFloorsUp">上楼层数</string>
|
||||
<string id="FldFloorsDown">下楼层数</string>
|
||||
<string id="FldMetersUp">爬升高度</string>
|
||||
<string id="FldPushDist">推行距离</string>
|
||||
<string id="FldPushes">推行次数</string>
|
||||
<string id="FldStepsWeek">本周步数</string>
|
||||
<string id="FldSteps7d">近 7 天步数</string>
|
||||
<string id="FldCalWeek">本周卡路里</string>
|
||||
<string id="FldCal7d">近 7 天卡路里</string>
|
||||
<string id="FldDistWeek">本周距离</string>
|
||||
<string id="FldDist7d">近 7 天距离</string>
|
||||
<string id="FldFloorsWeek">本周楼层</string>
|
||||
<string id="FldFloors7d">近 7 天楼层</string>
|
||||
<string id="FldActiveMin7d">近 7 天活动分钟</string>
|
||||
<string id="FldSteps28d">近 28 天步数</string>
|
||||
<string id="FldCal28d">近 28 天卡路里</string>
|
||||
<string id="FldDist28d">近 28 天距离</string>
|
||||
<string id="FldFloors28d">近 28 天楼层</string>
|
||||
<string id="FldHeartRate">心率</string>
|
||||
<string id="FldRestingHr">静息心率</string>
|
||||
<string id="FldStress">压力指数</string>
|
||||
<string id="FldPulseOx">血氧</string>
|
||||
<string id="FldRespiration">呼吸频率</string>
|
||||
<string id="FldBodyBattery">身体电量</string>
|
||||
<string id="FldMoveBar">活动条</string>
|
||||
<string id="FldMoveBarRev">活动条 (反向)</string>
|
||||
<string id="FldWeight">体重</string>
|
||||
<string id="FldBmi">BMI</string>
|
||||
<string id="FldRecovery">恢复时间</string>
|
||||
<string id="FldVo2Run">最大摄氧量 (跑)</string>
|
||||
<string id="FldVo2Bike">最大摄氧量 (骑)</string>
|
||||
<string id="FldBattery">电量</string>
|
||||
<string id="FldBatteryDays">剩余天数</string>
|
||||
<string id="FldBatteryBoth">电量 / 天数</string>
|
||||
<string id="FldAlarms">闹钟</string>
|
||||
<string id="FldNotifications">通知</string>
|
||||
<string id="FldDoNotDisturb">勿扰模式</string>
|
||||
<string id="FldBluetooth">蓝牙</string>
|
||||
<string id="FldSolar">太阳能强度</string>
|
||||
<string id="FldAltitude">海拔</string>
|
||||
<string id="FldSeaPressure">海平面气压</string>
|
||||
<string id="FldAmbPressure">环境气压</string>
|
||||
<string id="FldSunrise">日出</string>
|
||||
<string id="FldSunset">日落</string>
|
||||
<string id="FldNextSun">下一太阳事件</string>
|
||||
<string id="FldUntilSun">距太阳事件</string>
|
||||
<string id="FldMoonAge">月龄</string>
|
||||
<string id="FldMoonLit">月球照明</string>
|
||||
<string id="FldCivilAm">民用晨光始</string>
|
||||
<string id="FldNauticalAm">航海晨光始</string>
|
||||
<string id="FldAstroAm">天文晨光始</string>
|
||||
<string id="FldBlueAm">蓝调时刻 (晨)</string>
|
||||
<string id="FldGoldenAm">黄金时刻 (晨)</string>
|
||||
<string id="FldGoldenPm">黄金时刻 (昏)</string>
|
||||
<string id="FldBluePm">蓝调时刻 (昏)</string>
|
||||
<string id="FldCivilPm">民用昏影终</string>
|
||||
<string id="FldNauticalPm">航海昏影终</string>
|
||||
<string id="FldAstroPm">天文昏影终</string>
|
||||
<string id="FldTemp">气温</string>
|
||||
<string id="FldTempHiLo">最高/最低气温</string>
|
||||
<string id="FldFeelsLike">体感温度</string>
|
||||
<string id="FldTempHigh">最高气温</string>
|
||||
<string id="FldTempLow">最低气温</string>
|
||||
<string id="FldHumidity">湿度</string>
|
||||
<string id="FldPrecip">降水概率</string>
|
||||
<string id="FldWindSpeed">风速</string>
|
||||
<string id="FldWindBearing">风向角</string>
|
||||
<string id="FldWindDir">风向</string>
|
||||
<string id="FldWeatherTime">天气更新时间</string>
|
||||
<string id="FldPressureSea">海平面气压</string>
|
||||
<string id="FldUvi">紫外线指数</string>
|
||||
<string id="FldVisibility">能见度</string>
|
||||
<string id="FldDewPoint">露点</string>
|
||||
<string id="FldNextCal">下一个日程</string>
|
||||
<string id="FldTrainStatus">训练状态</string>
|
||||
<string id="FldRace5k">5k 完赛预测</string>
|
||||
<string id="FldRace10k">10k 完赛预测</string>
|
||||
<string id="FldRaceHalf">半马完赛预测</string>
|
||||
<string id="FldRaceFull">全马完赛预测</string>
|
||||
<string id="FldPace5k">5k 配速预测</string>
|
||||
<string id="FldPace10k">10k 配速预测</string>
|
||||
<string id="FldPaceHalf">半马配速预测</string>
|
||||
<string id="FldPaceFull">全马配速预测</string>
|
||||
<string id="FldGolfScore">最近高尔夫得分</string>
|
||||
<string id="FldRunWeek">本周跑步距离</string>
|
||||
<string id="FldBikeWeek">本周骑行距离</string>
|
||||
<string id="FldApp1">第三方 App 1</string>
|
||||
<string id="FldApp2">第三方 App 2</string>
|
||||
<string id="FldApp3">第三方 App 3</string>
|
||||
<string id="FldApp4">第三方 App 4</string>
|
||||
<string id="FldApp5">第三方 App 5</string>
|
||||
<string id="FldOwmTemp">OWM 气温</string>
|
||||
<string id="FldOwmMinMax">OWM 最高/最低</string>
|
||||
<string id="FldOwmFeels">OWM 体感温度</string>
|
||||
<string id="FldOwmMax">OWM 最高温</string>
|
||||
<string id="FldOwmMin">OWM 最低温</string>
|
||||
<string id="FldOwmDescShort">OWM 天气</string>
|
||||
<string id="FldOwmDescLong">OWM 天气详述</string>
|
||||
<string id="FldOwmHumidity">OWM 湿度</string>
|
||||
<string id="FldOwmPressure">OWM 气压</string>
|
||||
<string id="FldOwmVisibility">OWM 能见度</string>
|
||||
<string id="FldOwmWind">OWM 风速</string>
|
||||
<string id="FldOwmWindDeg">OWM 风向角</string>
|
||||
<string id="FldOwmWindDir">OWM 风向</string>
|
||||
<string id="FldOwmGust">OWM 阵风</string>
|
||||
<string id="FldOwmRain">OWM 1h 降雨</string>
|
||||
<string id="FldOwmSnow">OWM 1h 降雪</string>
|
||||
<string id="FldOwmClouds">OWM 云量</string>
|
||||
<string id="FldOwmPrecip">OWM 降水概率</string>
|
||||
<string id="FldOwmDew">OWM 露点</string>
|
||||
<string id="FldOwmTomorrow">OWM 明日气温</string>
|
||||
<string id="FldOwmMinMaxTmrw">OWM 明日高低温</string>
|
||||
<string id="FldOwmDayAfter">OWM 后日气温</string>
|
||||
<string id="FldOwmMinMaxD2">OWM 后日高低温</string>
|
||||
<string id="FldOwmAqi">OWM 空气质量</string>
|
||||
<string id="FldOwmAqiText">OWM 空气质量描述</string>
|
||||
<string id="FldOwmPm25">OWM PM2.5</string>
|
||||
<string id="FldOwmPm10">OWM PM10</string>
|
||||
<string id="FldOwmCo">OWM 一氧化碳</string>
|
||||
<string id="FldOwmCity">OWM 城市</string>
|
||||
<string id="FldOwmUpdated">OWM 更新时间</string>
|
||||
<string id="FldCustom1">自定义文字 1</string>
|
||||
<string id="FldCustom2">自定义文字 2</string>
|
||||
<string id="FldCustom3">自定义文字 3</string>
|
||||
|
||||
<!-- On-face captions, in FieldTable.IDS order. -->
|
||||
<string id="FieldLabels">,时间,UTC,时区1,时区2,日期,日期,日期,日,星期,星期,月,月,,周,秒,秒,周,时间,日期,日期,倒计时,步数,步数,还差,千卡,活动,分钟,中强度,高强度,周分钟,距离,楼层,下楼,爬升,推行,推行,周步数,7天,周千卡,7天,周距离,7天,周楼层,7天,7天,28天,28天,28天,28天,心率,静息,压力,血氧,呼吸,体能,活动,活动,体重,BMI,恢复,摄氧,摄氧,电量,天数,电量,闹钟,通知,勿扰,蓝牙,太阳能,海拔,气压,气压,日出,日落,太阳,太阳,月龄,月相,民用晨,航海晨,天文晨,蓝调晨,黄金晨,黄金昏,蓝调昏,民用昏,航海昏,天文昏,气温,高低温,体感,最高,最低,湿度,降水,风速,风向,风向,更新,气压,紫外线,能见度,露点,日程,训练,5K,10K,半马,全马,5K配速,10K配速,半马配速,全马配速,高尔夫,跑步,骑行,App1,App2,App3,App4,App5,气温,高低温,体感,最高,最低,天气,天气,湿度,气压,能见度,风速,风向,风向,阵风,降雨,降雪,云量,降水,露点,明日,明日,后日,后日,空气,空气,PM2.5,PM10,CO,城市,更新,,,</string>
|
||||
|
||||
<string id="MetricSteps">步数</string>
|
||||
<string id="MetricCalories">卡路里</string>
|
||||
<string id="MetricDistance">距离</string>
|
||||
<string id="MetricFloors">楼层</string>
|
||||
<string id="MetricMoveBar">动动条</string>
|
||||
<string id="MetricActiveMin">活动分钟</string>
|
||||
<string id="MetricBodyBattery">身体电量</string>
|
||||
</strings>
|
||||
45
resources-chn/strings/strings.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<strings>
|
||||
<string id="AppName">Fenix 8 V3</string>
|
||||
|
||||
<string id="SettingTheme">主题</string>
|
||||
<string id="SettingRingTR">进度环 · 右上</string>
|
||||
<string id="SettingRingTL">进度环 · 左上</string>
|
||||
<string id="SettingRingBR">进度环 · 右下</string>
|
||||
<string id="SettingRingBL">进度环 · 左下</string>
|
||||
<string id="SettingLeftTop">左上数据</string>
|
||||
<string id="SettingRightTop">右上数据</string>
|
||||
<string id="SettingBottomLeft">左下数据</string>
|
||||
<string id="SettingBottomRight">右下数据</string>
|
||||
<string id="SettingTempUnit">温度单位</string>
|
||||
<string id="SettingAltOffset1">第二时区 1(相对本地分钟数)</string>
|
||||
<string id="SettingAltOffset2">第二时区 2(相对本地分钟数)</string>
|
||||
<string id="SettingCountdownDate">倒计时目标日期(YYYY-MM-DD)</string>
|
||||
<string id="SettingTimeFmt">自定义时间格式(h m a)</string>
|
||||
<string id="SettingDateFmt1">日期格式 1(D M Y w)</string>
|
||||
<string id="SettingDateFmt2">日期格式 2(D M Y w)</string>
|
||||
<string id="SettingOwmKey">OpenWeatherMap API 密钥</string>
|
||||
<string id="SettingCustom1">自定义文字 1</string>
|
||||
<string id="SettingCustom2">自定义文字 2</string>
|
||||
<string id="SettingCustom3">自定义文字 3</string>
|
||||
<string id="SettingShowTop">显示顶部一行</string>
|
||||
<string id="SettingShowBand">显示日期带</string>
|
||||
<string id="SettingShowBottom">显示底部一行</string>
|
||||
|
||||
<string id="TempUnitAuto">跟随手表</string>
|
||||
<string id="TempUnitC">摄氏度 (°C)</string>
|
||||
<string id="TempUnitF">华氏度 (°F)</string>
|
||||
|
||||
<string id="ThemeEmber">1 · 余烬</string>
|
||||
<string id="ThemeAurora">2 · 极光</string>
|
||||
<string id="ThemeBrass">3 · 黄铜</string>
|
||||
<string id="ThemeVoltage">4 · 电压</string>
|
||||
<string id="ThemeReef">5 · 珊瑚礁</string>
|
||||
<string id="ThemeAcid">6 · 酸绿</string>
|
||||
<string id="ThemeKelp">7 · 海藻</string>
|
||||
|
||||
<!-- 表盘上的文案。逗号分隔的列表在运行时拆分。 -->
|
||||
<string id="DowNames">周日,周一,周二,周三,周四,周五,周六</string>
|
||||
<string id="MonNames">1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月</string>
|
||||
<string id="DateFormat">$1$$2$日</string>
|
||||
<string id="Meridiem">上午,下午</string>
|
||||
</strings>
|
||||
5
resources-icons-218/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-218/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 151 B |
BIN
resources-icons-218/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 148 B |
BIN
resources-icons-218/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 148 B |
5
resources-icons-240/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-240/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
resources-icons-240/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 148 B |
BIN
resources-icons-240/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 153 B |
5
resources-icons-260/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-260/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 172 B |
BIN
resources-icons-260/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 165 B |
BIN
resources-icons-260/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 163 B |
5
resources-icons-280/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-280/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 172 B |
BIN
resources-icons-280/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 168 B |
BIN
resources-icons-280/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 173 B |
5
resources-icons-360/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-360/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 579 B |
BIN
resources-icons-360/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 583 B |
BIN
resources-icons-360/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 621 B |
5
resources-icons-390/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-390/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 636 B |
BIN
resources-icons-390/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
resources-icons-390/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 677 B |
5
resources-icons-416/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-416/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 701 B |
BIN
resources-icons-416/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 733 B |
BIN
resources-icons-416/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 746 B |
5
resources-icons-454/drawables/drawables.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<drawables>
|
||||
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
|
||||
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
|
||||
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
|
||||
</drawables>
|
||||
BIN
resources-icons-454/drawables/sunrise.png
Normal file
|
After Width: | Height: | Size: 767 B |
BIN
resources-icons-454/drawables/sunset.png
Normal file
|
After Width: | Height: | Size: 783 B |
BIN
resources-icons-454/drawables/weather.png
Normal file
|
After Width: | Height: | Size: 829 B |
BIN
resources-launcher-38/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
3
resources-launcher-40/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-40/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
3
resources-launcher-54/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-54/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
3
resources-launcher-56/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-56/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
3
resources-launcher-60/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-60/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
3
resources-launcher-61/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-61/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
3
resources-launcher-65/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-65/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
3
resources-launcher-70/drawables/drawables.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
|
||||
</drawables>
|
||||
BIN
resources-launcher-70/drawables/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 498 B |
@@ -4,8 +4,23 @@
|
||||
<property id="RingTL" type="number">1</property>
|
||||
<property id="RingBR" type="number">4</property>
|
||||
<property id="RingBL" type="number">3</property>
|
||||
<property id="LeftTop" type="number">1</property>
|
||||
<property id="RightTop" type="number">0</property>
|
||||
<!-- Field ids follow https://watchface.io/docs/datafields; the defaults are
|
||||
the comps: sunrise, forecast high/low, distance, steps. -->
|
||||
<property id="LeftTop" type="number">712</property>
|
||||
<property id="RightTop" type="number">639</property>
|
||||
<property id="BottomLeft" type="number">3</property>
|
||||
<property id="BottomRight" type="number">1</property>
|
||||
<property id="TempUnit" type="number">0</property>
|
||||
<property id="AltOffset1" type="number">0</property>
|
||||
<property id="AltOffset2" type="number">0</property>
|
||||
<property id="CountdownDate" type="string"></property>
|
||||
<property id="TimeFmt" type="string">h:m a</property>
|
||||
<property id="DateFmt1" type="string">D/M</property>
|
||||
<property id="DateFmt2" type="string">w D M</property>
|
||||
<property id="OwmKey" type="string"></property>
|
||||
<property id="Custom1" type="string"></property>
|
||||
<property id="Custom2" type="string"></property>
|
||||
<property id="Custom3" type="string"></property>
|
||||
<property id="ShowTop" type="boolean">true</property>
|
||||
<property id="ShowBand" type="boolean">true</property>
|
||||
<property id="ShowBottom" type="boolean">true</property>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<settings>
|
||||
|
||||
<setting propertyKey="@Properties.Theme" title="@Strings.SettingTheme">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="1">@Strings.ThemeEmber</listEntry>
|
||||
@@ -11,6 +12,650 @@
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.LeftTop" title="@Strings.SettingLeftTop">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FldOff</listEntry>
|
||||
<listEntry value="860">@Strings.FldTime</listEntry>
|
||||
<listEntry value="880">@Strings.FldUtcTime</listEntry>
|
||||
<listEntry value="864">@Strings.FldAltTime1</listEntry>
|
||||
<listEntry value="865">@Strings.FldAltTime2</listEntry>
|
||||
<listEntry value="851">@Strings.FldDayMonthDay</listEntry>
|
||||
<listEntry value="872">@Strings.FldDayDate</listEntry>
|
||||
<listEntry value="852">@Strings.FldMonthDay</listEntry>
|
||||
<listEntry value="855">@Strings.FldDayOfMonth</listEntry>
|
||||
<listEntry value="870">@Strings.FldDayShort</listEntry>
|
||||
<listEntry value="856">@Strings.FldDayFull</listEntry>
|
||||
<listEntry value="853">@Strings.FldMonthName</listEntry>
|
||||
<listEntry value="868">@Strings.FldMonthNum</listEntry>
|
||||
<listEntry value="858">@Strings.FldMeridiem</listEntry>
|
||||
<listEntry value="859">@Strings.FldWeekIso</listEntry>
|
||||
<listEntry value="850">@Strings.FldSeconds</listEntry>
|
||||
<listEntry value="869">@Strings.FldSeconds0</listEntry>
|
||||
<listEntry value="873">@Strings.FldWeekCal</listEntry>
|
||||
<listEntry value="861">@Strings.FldCustomTime</listEntry>
|
||||
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
|
||||
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
|
||||
<listEntry value="735">@Strings.FldCountdown</listEntry>
|
||||
<listEntry value="1">@Strings.FldSteps</listEntry>
|
||||
<listEntry value="14">@Strings.FldStepsRound</listEntry>
|
||||
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
|
||||
<listEntry value="2">@Strings.FldCalories</listEntry>
|
||||
<listEntry value="603">@Strings.FldActiveCal</listEntry>
|
||||
<listEntry value="150">@Strings.FldActiveMin</listEntry>
|
||||
<listEntry value="152">@Strings.FldActiveMod</listEntry>
|
||||
<listEntry value="154">@Strings.FldActiveVig</listEntry>
|
||||
<listEntry value="151">@Strings.FldActiveWeek</listEntry>
|
||||
<listEntry value="3">@Strings.FldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FldFloorsUp</listEntry>
|
||||
<listEntry value="5">@Strings.FldFloorsDown</listEntry>
|
||||
<listEntry value="6">@Strings.FldMetersUp</listEntry>
|
||||
<listEntry value="9">@Strings.FldPushDist</listEntry>
|
||||
<listEntry value="10">@Strings.FldPushes</listEntry>
|
||||
<listEntry value="219">@Strings.FldStepsWeek</listEntry>
|
||||
<listEntry value="220">@Strings.FldSteps7d</listEntry>
|
||||
<listEntry value="211">@Strings.FldCalWeek</listEntry>
|
||||
<listEntry value="212">@Strings.FldCal7d</listEntry>
|
||||
<listEntry value="213">@Strings.FldDistWeek</listEntry>
|
||||
<listEntry value="214">@Strings.FldDist7d</listEntry>
|
||||
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
|
||||
<listEntry value="204">@Strings.FldFloors7d</listEntry>
|
||||
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
|
||||
<listEntry value="828">@Strings.FldSteps28d</listEntry>
|
||||
<listEntry value="832">@Strings.FldCal28d</listEntry>
|
||||
<listEntry value="836">@Strings.FldDist28d</listEntry>
|
||||
<listEntry value="840">@Strings.FldFloors28d</listEntry>
|
||||
<listEntry value="618">@Strings.FldHeartRate</listEntry>
|
||||
<listEntry value="276">@Strings.FldRestingHr</listEntry>
|
||||
<listEntry value="12">@Strings.FldStress</listEntry>
|
||||
<listEntry value="635">@Strings.FldPulseOx</listEntry>
|
||||
<listEntry value="11">@Strings.FldRespiration</listEntry>
|
||||
<listEntry value="623">@Strings.FldBodyBattery</listEntry>
|
||||
<listEntry value="8">@Strings.FldMoveBar</listEntry>
|
||||
<listEntry value="15">@Strings.FldMoveBarRev</listEntry>
|
||||
<listEntry value="281">@Strings.FldWeight</listEntry>
|
||||
<listEntry value="742">@Strings.FldBmi</listEntry>
|
||||
<listEntry value="13">@Strings.FldRecovery</listEntry>
|
||||
<listEntry value="278">@Strings.FldVo2Run</listEntry>
|
||||
<listEntry value="277">@Strings.FldVo2Bike</listEntry>
|
||||
<listEntry value="250">@Strings.FldBattery</listEntry>
|
||||
<listEntry value="251">@Strings.FldBatteryDays</listEntry>
|
||||
<listEntry value="257">@Strings.FldBatteryBoth</listEntry>
|
||||
<listEntry value="253">@Strings.FldAlarms</listEntry>
|
||||
<listEntry value="255">@Strings.FldNotifications</listEntry>
|
||||
<listEntry value="254">@Strings.FldDoNotDisturb</listEntry>
|
||||
<listEntry value="256">@Strings.FldBluetooth</listEntry>
|
||||
<listEntry value="252">@Strings.FldSolar</listEntry>
|
||||
<listEntry value="615">@Strings.FldAltitude</listEntry>
|
||||
<listEntry value="616">@Strings.FldSeaPressure</listEntry>
|
||||
<listEntry value="733">@Strings.FldAmbPressure</listEntry>
|
||||
<listEntry value="712">@Strings.FldSunrise</listEntry>
|
||||
<listEntry value="717">@Strings.FldSunset</listEntry>
|
||||
<listEntry value="700">@Strings.FldNextSun</listEntry>
|
||||
<listEntry value="731">@Strings.FldUntilSun</listEntry>
|
||||
<listEntry value="706">@Strings.FldMoonAge</listEntry>
|
||||
<listEntry value="732">@Strings.FldMoonLit</listEntry>
|
||||
<listEntry value="710">@Strings.FldCivilAm</listEntry>
|
||||
<listEntry value="709">@Strings.FldNauticalAm</listEntry>
|
||||
<listEntry value="708">@Strings.FldAstroAm</listEntry>
|
||||
<listEntry value="711">@Strings.FldBlueAm</listEntry>
|
||||
<listEntry value="714">@Strings.FldGoldenAm</listEntry>
|
||||
<listEntry value="715">@Strings.FldGoldenPm</listEntry>
|
||||
<listEntry value="718">@Strings.FldBluePm</listEntry>
|
||||
<listEntry value="719">@Strings.FldCivilPm</listEntry>
|
||||
<listEntry value="720">@Strings.FldNauticalPm</listEntry>
|
||||
<listEntry value="721">@Strings.FldAstroPm</listEntry>
|
||||
<listEntry value="306">@Strings.FldTemp</listEntry>
|
||||
<listEntry value="639">@Strings.FldTempHiLo</listEntry>
|
||||
<listEntry value="300">@Strings.FldFeelsLike</listEntry>
|
||||
<listEntry value="301">@Strings.FldTempHigh</listEntry>
|
||||
<listEntry value="302">@Strings.FldTempLow</listEntry>
|
||||
<listEntry value="305">@Strings.FldHumidity</listEntry>
|
||||
<listEntry value="304">@Strings.FldPrecip</listEntry>
|
||||
<listEntry value="308">@Strings.FldWindSpeed</listEntry>
|
||||
<listEntry value="307">@Strings.FldWindBearing</listEntry>
|
||||
<listEntry value="310">@Strings.FldWindDir</listEntry>
|
||||
<listEntry value="309">@Strings.FldWeatherTime</listEntry>
|
||||
<listEntry value="312">@Strings.FldPressureSea</listEntry>
|
||||
<listEntry value="313">@Strings.FldUvi</listEntry>
|
||||
<listEntry value="314">@Strings.FldVisibility</listEntry>
|
||||
<listEntry value="315">@Strings.FldDewPoint</listEntry>
|
||||
<listEntry value="612">@Strings.FldNextCal</listEntry>
|
||||
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
|
||||
<listEntry value="627">@Strings.FldRace5k</listEntry>
|
||||
<listEntry value="628">@Strings.FldRace10k</listEntry>
|
||||
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
|
||||
<listEntry value="630">@Strings.FldRaceFull</listEntry>
|
||||
<listEntry value="631">@Strings.FldPace5k</listEntry>
|
||||
<listEntry value="632">@Strings.FldPace10k</listEntry>
|
||||
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
|
||||
<listEntry value="634">@Strings.FldPaceFull</listEntry>
|
||||
<listEntry value="641">@Strings.FldGolfScore</listEntry>
|
||||
<listEntry value="619">@Strings.FldRunWeek</listEntry>
|
||||
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
|
||||
<listEntry value="695">@Strings.FldApp1</listEntry>
|
||||
<listEntry value="696">@Strings.FldApp2</listEntry>
|
||||
<listEntry value="697">@Strings.FldApp3</listEntry>
|
||||
<listEntry value="698">@Strings.FldApp4</listEntry>
|
||||
<listEntry value="699">@Strings.FldApp5</listEntry>
|
||||
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
|
||||
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
|
||||
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
|
||||
<listEntry value="504">@Strings.FldOwmMax</listEntry>
|
||||
<listEntry value="505">@Strings.FldOwmMin</listEntry>
|
||||
<listEntry value="507">@Strings.FldOwmDescShort</listEntry>
|
||||
<listEntry value="502">@Strings.FldOwmDescLong</listEntry>
|
||||
<listEntry value="501">@Strings.FldOwmHumidity</listEntry>
|
||||
<listEntry value="506">@Strings.FldOwmPressure</listEntry>
|
||||
<listEntry value="509">@Strings.FldOwmVisibility</listEntry>
|
||||
<listEntry value="510">@Strings.FldOwmWind</listEntry>
|
||||
<listEntry value="511">@Strings.FldOwmWindDeg</listEntry>
|
||||
<listEntry value="518">@Strings.FldOwmWindDir</listEntry>
|
||||
<listEntry value="512">@Strings.FldOwmGust</listEntry>
|
||||
<listEntry value="513">@Strings.FldOwmRain</listEntry>
|
||||
<listEntry value="532">@Strings.FldOwmSnow</listEntry>
|
||||
<listEntry value="536">@Strings.FldOwmClouds</listEntry>
|
||||
<listEntry value="537">@Strings.FldOwmPrecip</listEntry>
|
||||
<listEntry value="530">@Strings.FldOwmDew</listEntry>
|
||||
<listEntry value="534">@Strings.FldOwmTomorrow</listEntry>
|
||||
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
|
||||
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
|
||||
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
|
||||
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
|
||||
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
|
||||
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
|
||||
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
|
||||
<listEntry value="542">@Strings.FldOwmCo</listEntry>
|
||||
<listEntry value="516">@Strings.FldOwmCity</listEntry>
|
||||
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
|
||||
<listEntry value="703">@Strings.FldCustom1</listEntry>
|
||||
<listEntry value="704">@Strings.FldCustom2</listEntry>
|
||||
<listEntry value="705">@Strings.FldCustom3</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RightTop" title="@Strings.SettingRightTop">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FldOff</listEntry>
|
||||
<listEntry value="860">@Strings.FldTime</listEntry>
|
||||
<listEntry value="880">@Strings.FldUtcTime</listEntry>
|
||||
<listEntry value="864">@Strings.FldAltTime1</listEntry>
|
||||
<listEntry value="865">@Strings.FldAltTime2</listEntry>
|
||||
<listEntry value="851">@Strings.FldDayMonthDay</listEntry>
|
||||
<listEntry value="872">@Strings.FldDayDate</listEntry>
|
||||
<listEntry value="852">@Strings.FldMonthDay</listEntry>
|
||||
<listEntry value="855">@Strings.FldDayOfMonth</listEntry>
|
||||
<listEntry value="870">@Strings.FldDayShort</listEntry>
|
||||
<listEntry value="856">@Strings.FldDayFull</listEntry>
|
||||
<listEntry value="853">@Strings.FldMonthName</listEntry>
|
||||
<listEntry value="868">@Strings.FldMonthNum</listEntry>
|
||||
<listEntry value="858">@Strings.FldMeridiem</listEntry>
|
||||
<listEntry value="859">@Strings.FldWeekIso</listEntry>
|
||||
<listEntry value="850">@Strings.FldSeconds</listEntry>
|
||||
<listEntry value="869">@Strings.FldSeconds0</listEntry>
|
||||
<listEntry value="873">@Strings.FldWeekCal</listEntry>
|
||||
<listEntry value="861">@Strings.FldCustomTime</listEntry>
|
||||
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
|
||||
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
|
||||
<listEntry value="735">@Strings.FldCountdown</listEntry>
|
||||
<listEntry value="1">@Strings.FldSteps</listEntry>
|
||||
<listEntry value="14">@Strings.FldStepsRound</listEntry>
|
||||
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
|
||||
<listEntry value="2">@Strings.FldCalories</listEntry>
|
||||
<listEntry value="603">@Strings.FldActiveCal</listEntry>
|
||||
<listEntry value="150">@Strings.FldActiveMin</listEntry>
|
||||
<listEntry value="152">@Strings.FldActiveMod</listEntry>
|
||||
<listEntry value="154">@Strings.FldActiveVig</listEntry>
|
||||
<listEntry value="151">@Strings.FldActiveWeek</listEntry>
|
||||
<listEntry value="3">@Strings.FldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FldFloorsUp</listEntry>
|
||||
<listEntry value="5">@Strings.FldFloorsDown</listEntry>
|
||||
<listEntry value="6">@Strings.FldMetersUp</listEntry>
|
||||
<listEntry value="9">@Strings.FldPushDist</listEntry>
|
||||
<listEntry value="10">@Strings.FldPushes</listEntry>
|
||||
<listEntry value="219">@Strings.FldStepsWeek</listEntry>
|
||||
<listEntry value="220">@Strings.FldSteps7d</listEntry>
|
||||
<listEntry value="211">@Strings.FldCalWeek</listEntry>
|
||||
<listEntry value="212">@Strings.FldCal7d</listEntry>
|
||||
<listEntry value="213">@Strings.FldDistWeek</listEntry>
|
||||
<listEntry value="214">@Strings.FldDist7d</listEntry>
|
||||
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
|
||||
<listEntry value="204">@Strings.FldFloors7d</listEntry>
|
||||
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
|
||||
<listEntry value="828">@Strings.FldSteps28d</listEntry>
|
||||
<listEntry value="832">@Strings.FldCal28d</listEntry>
|
||||
<listEntry value="836">@Strings.FldDist28d</listEntry>
|
||||
<listEntry value="840">@Strings.FldFloors28d</listEntry>
|
||||
<listEntry value="618">@Strings.FldHeartRate</listEntry>
|
||||
<listEntry value="276">@Strings.FldRestingHr</listEntry>
|
||||
<listEntry value="12">@Strings.FldStress</listEntry>
|
||||
<listEntry value="635">@Strings.FldPulseOx</listEntry>
|
||||
<listEntry value="11">@Strings.FldRespiration</listEntry>
|
||||
<listEntry value="623">@Strings.FldBodyBattery</listEntry>
|
||||
<listEntry value="8">@Strings.FldMoveBar</listEntry>
|
||||
<listEntry value="15">@Strings.FldMoveBarRev</listEntry>
|
||||
<listEntry value="281">@Strings.FldWeight</listEntry>
|
||||
<listEntry value="742">@Strings.FldBmi</listEntry>
|
||||
<listEntry value="13">@Strings.FldRecovery</listEntry>
|
||||
<listEntry value="278">@Strings.FldVo2Run</listEntry>
|
||||
<listEntry value="277">@Strings.FldVo2Bike</listEntry>
|
||||
<listEntry value="250">@Strings.FldBattery</listEntry>
|
||||
<listEntry value="251">@Strings.FldBatteryDays</listEntry>
|
||||
<listEntry value="257">@Strings.FldBatteryBoth</listEntry>
|
||||
<listEntry value="253">@Strings.FldAlarms</listEntry>
|
||||
<listEntry value="255">@Strings.FldNotifications</listEntry>
|
||||
<listEntry value="254">@Strings.FldDoNotDisturb</listEntry>
|
||||
<listEntry value="256">@Strings.FldBluetooth</listEntry>
|
||||
<listEntry value="252">@Strings.FldSolar</listEntry>
|
||||
<listEntry value="615">@Strings.FldAltitude</listEntry>
|
||||
<listEntry value="616">@Strings.FldSeaPressure</listEntry>
|
||||
<listEntry value="733">@Strings.FldAmbPressure</listEntry>
|
||||
<listEntry value="712">@Strings.FldSunrise</listEntry>
|
||||
<listEntry value="717">@Strings.FldSunset</listEntry>
|
||||
<listEntry value="700">@Strings.FldNextSun</listEntry>
|
||||
<listEntry value="731">@Strings.FldUntilSun</listEntry>
|
||||
<listEntry value="706">@Strings.FldMoonAge</listEntry>
|
||||
<listEntry value="732">@Strings.FldMoonLit</listEntry>
|
||||
<listEntry value="710">@Strings.FldCivilAm</listEntry>
|
||||
<listEntry value="709">@Strings.FldNauticalAm</listEntry>
|
||||
<listEntry value="708">@Strings.FldAstroAm</listEntry>
|
||||
<listEntry value="711">@Strings.FldBlueAm</listEntry>
|
||||
<listEntry value="714">@Strings.FldGoldenAm</listEntry>
|
||||
<listEntry value="715">@Strings.FldGoldenPm</listEntry>
|
||||
<listEntry value="718">@Strings.FldBluePm</listEntry>
|
||||
<listEntry value="719">@Strings.FldCivilPm</listEntry>
|
||||
<listEntry value="720">@Strings.FldNauticalPm</listEntry>
|
||||
<listEntry value="721">@Strings.FldAstroPm</listEntry>
|
||||
<listEntry value="306">@Strings.FldTemp</listEntry>
|
||||
<listEntry value="639">@Strings.FldTempHiLo</listEntry>
|
||||
<listEntry value="300">@Strings.FldFeelsLike</listEntry>
|
||||
<listEntry value="301">@Strings.FldTempHigh</listEntry>
|
||||
<listEntry value="302">@Strings.FldTempLow</listEntry>
|
||||
<listEntry value="305">@Strings.FldHumidity</listEntry>
|
||||
<listEntry value="304">@Strings.FldPrecip</listEntry>
|
||||
<listEntry value="308">@Strings.FldWindSpeed</listEntry>
|
||||
<listEntry value="307">@Strings.FldWindBearing</listEntry>
|
||||
<listEntry value="310">@Strings.FldWindDir</listEntry>
|
||||
<listEntry value="309">@Strings.FldWeatherTime</listEntry>
|
||||
<listEntry value="312">@Strings.FldPressureSea</listEntry>
|
||||
<listEntry value="313">@Strings.FldUvi</listEntry>
|
||||
<listEntry value="314">@Strings.FldVisibility</listEntry>
|
||||
<listEntry value="315">@Strings.FldDewPoint</listEntry>
|
||||
<listEntry value="612">@Strings.FldNextCal</listEntry>
|
||||
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
|
||||
<listEntry value="627">@Strings.FldRace5k</listEntry>
|
||||
<listEntry value="628">@Strings.FldRace10k</listEntry>
|
||||
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
|
||||
<listEntry value="630">@Strings.FldRaceFull</listEntry>
|
||||
<listEntry value="631">@Strings.FldPace5k</listEntry>
|
||||
<listEntry value="632">@Strings.FldPace10k</listEntry>
|
||||
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
|
||||
<listEntry value="634">@Strings.FldPaceFull</listEntry>
|
||||
<listEntry value="641">@Strings.FldGolfScore</listEntry>
|
||||
<listEntry value="619">@Strings.FldRunWeek</listEntry>
|
||||
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
|
||||
<listEntry value="695">@Strings.FldApp1</listEntry>
|
||||
<listEntry value="696">@Strings.FldApp2</listEntry>
|
||||
<listEntry value="697">@Strings.FldApp3</listEntry>
|
||||
<listEntry value="698">@Strings.FldApp4</listEntry>
|
||||
<listEntry value="699">@Strings.FldApp5</listEntry>
|
||||
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
|
||||
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
|
||||
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
|
||||
<listEntry value="504">@Strings.FldOwmMax</listEntry>
|
||||
<listEntry value="505">@Strings.FldOwmMin</listEntry>
|
||||
<listEntry value="507">@Strings.FldOwmDescShort</listEntry>
|
||||
<listEntry value="502">@Strings.FldOwmDescLong</listEntry>
|
||||
<listEntry value="501">@Strings.FldOwmHumidity</listEntry>
|
||||
<listEntry value="506">@Strings.FldOwmPressure</listEntry>
|
||||
<listEntry value="509">@Strings.FldOwmVisibility</listEntry>
|
||||
<listEntry value="510">@Strings.FldOwmWind</listEntry>
|
||||
<listEntry value="511">@Strings.FldOwmWindDeg</listEntry>
|
||||
<listEntry value="518">@Strings.FldOwmWindDir</listEntry>
|
||||
<listEntry value="512">@Strings.FldOwmGust</listEntry>
|
||||
<listEntry value="513">@Strings.FldOwmRain</listEntry>
|
||||
<listEntry value="532">@Strings.FldOwmSnow</listEntry>
|
||||
<listEntry value="536">@Strings.FldOwmClouds</listEntry>
|
||||
<listEntry value="537">@Strings.FldOwmPrecip</listEntry>
|
||||
<listEntry value="530">@Strings.FldOwmDew</listEntry>
|
||||
<listEntry value="534">@Strings.FldOwmTomorrow</listEntry>
|
||||
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
|
||||
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
|
||||
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
|
||||
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
|
||||
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
|
||||
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
|
||||
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
|
||||
<listEntry value="542">@Strings.FldOwmCo</listEntry>
|
||||
<listEntry value="516">@Strings.FldOwmCity</listEntry>
|
||||
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
|
||||
<listEntry value="703">@Strings.FldCustom1</listEntry>
|
||||
<listEntry value="704">@Strings.FldCustom2</listEntry>
|
||||
<listEntry value="705">@Strings.FldCustom3</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.BottomLeft" title="@Strings.SettingBottomLeft">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FldOff</listEntry>
|
||||
<listEntry value="860">@Strings.FldTime</listEntry>
|
||||
<listEntry value="880">@Strings.FldUtcTime</listEntry>
|
||||
<listEntry value="864">@Strings.FldAltTime1</listEntry>
|
||||
<listEntry value="865">@Strings.FldAltTime2</listEntry>
|
||||
<listEntry value="851">@Strings.FldDayMonthDay</listEntry>
|
||||
<listEntry value="872">@Strings.FldDayDate</listEntry>
|
||||
<listEntry value="852">@Strings.FldMonthDay</listEntry>
|
||||
<listEntry value="855">@Strings.FldDayOfMonth</listEntry>
|
||||
<listEntry value="870">@Strings.FldDayShort</listEntry>
|
||||
<listEntry value="856">@Strings.FldDayFull</listEntry>
|
||||
<listEntry value="853">@Strings.FldMonthName</listEntry>
|
||||
<listEntry value="868">@Strings.FldMonthNum</listEntry>
|
||||
<listEntry value="858">@Strings.FldMeridiem</listEntry>
|
||||
<listEntry value="859">@Strings.FldWeekIso</listEntry>
|
||||
<listEntry value="850">@Strings.FldSeconds</listEntry>
|
||||
<listEntry value="869">@Strings.FldSeconds0</listEntry>
|
||||
<listEntry value="873">@Strings.FldWeekCal</listEntry>
|
||||
<listEntry value="861">@Strings.FldCustomTime</listEntry>
|
||||
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
|
||||
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
|
||||
<listEntry value="735">@Strings.FldCountdown</listEntry>
|
||||
<listEntry value="1">@Strings.FldSteps</listEntry>
|
||||
<listEntry value="14">@Strings.FldStepsRound</listEntry>
|
||||
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
|
||||
<listEntry value="2">@Strings.FldCalories</listEntry>
|
||||
<listEntry value="603">@Strings.FldActiveCal</listEntry>
|
||||
<listEntry value="150">@Strings.FldActiveMin</listEntry>
|
||||
<listEntry value="152">@Strings.FldActiveMod</listEntry>
|
||||
<listEntry value="154">@Strings.FldActiveVig</listEntry>
|
||||
<listEntry value="151">@Strings.FldActiveWeek</listEntry>
|
||||
<listEntry value="3">@Strings.FldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FldFloorsUp</listEntry>
|
||||
<listEntry value="5">@Strings.FldFloorsDown</listEntry>
|
||||
<listEntry value="6">@Strings.FldMetersUp</listEntry>
|
||||
<listEntry value="9">@Strings.FldPushDist</listEntry>
|
||||
<listEntry value="10">@Strings.FldPushes</listEntry>
|
||||
<listEntry value="219">@Strings.FldStepsWeek</listEntry>
|
||||
<listEntry value="220">@Strings.FldSteps7d</listEntry>
|
||||
<listEntry value="211">@Strings.FldCalWeek</listEntry>
|
||||
<listEntry value="212">@Strings.FldCal7d</listEntry>
|
||||
<listEntry value="213">@Strings.FldDistWeek</listEntry>
|
||||
<listEntry value="214">@Strings.FldDist7d</listEntry>
|
||||
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
|
||||
<listEntry value="204">@Strings.FldFloors7d</listEntry>
|
||||
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
|
||||
<listEntry value="828">@Strings.FldSteps28d</listEntry>
|
||||
<listEntry value="832">@Strings.FldCal28d</listEntry>
|
||||
<listEntry value="836">@Strings.FldDist28d</listEntry>
|
||||
<listEntry value="840">@Strings.FldFloors28d</listEntry>
|
||||
<listEntry value="618">@Strings.FldHeartRate</listEntry>
|
||||
<listEntry value="276">@Strings.FldRestingHr</listEntry>
|
||||
<listEntry value="12">@Strings.FldStress</listEntry>
|
||||
<listEntry value="635">@Strings.FldPulseOx</listEntry>
|
||||
<listEntry value="11">@Strings.FldRespiration</listEntry>
|
||||
<listEntry value="623">@Strings.FldBodyBattery</listEntry>
|
||||
<listEntry value="8">@Strings.FldMoveBar</listEntry>
|
||||
<listEntry value="15">@Strings.FldMoveBarRev</listEntry>
|
||||
<listEntry value="281">@Strings.FldWeight</listEntry>
|
||||
<listEntry value="742">@Strings.FldBmi</listEntry>
|
||||
<listEntry value="13">@Strings.FldRecovery</listEntry>
|
||||
<listEntry value="278">@Strings.FldVo2Run</listEntry>
|
||||
<listEntry value="277">@Strings.FldVo2Bike</listEntry>
|
||||
<listEntry value="250">@Strings.FldBattery</listEntry>
|
||||
<listEntry value="251">@Strings.FldBatteryDays</listEntry>
|
||||
<listEntry value="257">@Strings.FldBatteryBoth</listEntry>
|
||||
<listEntry value="253">@Strings.FldAlarms</listEntry>
|
||||
<listEntry value="255">@Strings.FldNotifications</listEntry>
|
||||
<listEntry value="254">@Strings.FldDoNotDisturb</listEntry>
|
||||
<listEntry value="256">@Strings.FldBluetooth</listEntry>
|
||||
<listEntry value="252">@Strings.FldSolar</listEntry>
|
||||
<listEntry value="615">@Strings.FldAltitude</listEntry>
|
||||
<listEntry value="616">@Strings.FldSeaPressure</listEntry>
|
||||
<listEntry value="733">@Strings.FldAmbPressure</listEntry>
|
||||
<listEntry value="712">@Strings.FldSunrise</listEntry>
|
||||
<listEntry value="717">@Strings.FldSunset</listEntry>
|
||||
<listEntry value="700">@Strings.FldNextSun</listEntry>
|
||||
<listEntry value="731">@Strings.FldUntilSun</listEntry>
|
||||
<listEntry value="706">@Strings.FldMoonAge</listEntry>
|
||||
<listEntry value="732">@Strings.FldMoonLit</listEntry>
|
||||
<listEntry value="710">@Strings.FldCivilAm</listEntry>
|
||||
<listEntry value="709">@Strings.FldNauticalAm</listEntry>
|
||||
<listEntry value="708">@Strings.FldAstroAm</listEntry>
|
||||
<listEntry value="711">@Strings.FldBlueAm</listEntry>
|
||||
<listEntry value="714">@Strings.FldGoldenAm</listEntry>
|
||||
<listEntry value="715">@Strings.FldGoldenPm</listEntry>
|
||||
<listEntry value="718">@Strings.FldBluePm</listEntry>
|
||||
<listEntry value="719">@Strings.FldCivilPm</listEntry>
|
||||
<listEntry value="720">@Strings.FldNauticalPm</listEntry>
|
||||
<listEntry value="721">@Strings.FldAstroPm</listEntry>
|
||||
<listEntry value="306">@Strings.FldTemp</listEntry>
|
||||
<listEntry value="639">@Strings.FldTempHiLo</listEntry>
|
||||
<listEntry value="300">@Strings.FldFeelsLike</listEntry>
|
||||
<listEntry value="301">@Strings.FldTempHigh</listEntry>
|
||||
<listEntry value="302">@Strings.FldTempLow</listEntry>
|
||||
<listEntry value="305">@Strings.FldHumidity</listEntry>
|
||||
<listEntry value="304">@Strings.FldPrecip</listEntry>
|
||||
<listEntry value="308">@Strings.FldWindSpeed</listEntry>
|
||||
<listEntry value="307">@Strings.FldWindBearing</listEntry>
|
||||
<listEntry value="310">@Strings.FldWindDir</listEntry>
|
||||
<listEntry value="309">@Strings.FldWeatherTime</listEntry>
|
||||
<listEntry value="312">@Strings.FldPressureSea</listEntry>
|
||||
<listEntry value="313">@Strings.FldUvi</listEntry>
|
||||
<listEntry value="314">@Strings.FldVisibility</listEntry>
|
||||
<listEntry value="315">@Strings.FldDewPoint</listEntry>
|
||||
<listEntry value="612">@Strings.FldNextCal</listEntry>
|
||||
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
|
||||
<listEntry value="627">@Strings.FldRace5k</listEntry>
|
||||
<listEntry value="628">@Strings.FldRace10k</listEntry>
|
||||
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
|
||||
<listEntry value="630">@Strings.FldRaceFull</listEntry>
|
||||
<listEntry value="631">@Strings.FldPace5k</listEntry>
|
||||
<listEntry value="632">@Strings.FldPace10k</listEntry>
|
||||
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
|
||||
<listEntry value="634">@Strings.FldPaceFull</listEntry>
|
||||
<listEntry value="641">@Strings.FldGolfScore</listEntry>
|
||||
<listEntry value="619">@Strings.FldRunWeek</listEntry>
|
||||
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
|
||||
<listEntry value="695">@Strings.FldApp1</listEntry>
|
||||
<listEntry value="696">@Strings.FldApp2</listEntry>
|
||||
<listEntry value="697">@Strings.FldApp3</listEntry>
|
||||
<listEntry value="698">@Strings.FldApp4</listEntry>
|
||||
<listEntry value="699">@Strings.FldApp5</listEntry>
|
||||
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
|
||||
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
|
||||
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
|
||||
<listEntry value="504">@Strings.FldOwmMax</listEntry>
|
||||
<listEntry value="505">@Strings.FldOwmMin</listEntry>
|
||||
<listEntry value="507">@Strings.FldOwmDescShort</listEntry>
|
||||
<listEntry value="502">@Strings.FldOwmDescLong</listEntry>
|
||||
<listEntry value="501">@Strings.FldOwmHumidity</listEntry>
|
||||
<listEntry value="506">@Strings.FldOwmPressure</listEntry>
|
||||
<listEntry value="509">@Strings.FldOwmVisibility</listEntry>
|
||||
<listEntry value="510">@Strings.FldOwmWind</listEntry>
|
||||
<listEntry value="511">@Strings.FldOwmWindDeg</listEntry>
|
||||
<listEntry value="518">@Strings.FldOwmWindDir</listEntry>
|
||||
<listEntry value="512">@Strings.FldOwmGust</listEntry>
|
||||
<listEntry value="513">@Strings.FldOwmRain</listEntry>
|
||||
<listEntry value="532">@Strings.FldOwmSnow</listEntry>
|
||||
<listEntry value="536">@Strings.FldOwmClouds</listEntry>
|
||||
<listEntry value="537">@Strings.FldOwmPrecip</listEntry>
|
||||
<listEntry value="530">@Strings.FldOwmDew</listEntry>
|
||||
<listEntry value="534">@Strings.FldOwmTomorrow</listEntry>
|
||||
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
|
||||
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
|
||||
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
|
||||
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
|
||||
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
|
||||
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
|
||||
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
|
||||
<listEntry value="542">@Strings.FldOwmCo</listEntry>
|
||||
<listEntry value="516">@Strings.FldOwmCity</listEntry>
|
||||
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
|
||||
<listEntry value="703">@Strings.FldCustom1</listEntry>
|
||||
<listEntry value="704">@Strings.FldCustom2</listEntry>
|
||||
<listEntry value="705">@Strings.FldCustom3</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.BottomRight" title="@Strings.SettingBottomRight">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FldOff</listEntry>
|
||||
<listEntry value="860">@Strings.FldTime</listEntry>
|
||||
<listEntry value="880">@Strings.FldUtcTime</listEntry>
|
||||
<listEntry value="864">@Strings.FldAltTime1</listEntry>
|
||||
<listEntry value="865">@Strings.FldAltTime2</listEntry>
|
||||
<listEntry value="851">@Strings.FldDayMonthDay</listEntry>
|
||||
<listEntry value="872">@Strings.FldDayDate</listEntry>
|
||||
<listEntry value="852">@Strings.FldMonthDay</listEntry>
|
||||
<listEntry value="855">@Strings.FldDayOfMonth</listEntry>
|
||||
<listEntry value="870">@Strings.FldDayShort</listEntry>
|
||||
<listEntry value="856">@Strings.FldDayFull</listEntry>
|
||||
<listEntry value="853">@Strings.FldMonthName</listEntry>
|
||||
<listEntry value="868">@Strings.FldMonthNum</listEntry>
|
||||
<listEntry value="858">@Strings.FldMeridiem</listEntry>
|
||||
<listEntry value="859">@Strings.FldWeekIso</listEntry>
|
||||
<listEntry value="850">@Strings.FldSeconds</listEntry>
|
||||
<listEntry value="869">@Strings.FldSeconds0</listEntry>
|
||||
<listEntry value="873">@Strings.FldWeekCal</listEntry>
|
||||
<listEntry value="861">@Strings.FldCustomTime</listEntry>
|
||||
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
|
||||
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
|
||||
<listEntry value="735">@Strings.FldCountdown</listEntry>
|
||||
<listEntry value="1">@Strings.FldSteps</listEntry>
|
||||
<listEntry value="14">@Strings.FldStepsRound</listEntry>
|
||||
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
|
||||
<listEntry value="2">@Strings.FldCalories</listEntry>
|
||||
<listEntry value="603">@Strings.FldActiveCal</listEntry>
|
||||
<listEntry value="150">@Strings.FldActiveMin</listEntry>
|
||||
<listEntry value="152">@Strings.FldActiveMod</listEntry>
|
||||
<listEntry value="154">@Strings.FldActiveVig</listEntry>
|
||||
<listEntry value="151">@Strings.FldActiveWeek</listEntry>
|
||||
<listEntry value="3">@Strings.FldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FldFloorsUp</listEntry>
|
||||
<listEntry value="5">@Strings.FldFloorsDown</listEntry>
|
||||
<listEntry value="6">@Strings.FldMetersUp</listEntry>
|
||||
<listEntry value="9">@Strings.FldPushDist</listEntry>
|
||||
<listEntry value="10">@Strings.FldPushes</listEntry>
|
||||
<listEntry value="219">@Strings.FldStepsWeek</listEntry>
|
||||
<listEntry value="220">@Strings.FldSteps7d</listEntry>
|
||||
<listEntry value="211">@Strings.FldCalWeek</listEntry>
|
||||
<listEntry value="212">@Strings.FldCal7d</listEntry>
|
||||
<listEntry value="213">@Strings.FldDistWeek</listEntry>
|
||||
<listEntry value="214">@Strings.FldDist7d</listEntry>
|
||||
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
|
||||
<listEntry value="204">@Strings.FldFloors7d</listEntry>
|
||||
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
|
||||
<listEntry value="828">@Strings.FldSteps28d</listEntry>
|
||||
<listEntry value="832">@Strings.FldCal28d</listEntry>
|
||||
<listEntry value="836">@Strings.FldDist28d</listEntry>
|
||||
<listEntry value="840">@Strings.FldFloors28d</listEntry>
|
||||
<listEntry value="618">@Strings.FldHeartRate</listEntry>
|
||||
<listEntry value="276">@Strings.FldRestingHr</listEntry>
|
||||
<listEntry value="12">@Strings.FldStress</listEntry>
|
||||
<listEntry value="635">@Strings.FldPulseOx</listEntry>
|
||||
<listEntry value="11">@Strings.FldRespiration</listEntry>
|
||||
<listEntry value="623">@Strings.FldBodyBattery</listEntry>
|
||||
<listEntry value="8">@Strings.FldMoveBar</listEntry>
|
||||
<listEntry value="15">@Strings.FldMoveBarRev</listEntry>
|
||||
<listEntry value="281">@Strings.FldWeight</listEntry>
|
||||
<listEntry value="742">@Strings.FldBmi</listEntry>
|
||||
<listEntry value="13">@Strings.FldRecovery</listEntry>
|
||||
<listEntry value="278">@Strings.FldVo2Run</listEntry>
|
||||
<listEntry value="277">@Strings.FldVo2Bike</listEntry>
|
||||
<listEntry value="250">@Strings.FldBattery</listEntry>
|
||||
<listEntry value="251">@Strings.FldBatteryDays</listEntry>
|
||||
<listEntry value="257">@Strings.FldBatteryBoth</listEntry>
|
||||
<listEntry value="253">@Strings.FldAlarms</listEntry>
|
||||
<listEntry value="255">@Strings.FldNotifications</listEntry>
|
||||
<listEntry value="254">@Strings.FldDoNotDisturb</listEntry>
|
||||
<listEntry value="256">@Strings.FldBluetooth</listEntry>
|
||||
<listEntry value="252">@Strings.FldSolar</listEntry>
|
||||
<listEntry value="615">@Strings.FldAltitude</listEntry>
|
||||
<listEntry value="616">@Strings.FldSeaPressure</listEntry>
|
||||
<listEntry value="733">@Strings.FldAmbPressure</listEntry>
|
||||
<listEntry value="712">@Strings.FldSunrise</listEntry>
|
||||
<listEntry value="717">@Strings.FldSunset</listEntry>
|
||||
<listEntry value="700">@Strings.FldNextSun</listEntry>
|
||||
<listEntry value="731">@Strings.FldUntilSun</listEntry>
|
||||
<listEntry value="706">@Strings.FldMoonAge</listEntry>
|
||||
<listEntry value="732">@Strings.FldMoonLit</listEntry>
|
||||
<listEntry value="710">@Strings.FldCivilAm</listEntry>
|
||||
<listEntry value="709">@Strings.FldNauticalAm</listEntry>
|
||||
<listEntry value="708">@Strings.FldAstroAm</listEntry>
|
||||
<listEntry value="711">@Strings.FldBlueAm</listEntry>
|
||||
<listEntry value="714">@Strings.FldGoldenAm</listEntry>
|
||||
<listEntry value="715">@Strings.FldGoldenPm</listEntry>
|
||||
<listEntry value="718">@Strings.FldBluePm</listEntry>
|
||||
<listEntry value="719">@Strings.FldCivilPm</listEntry>
|
||||
<listEntry value="720">@Strings.FldNauticalPm</listEntry>
|
||||
<listEntry value="721">@Strings.FldAstroPm</listEntry>
|
||||
<listEntry value="306">@Strings.FldTemp</listEntry>
|
||||
<listEntry value="639">@Strings.FldTempHiLo</listEntry>
|
||||
<listEntry value="300">@Strings.FldFeelsLike</listEntry>
|
||||
<listEntry value="301">@Strings.FldTempHigh</listEntry>
|
||||
<listEntry value="302">@Strings.FldTempLow</listEntry>
|
||||
<listEntry value="305">@Strings.FldHumidity</listEntry>
|
||||
<listEntry value="304">@Strings.FldPrecip</listEntry>
|
||||
<listEntry value="308">@Strings.FldWindSpeed</listEntry>
|
||||
<listEntry value="307">@Strings.FldWindBearing</listEntry>
|
||||
<listEntry value="310">@Strings.FldWindDir</listEntry>
|
||||
<listEntry value="309">@Strings.FldWeatherTime</listEntry>
|
||||
<listEntry value="312">@Strings.FldPressureSea</listEntry>
|
||||
<listEntry value="313">@Strings.FldUvi</listEntry>
|
||||
<listEntry value="314">@Strings.FldVisibility</listEntry>
|
||||
<listEntry value="315">@Strings.FldDewPoint</listEntry>
|
||||
<listEntry value="612">@Strings.FldNextCal</listEntry>
|
||||
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
|
||||
<listEntry value="627">@Strings.FldRace5k</listEntry>
|
||||
<listEntry value="628">@Strings.FldRace10k</listEntry>
|
||||
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
|
||||
<listEntry value="630">@Strings.FldRaceFull</listEntry>
|
||||
<listEntry value="631">@Strings.FldPace5k</listEntry>
|
||||
<listEntry value="632">@Strings.FldPace10k</listEntry>
|
||||
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
|
||||
<listEntry value="634">@Strings.FldPaceFull</listEntry>
|
||||
<listEntry value="641">@Strings.FldGolfScore</listEntry>
|
||||
<listEntry value="619">@Strings.FldRunWeek</listEntry>
|
||||
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
|
||||
<listEntry value="695">@Strings.FldApp1</listEntry>
|
||||
<listEntry value="696">@Strings.FldApp2</listEntry>
|
||||
<listEntry value="697">@Strings.FldApp3</listEntry>
|
||||
<listEntry value="698">@Strings.FldApp4</listEntry>
|
||||
<listEntry value="699">@Strings.FldApp5</listEntry>
|
||||
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
|
||||
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
|
||||
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
|
||||
<listEntry value="504">@Strings.FldOwmMax</listEntry>
|
||||
<listEntry value="505">@Strings.FldOwmMin</listEntry>
|
||||
<listEntry value="507">@Strings.FldOwmDescShort</listEntry>
|
||||
<listEntry value="502">@Strings.FldOwmDescLong</listEntry>
|
||||
<listEntry value="501">@Strings.FldOwmHumidity</listEntry>
|
||||
<listEntry value="506">@Strings.FldOwmPressure</listEntry>
|
||||
<listEntry value="509">@Strings.FldOwmVisibility</listEntry>
|
||||
<listEntry value="510">@Strings.FldOwmWind</listEntry>
|
||||
<listEntry value="511">@Strings.FldOwmWindDeg</listEntry>
|
||||
<listEntry value="518">@Strings.FldOwmWindDir</listEntry>
|
||||
<listEntry value="512">@Strings.FldOwmGust</listEntry>
|
||||
<listEntry value="513">@Strings.FldOwmRain</listEntry>
|
||||
<listEntry value="532">@Strings.FldOwmSnow</listEntry>
|
||||
<listEntry value="536">@Strings.FldOwmClouds</listEntry>
|
||||
<listEntry value="537">@Strings.FldOwmPrecip</listEntry>
|
||||
<listEntry value="530">@Strings.FldOwmDew</listEntry>
|
||||
<listEntry value="534">@Strings.FldOwmTomorrow</listEntry>
|
||||
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
|
||||
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
|
||||
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
|
||||
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
|
||||
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
|
||||
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
|
||||
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
|
||||
<listEntry value="542">@Strings.FldOwmCo</listEntry>
|
||||
<listEntry value="516">@Strings.FldOwmCity</listEntry>
|
||||
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
|
||||
<listEntry value="703">@Strings.FldCustom1</listEntry>
|
||||
<listEntry value="704">@Strings.FldCustom2</listEntry>
|
||||
<listEntry value="705">@Strings.FldCustom3</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RingTR" title="@Strings.SettingRingTR">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.MetricSteps</listEntry>
|
||||
@@ -18,6 +663,8 @@
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
<listEntry value="5">@Strings.MetricActiveMin</listEntry>
|
||||
<listEntry value="6">@Strings.MetricBodyBattery</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
@@ -28,6 +675,8 @@
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
<listEntry value="5">@Strings.MetricActiveMin</listEntry>
|
||||
<listEntry value="6">@Strings.MetricBodyBattery</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
@@ -38,6 +687,8 @@
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
<listEntry value="5">@Strings.MetricActiveMin</listEntry>
|
||||
<listEntry value="6">@Strings.MetricBodyBattery</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
@@ -48,29 +699,58 @@
|
||||
<listEntry value="2">@Strings.MetricDistance</listEntry>
|
||||
<listEntry value="3">@Strings.MetricFloors</listEntry>
|
||||
<listEntry value="4">@Strings.MetricMoveBar</listEntry>
|
||||
<listEntry value="5">@Strings.MetricActiveMin</listEntry>
|
||||
<listEntry value="6">@Strings.MetricBodyBattery</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.LeftTop" title="@Strings.SettingLeftTop">
|
||||
|
||||
<setting propertyKey="@Properties.TempUnit" title="@Strings.SettingTempUnit">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FieldWeather</listEntry>
|
||||
<listEntry value="1">@Strings.FieldCalories</listEntry>
|
||||
<listEntry value="2">@Strings.FieldSteps</listEntry>
|
||||
<listEntry value="3">@Strings.FieldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FieldFloors</listEntry>
|
||||
<listEntry value="5">@Strings.FieldBattery</listEntry>
|
||||
<listEntry value="0">@Strings.TempUnitAuto</listEntry>
|
||||
<listEntry value="1">@Strings.TempUnitC</listEntry>
|
||||
<listEntry value="2">@Strings.TempUnitF</listEntry>
|
||||
</settingConfig>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.RightTop" title="@Strings.SettingRightTop">
|
||||
<settingConfig type="list">
|
||||
<listEntry value="0">@Strings.FieldWeather</listEntry>
|
||||
<listEntry value="1">@Strings.FieldCalories</listEntry>
|
||||
<listEntry value="2">@Strings.FieldSteps</listEntry>
|
||||
<listEntry value="3">@Strings.FieldDistance</listEntry>
|
||||
<listEntry value="4">@Strings.FieldFloors</listEntry>
|
||||
<listEntry value="5">@Strings.FieldBattery</listEntry>
|
||||
</settingConfig>
|
||||
<setting propertyKey="@Properties.AltOffset1" title="@Strings.SettingAltOffset1">
|
||||
<settingConfig type="numeric" min="-720" max="840"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.AltOffset2" title="@Strings.SettingAltOffset2">
|
||||
<settingConfig type="numeric" min="-720" max="840"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.CountdownDate" title="@Strings.SettingCountdownDate">
|
||||
<settingConfig type="alphaNumeric" maxLength="10"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.TimeFmt" title="@Strings.SettingTimeFmt">
|
||||
<settingConfig type="alphaNumeric" maxLength="16"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.DateFmt1" title="@Strings.SettingDateFmt1">
|
||||
<settingConfig type="alphaNumeric" maxLength="16"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.DateFmt2" title="@Strings.SettingDateFmt2">
|
||||
<settingConfig type="alphaNumeric" maxLength="16"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.OwmKey" title="@Strings.SettingOwmKey">
|
||||
<settingConfig type="alphaNumeric" maxLength="40"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.Custom1" title="@Strings.SettingCustom1">
|
||||
<settingConfig type="alphaNumeric" maxLength="12"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.Custom2" title="@Strings.SettingCustom2">
|
||||
<settingConfig type="alphaNumeric" maxLength="12"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.Custom3" title="@Strings.SettingCustom3">
|
||||
<settingConfig type="alphaNumeric" maxLength="12"/>
|
||||
</setting>
|
||||
|
||||
<setting propertyKey="@Properties.ShowTop" title="@Strings.SettingShowTop">
|
||||
|
||||
170
resources/strings/fields.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<strings>
|
||||
<!-- Field names shown in the settings picker. -->
|
||||
<string id="FldOff">Off</string>
|
||||
<string id="FldTime">Time</string>
|
||||
<string id="FldUtcTime">UTC Time</string>
|
||||
<string id="FldAltTime1">Alternate Time 1</string>
|
||||
<string id="FldAltTime2">Alternate Time 2</string>
|
||||
<string id="FldDayMonthDay">Day Month Day</string>
|
||||
<string id="FldDayDate">Day Date</string>
|
||||
<string id="FldMonthDay">Month Day</string>
|
||||
<string id="FldDayOfMonth">Day of Month</string>
|
||||
<string id="FldDayShort">Day (Tue)</string>
|
||||
<string id="FldDayFull">Full Day (Tuesday)</string>
|
||||
<string id="FldMonthName">Month (Jan)</string>
|
||||
<string id="FldMonthNum">Month (6)</string>
|
||||
<string id="FldMeridiem">AM/PM</string>
|
||||
<string id="FldWeekIso">Week Number (ISO)</string>
|
||||
<string id="FldSeconds">Seconds</string>
|
||||
<string id="FldSeconds0">Seconds (09)</string>
|
||||
<string id="FldWeekCal">Week Number</string>
|
||||
<string id="FldCustomTime">Custom Time Format</string>
|
||||
<string id="FldDateFmt1">Date Format 1</string>
|
||||
<string id="FldDateFmt2">Date Format 2</string>
|
||||
<string id="FldCountdown">Event Countdown</string>
|
||||
<string id="FldSteps">Steps</string>
|
||||
<string id="FldStepsRound">Steps Rounded</string>
|
||||
<string id="FldStepsToGo">Steps to Go</string>
|
||||
<string id="FldCalories">Calories</string>
|
||||
<string id="FldActiveCal">Active Calories</string>
|
||||
<string id="FldActiveMin">Active Minutes</string>
|
||||
<string id="FldActiveMod">Active Min (Moderate)</string>
|
||||
<string id="FldActiveVig">Active Min (Vigorous)</string>
|
||||
<string id="FldActiveWeek">Active Minutes (Week)</string>
|
||||
<string id="FldDistance">Distance</string>
|
||||
<string id="FldFloorsUp">Floors Climbed</string>
|
||||
<string id="FldFloorsDown">Floors Descended</string>
|
||||
<string id="FldMetersUp">Distance Climbed</string>
|
||||
<string id="FldPushDist">Push Distance</string>
|
||||
<string id="FldPushes">Pushes</string>
|
||||
<string id="FldStepsWeek">Steps (Week to Date)</string>
|
||||
<string id="FldSteps7d">Steps (Last 7 Days)</string>
|
||||
<string id="FldCalWeek">Calories (Week to Date)</string>
|
||||
<string id="FldCal7d">Calories (Last 7 Days)</string>
|
||||
<string id="FldDistWeek">Distance (Week to Date)</string>
|
||||
<string id="FldDist7d">Distance (Last 7 Days)</string>
|
||||
<string id="FldFloorsWeek">Floors (Week to Date)</string>
|
||||
<string id="FldFloors7d">Floors (Last 7 Days)</string>
|
||||
<string id="FldActiveMin7d">Active Minutes (7 Days)</string>
|
||||
<string id="FldSteps28d">Steps (28 Days)</string>
|
||||
<string id="FldCal28d">Calories (28 Days)</string>
|
||||
<string id="FldDist28d">Distance (28 Days)</string>
|
||||
<string id="FldFloors28d">Floors (28 Days)</string>
|
||||
<string id="FldHeartRate">Heart Rate</string>
|
||||
<string id="FldRestingHr">Resting Heart Rate</string>
|
||||
<string id="FldStress">Stress Score</string>
|
||||
<string id="FldPulseOx">Pulse Ox</string>
|
||||
<string id="FldRespiration">Respiration Rate</string>
|
||||
<string id="FldBodyBattery">Body Battery</string>
|
||||
<string id="FldMoveBar">Move Bar</string>
|
||||
<string id="FldMoveBarRev">Move Bar Reversed</string>
|
||||
<string id="FldWeight">Weight</string>
|
||||
<string id="FldBmi">BMI</string>
|
||||
<string id="FldRecovery">Time to Recovery</string>
|
||||
<string id="FldVo2Run">VO2 Max (Run)</string>
|
||||
<string id="FldVo2Bike">VO2 Max (Bike)</string>
|
||||
<string id="FldBattery">Battery</string>
|
||||
<string id="FldBatteryDays">Battery in Days</string>
|
||||
<string id="FldBatteryBoth">Battery / Days</string>
|
||||
<string id="FldAlarms">Alarms</string>
|
||||
<string id="FldNotifications">Notifications</string>
|
||||
<string id="FldDoNotDisturb">Do Not Disturb</string>
|
||||
<string id="FldBluetooth">Bluetooth</string>
|
||||
<string id="FldSolar">Solar Intensity</string>
|
||||
<string id="FldAltitude">Altitude</string>
|
||||
<string id="FldSeaPressure">Sea Level Pressure</string>
|
||||
<string id="FldAmbPressure">Ambient Pressure</string>
|
||||
<string id="FldSunrise">Sunrise</string>
|
||||
<string id="FldSunset">Sunset</string>
|
||||
<string id="FldNextSun">Next Sun Event</string>
|
||||
<string id="FldUntilSun">Time Until Sun Event</string>
|
||||
<string id="FldMoonAge">Moon Age</string>
|
||||
<string id="FldMoonLit">Moon Illumination</string>
|
||||
<string id="FldCivilAm">Civil Twilight Start</string>
|
||||
<string id="FldNauticalAm">Nautical Twilight Start</string>
|
||||
<string id="FldAstroAm">Astronomical Twilight Start</string>
|
||||
<string id="FldBlueAm">Blue Hour (AM)</string>
|
||||
<string id="FldGoldenAm">Golden Hour (AM)</string>
|
||||
<string id="FldGoldenPm">Golden Hour (PM)</string>
|
||||
<string id="FldBluePm">Blue Hour (PM)</string>
|
||||
<string id="FldCivilPm">Civil Twilight End</string>
|
||||
<string id="FldNauticalPm">Nautical Twilight End</string>
|
||||
<string id="FldAstroPm">Astronomical Twilight End</string>
|
||||
<string id="FldTemp">Temperature</string>
|
||||
<string id="FldTempHiLo">Temperature High/Low</string>
|
||||
<string id="FldFeelsLike">Feels Like</string>
|
||||
<string id="FldTempHigh">High Temperature</string>
|
||||
<string id="FldTempLow">Low Temperature</string>
|
||||
<string id="FldHumidity">Humidity</string>
|
||||
<string id="FldPrecip">Precipitation %</string>
|
||||
<string id="FldWindSpeed">Wind Speed</string>
|
||||
<string id="FldWindBearing">Wind Bearing</string>
|
||||
<string id="FldWindDir">Wind Direction</string>
|
||||
<string id="FldWeatherTime">Weather Update Time</string>
|
||||
<string id="FldPressureSea">Pressure (Sea Level)</string>
|
||||
<string id="FldUvi">UV Index</string>
|
||||
<string id="FldVisibility">Visibility</string>
|
||||
<string id="FldDewPoint">Dew Point</string>
|
||||
<string id="FldNextCal">Next Calendar Event</string>
|
||||
<string id="FldTrainStatus">Training Status</string>
|
||||
<string id="FldRace5k">Race Time (5k)</string>
|
||||
<string id="FldRace10k">Race Time (10k)</string>
|
||||
<string id="FldRaceHalf">Race Time (Half)</string>
|
||||
<string id="FldRaceFull">Race Time (Marathon)</string>
|
||||
<string id="FldPace5k">Pace (5k)</string>
|
||||
<string id="FldPace10k">Pace (10k)</string>
|
||||
<string id="FldPaceHalf">Pace (Half)</string>
|
||||
<string id="FldPaceFull">Pace (Marathon)</string>
|
||||
<string id="FldGolfScore">Last Golf Round Score</string>
|
||||
<string id="FldRunWeek">Run Distance (Week)</string>
|
||||
<string id="FldBikeWeek">Bike Distance (Week)</string>
|
||||
<string id="FldApp1">App 1 (on watch)</string>
|
||||
<string id="FldApp2">App 2 (on watch)</string>
|
||||
<string id="FldApp3">App 3 (on watch)</string>
|
||||
<string id="FldApp4">App 4 (on watch)</string>
|
||||
<string id="FldApp5">App 5 (on watch)</string>
|
||||
<string id="FldOwmTemp">OWM Temperature</string>
|
||||
<string id="FldOwmMinMax">OWM Max/Min Temp</string>
|
||||
<string id="FldOwmFeels">OWM Feels Like</string>
|
||||
<string id="FldOwmMax">OWM Temp Max</string>
|
||||
<string id="FldOwmMin">OWM Temp Min</string>
|
||||
<string id="FldOwmDescShort">OWM Description</string>
|
||||
<string id="FldOwmDescLong">OWM Description (Long)</string>
|
||||
<string id="FldOwmHumidity">OWM Humidity</string>
|
||||
<string id="FldOwmPressure">OWM Pressure</string>
|
||||
<string id="FldOwmVisibility">OWM Visibility</string>
|
||||
<string id="FldOwmWind">OWM Wind Speed</string>
|
||||
<string id="FldOwmWindDeg">OWM Wind Degrees</string>
|
||||
<string id="FldOwmWindDir">OWM Wind Direction</string>
|
||||
<string id="FldOwmGust">OWM Wind Gust</string>
|
||||
<string id="FldOwmRain">OWM Rain 1h</string>
|
||||
<string id="FldOwmSnow">OWM Snow 1h</string>
|
||||
<string id="FldOwmClouds">OWM Cloud Cover</string>
|
||||
<string id="FldOwmPrecip">OWM Precipitation %</string>
|
||||
<string id="FldOwmDew">OWM Dew Point</string>
|
||||
<string id="FldOwmTomorrow">OWM Temp Tomorrow</string>
|
||||
<string id="FldOwmMinMaxTmrw">OWM Min/Max Tomorrow</string>
|
||||
<string id="FldOwmDayAfter">OWM Temp Day After</string>
|
||||
<string id="FldOwmMinMaxD2">OWM Min/Max Day After</string>
|
||||
<string id="FldOwmAqi">OWM Air Quality (AQI)</string>
|
||||
<string id="FldOwmAqiText">OWM AQI Description</string>
|
||||
<string id="FldOwmPm25">OWM PM2.5</string>
|
||||
<string id="FldOwmPm10">OWM PM10</string>
|
||||
<string id="FldOwmCo">OWM CO</string>
|
||||
<string id="FldOwmCity">OWM City Name</string>
|
||||
<string id="FldOwmUpdated">OWM Update Time</string>
|
||||
<string id="FldCustom1">Custom Text 1</string>
|
||||
<string id="FldCustom2">Custom Text 2</string>
|
||||
<string id="FldCustom3">Custom Text 3</string>
|
||||
|
||||
<!-- On-face captions, in FieldTable.IDS order. -->
|
||||
<string id="FieldLabels">,TIME,UTC,TZ1,TZ2,DATE,DATE,DATE,DAY,DAY,DAY,MON,MON,,WEEK,SEC,SEC,WEEK,TIME,DATE,DATE,DAYS,STEP,STEP,TO GO,KCAL,ACAL,MIN,MOD,VIG,WEEK,DST,FLR,DESC,ASC,PUSH,PUSH,WEEK,7D,WEEK,7D,WEEK,7D,WEEK,7D,7D,28D,28D,28D,28D,HR,RHR,STRS,SPO2,RESP,BODY,MOVE,MOVE,WT,BMI,RCVR,VO2,VO2,BATT,DAYS,BATT,ALRM,MSG,DND,BT,SOLR,ALT,hPa,hPa,RISE,SET,SUN,SUN,MOON,MOON,CIVIL,NAUT,ASTR,BLUE,GOLD,GOLD,BLUE,CIVIL,NAUT,ASTR,TEMP,HI/LO,FEEL,HIGH,LOW,HUM,RAIN,WIND,WIND,WIND,WX,hPa,UVI,VIS,DEW,CAL,TRN,5K,10K,HM,FM,5K,10K,HM,FM,GOLF,RUN,BIKE,APP1,APP2,APP3,APP4,APP5,TEMP,HI/LO,FEEL,HIGH,LOW,WX,WX,HUM,hPa,VIS,WIND,WIND,WIND,GUST,RAIN,SNOW,CLD,RAIN,DEW,TMRW,TMRW,D+2,D+2,AQI,AQI,PM2.5,PM10,CO,CITY,OWM,,,</string>
|
||||
|
||||
<string id="MetricSteps">Steps</string>
|
||||
<string id="MetricCalories">Calories</string>
|
||||
<string id="MetricDistance">Distance</string>
|
||||
<string id="MetricFloors">Floors</string>
|
||||
<string id="MetricMoveBar">Move Bar</string>
|
||||
<string id="MetricActiveMin">Active Minutes</string>
|
||||
<string id="MetricBodyBattery">Body Battery</string>
|
||||
</strings>
|
||||
@@ -6,12 +6,29 @@
|
||||
<string id="SettingRingTL">Ring · Top-Left</string>
|
||||
<string id="SettingRingBR">Ring · Bottom-Right</string>
|
||||
<string id="SettingRingBL">Ring · Bottom-Left</string>
|
||||
<string id="SettingLeftTop">Top-Left Complication</string>
|
||||
<string id="SettingRightTop">Top-Right Complication</string>
|
||||
<string id="SettingLeftTop">Top-Left Field</string>
|
||||
<string id="SettingRightTop">Top-Right Field</string>
|
||||
<string id="SettingBottomLeft">Bottom-Left Field</string>
|
||||
<string id="SettingBottomRight">Bottom-Right Field</string>
|
||||
<string id="SettingTempUnit">Temperature Unit</string>
|
||||
<string id="SettingAltOffset1">Alternate Time 1 (minutes from local)</string>
|
||||
<string id="SettingAltOffset2">Alternate Time 2 (minutes from local)</string>
|
||||
<string id="SettingCountdownDate">Countdown Target (YYYY-MM-DD)</string>
|
||||
<string id="SettingTimeFmt">Custom Time Format (h m a)</string>
|
||||
<string id="SettingDateFmt1">Date Format 1 (D M Y w)</string>
|
||||
<string id="SettingDateFmt2">Date Format 2 (D M Y w)</string>
|
||||
<string id="SettingOwmKey">OpenWeatherMap API Key</string>
|
||||
<string id="SettingCustom1">Custom Text 1</string>
|
||||
<string id="SettingCustom2">Custom Text 2</string>
|
||||
<string id="SettingCustom3">Custom Text 3</string>
|
||||
<string id="SettingShowTop">Show Top Row</string>
|
||||
<string id="SettingShowBand">Show Date Band</string>
|
||||
<string id="SettingShowBottom">Show Bottom Row</string>
|
||||
|
||||
<string id="TempUnitAuto">Follow watch</string>
|
||||
<string id="TempUnitC">Celsius (°C)</string>
|
||||
<string id="TempUnitF">Fahrenheit (°F)</string>
|
||||
|
||||
<string id="ThemeEmber">1 · Ember</string>
|
||||
<string id="ThemeAurora">2 · Aurora</string>
|
||||
<string id="ThemeBrass">3 · Brass</string>
|
||||
@@ -20,16 +37,9 @@
|
||||
<string id="ThemeAcid">6 · Acid</string>
|
||||
<string id="ThemeKelp">7 · Kelp</string>
|
||||
|
||||
<string id="MetricSteps">Steps</string>
|
||||
<string id="MetricCalories">Calories</string>
|
||||
<string id="MetricDistance">Distance</string>
|
||||
<string id="MetricFloors">Floors</string>
|
||||
<string id="MetricMoveBar">Move Bar</string>
|
||||
|
||||
<string id="FieldWeather">Weather</string>
|
||||
<string id="FieldCalories">Calories</string>
|
||||
<string id="FieldSteps">Steps</string>
|
||||
<string id="FieldDistance">Distance</string>
|
||||
<string id="FieldFloors">Floors</string>
|
||||
<string id="FieldBattery">Battery</string>
|
||||
<!-- On-face wording. Comma-separated lists are split at runtime. -->
|
||||
<string id="DowNames">SUN,MON,TUE,WED,THU,FRI,SAT</string>
|
||||
<string id="MonNames">JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC</string>
|
||||
<string id="DateFormat">$1$ $2$</string>
|
||||
<string id="Meridiem">AM,PM</string>
|
||||
</strings>
|
||||
|
||||
184
source/Comp.mc
Normal file
@@ -0,0 +1,184 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Complications;
|
||||
import Toybox.System;
|
||||
|
||||
// ============================================================================
|
||||
// 系统 Complication 桥接
|
||||
// ----------------------------------------------------------------------------
|
||||
// Toybox.Complications(CIQ 4.2+,本项目支持的 17 款设备全都有)让表盘可以订阅
|
||||
// 系统和**其它 App 发布**的复杂功能数据。
|
||||
//
|
||||
// 这个模块解锁了一批我原本判断「表盘做不到」的字段 —— 当时只查了
|
||||
// ActivityMonitor / UserProfile / Weather,漏掉了这条路:
|
||||
//
|
||||
// 日历事件 CIQ 确实没有日历 API,但系统把「下一个日程」作为
|
||||
// complication 发布出来了
|
||||
// 训练状态 ActivityMonitor.Info 上没有,这里有
|
||||
// 完赛/配速预测 同上,5k/10k/半马/全马各两套
|
||||
// 本周跑步/骑行距离 这是**按运动分类**的,ActivityMonitor 的按天历史做不出来
|
||||
// 高尔夫成绩 无其它来源
|
||||
// 第三方 App CGM、补水、行情这类 App 发布的复杂功能,
|
||||
// 通过迭代器按序号取(对应目录里的 App 1..App 5)
|
||||
//
|
||||
// 取值策略:每次绘制现查(getComplication 很轻量),不做订阅回调 ——
|
||||
// 表盘每分钟才重绘一次,订阅的实时性没有意义,反而要维护状态。
|
||||
// ============================================================================
|
||||
module Comp {
|
||||
|
||||
// 字段编号 → complication 类型。
|
||||
// 返回 null 表示这个编号不归本模块管。
|
||||
function typeFor(id as Number) {
|
||||
if (id == 612) { return Complications.COMPLICATION_TYPE_CALENDAR_EVENTS; }
|
||||
if (id == 626) { return Complications.COMPLICATION_TYPE_TRAINING_STATUS; }
|
||||
if (id == 627) { return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_5K; }
|
||||
if (id == 628) { return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_10K; }
|
||||
if (id == 629) {
|
||||
return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_HALF_MARATHON;
|
||||
}
|
||||
if (id == 630) {
|
||||
return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_MARATHON;
|
||||
}
|
||||
if (id == 631) { return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_5K; }
|
||||
if (id == 632) { return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_10K; }
|
||||
if (id == 633) {
|
||||
return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_HALF_MARATHON;
|
||||
}
|
||||
if (id == 634) {
|
||||
return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_MARATHON;
|
||||
}
|
||||
if (id == 641) {
|
||||
return Complications.COMPLICATION_TYPE_LAST_GOLF_ROUND_SCORE;
|
||||
}
|
||||
if (id == 619) {
|
||||
return Complications.COMPLICATION_TYPE_WEEKLY_RUN_DISTANCE;
|
||||
}
|
||||
if (id == 620) {
|
||||
return Complications.COMPLICATION_TYPE_WEEKLY_BIKE_DISTANCE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 第三方 App 列表的每帧缓存。
|
||||
//
|
||||
// 枚举一次要走完整个迭代器,四个数据位如果都选了 App 槽就会走四遍。
|
||||
// 表盘每分钟才画一次,本来不算大开销,但 onUpdate 有看门狗时限,
|
||||
// 能省则省 —— 所以每帧只枚举一次,由 view 在绘制开始时调 beginFrame() 失效。
|
||||
var mApps as Array<Complications.Complication>?;
|
||||
|
||||
function beginFrame() as Void {
|
||||
mApps = null;
|
||||
}
|
||||
|
||||
// 695..699 是「第三方 App 1..5」,不对应固定类型,靠迭代器按序号取。
|
||||
function isAppSlot(id as Number) as Boolean {
|
||||
return id >= 695 && id <= 699;
|
||||
}
|
||||
|
||||
function owns(id as Number) as Boolean {
|
||||
return isAppSlot(id) || typeFor(id) != null;
|
||||
}
|
||||
|
||||
// 按类型取一个 complication 对象;取不到返回 null。
|
||||
// 用 try 包住是因为设备不支持某个类型时 getComplication 会抛,而不是返回
|
||||
// null —— 和本项目里其它 CIQ API 的老毛病一样。
|
||||
function byType(type) as Complications.Complication? {
|
||||
try {
|
||||
return Complications.getComplication(new Complications.Id(type));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 取第 n 个**第三方发布**的 complication(n 从 0 起)。
|
||||
//
|
||||
// 迭代器里既有系统自带的,也有别的 App 发布的。这里只保留「有短标签且有值」
|
||||
// 的项 —— 没装对应 App 时那些是空壳,全留着的话序号会随手表上装了什么 App
|
||||
// 而漂移,用户设好的第 3 个明天可能就变成别的东西了。
|
||||
//
|
||||
// Complication 的成员是 complicationId / value / unit / shortLabel /
|
||||
// longLabel / ranges(没有 label,别写错)。
|
||||
function nthApp(n as Number) as Complications.Complication? {
|
||||
if (mApps == null) {
|
||||
var list = [] as Array<Complications.Complication>;
|
||||
try {
|
||||
var it = Complications.getComplications();
|
||||
if (it != null) {
|
||||
var c = it.next();
|
||||
while (c != null) {
|
||||
if (c.value != null && c.shortLabel != null
|
||||
&& c.shortLabel instanceof Lang.String) {
|
||||
list.add(c);
|
||||
}
|
||||
c = it.next();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 设备不支持枚举时就当作一个都没有。
|
||||
}
|
||||
mApps = list;
|
||||
}
|
||||
return (n >= 0 && n < mApps.size()) ? mApps[n] : null;
|
||||
}
|
||||
|
||||
// 把 complication 的值转成可显示的字符串。
|
||||
//
|
||||
// value 的类型随 complication 而变(数字、字符串都有),unit 有时带单位词。
|
||||
// 完赛/配速预测的 value 是**秒数**,直接印出来是一串没意义的数字,
|
||||
// 所以单独格式化成 h:mm:ss / mm:ss。
|
||||
function render(c as Complications.Complication?, id as Number) as String? {
|
||||
if (c == null || c.value == null) { return null; }
|
||||
var v = c.value;
|
||||
if (isTimeSeconds(id) && !(v instanceof Lang.String)) {
|
||||
return hms(v.toNumber());
|
||||
}
|
||||
// 浮点直接 toString 会印成 "0.000000",统一保留一位小数。
|
||||
if (v instanceof Lang.Float || v instanceof Lang.Double) {
|
||||
return trimUnit(v.toFloat().format("%.1f"), c);
|
||||
}
|
||||
var s = v.toString();
|
||||
// 单位词能明显帮上忙时才拼(比如距离的 km),纯数字类就不拼了。
|
||||
//
|
||||
// ⚠️ unit 的类型不保证是 String —— 周跑步距离那种返回的不是字符串,
|
||||
// 直接调 .length() 会抛 "Could not find symbol 'length'"。
|
||||
// 所以先用 instanceof 判一次再说。
|
||||
if (c.unit != null && c.unit instanceof Lang.String) {
|
||||
var u = c.unit as String;
|
||||
if (u.length() > 0 && u.length() <= 3) { s = s + u; }
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// 把单位词拼到已格式化好的数值后面(如果 unit 确实是个短字符串)。
|
||||
function trimUnit(s as String, c as Complications.Complication) as String {
|
||||
if (c.unit != null && c.unit instanceof Lang.String) {
|
||||
var u = c.unit as String;
|
||||
if (u.length() > 0 && u.length() <= 3) { return s + u; }
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// 完赛预测(627–630)与配速预测(631–634)都是秒。
|
||||
function isTimeSeconds(id as Number) as Boolean {
|
||||
return id >= 627 && id <= 634;
|
||||
}
|
||||
|
||||
// 秒 → h:mm:ss,不足一小时则 m:ss。配速通常是后者。
|
||||
function hms(total as Number) as String {
|
||||
if (total <= 0) { return "--"; }
|
||||
var h = total / 3600;
|
||||
var m = (total % 3600) / 60;
|
||||
var s = total % 60;
|
||||
if (h > 0) {
|
||||
return h.format("%d") + ":" + m.format("%02d") + ":" + s.format("%02d");
|
||||
}
|
||||
return m.format("%d") + ":" + s.format("%02d");
|
||||
}
|
||||
|
||||
// 字段取值总入口。不属于本模块的编号返回 null。
|
||||
function value(id as Number) as String? {
|
||||
if (!owns(id)) { return null; }
|
||||
var c = isAppSlot(id) ? nthApp(id - 695) : byType(typeFor(id));
|
||||
var out = render(c, id);
|
||||
return out == null ? "--" : out;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
import Toybox.Application;
|
||||
import Toybox.Background;
|
||||
import Toybox.Lang;
|
||||
import Toybox.System;
|
||||
import Toybox.Time;
|
||||
import Toybox.WatchUi;
|
||||
import Owm;
|
||||
|
||||
// ============================================================================
|
||||
// 应用入口
|
||||
// ----------------------------------------------------------------------------
|
||||
// 表盘没有菜单也拿不到输入,所以这个壳子只做三件事:
|
||||
// 1. 交出 view
|
||||
// 2. 设置变更时请求重绘
|
||||
// 3. 管理拉取 OpenWeatherMap 的后台服务
|
||||
// ============================================================================
|
||||
class Fenix8V3App extends Application.AppBase {
|
||||
|
||||
// OWM 免费档的调用额度很宽裕,但表盘每分钟才重绘一次、数据本身也是小时级的,
|
||||
// 拉太勤只是白白唤醒射频费电。半小时是个合适的折中。
|
||||
const OWM_PERIOD_MIN = 30;
|
||||
|
||||
function initialize() {
|
||||
AppBase.initialize();
|
||||
}
|
||||
@@ -14,13 +30,55 @@ class Fenix8V3App extends Application.AppBase {
|
||||
function onStop(state as Dictionary?) as Void {
|
||||
}
|
||||
|
||||
// ⚠️ 定时器的注册必须放在 getInitialView 里,不能放 onStart。
|
||||
// 因为 onStart **在后台服务进程里也会被调用一次**,而在后台进程里调
|
||||
// registerForTemporalEvent 会抛 Out of Bounds。
|
||||
// getInitialView 只在前台进程运行,是安全的位置。
|
||||
function getInitialView() as [Views] or [Views, InputDelegates] {
|
||||
scheduleWeather();
|
||||
return [new Fenix8V3View()];
|
||||
}
|
||||
|
||||
function getServiceDelegate() as [System.ServiceDelegate] {
|
||||
return [new Fenix8V3Background()];
|
||||
}
|
||||
|
||||
// 只有用户真的填了 API key 才注册定时器 —— 没填就没什么可拉的,
|
||||
// 注册了纯属浪费电。反过来,用户把 key 清空时也要及时注销。
|
||||
function scheduleWeather() as Void {
|
||||
if (!(Background has :registerForTemporalEvent)) { return; }
|
||||
// ⚠️ deleteTemporalEvent 在「本来就没注册」时会抛 Out of Bounds,
|
||||
// 而这个错误是 VM 级的,**try/catch 抓不住**。
|
||||
// 所以这里不去问系统当前状态,而是自己在 Storage 里记一个标志位。
|
||||
// 重复注册本身是无害的(只是重置周期),所以只在状态真的变化时动手。
|
||||
var want = Owm.enabled();
|
||||
var have = Application.Storage.getValue("bgOn");
|
||||
have = (have != null) && (have as Boolean);
|
||||
if (want == have) { return; }
|
||||
if (want) {
|
||||
Background.registerForTemporalEvent(
|
||||
new Time.Duration(OWM_PERIOD_MIN * 60));
|
||||
} else {
|
||||
Background.deleteTemporalEvent();
|
||||
}
|
||||
Application.Storage.setValue("bgOn", want);
|
||||
}
|
||||
|
||||
// 手机把新设置推下来时触发。
|
||||
// view 每次绘制都是现读设置,所以重绘一下就够了;
|
||||
// 但天气定时器要跟着新的 key 状态启停,所以先过一遍 scheduleWeather。
|
||||
function onSettingsChanged() as Void {
|
||||
scheduleWeather();
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
// 后台服务把结果通过这里交回前台。存进 Storage 供表盘读取,然后重绘。
|
||||
function onBackgroundData(data as Application.PersistableType) as Void {
|
||||
if (data != null) {
|
||||
Application.Storage.setValue(Owm.STORE, data);
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getApp() as Fenix8V3App {
|
||||
|
||||
193
source/Fenix8V3Background.mc
Normal file
@@ -0,0 +1,193 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Application;
|
||||
import Toybox.Background;
|
||||
import Toybox.Communications;
|
||||
import Toybox.System;
|
||||
import Toybox.Time;
|
||||
import Toybox.Position;
|
||||
|
||||
// ============================================================================
|
||||
// OpenWeatherMap —— 后台拉取端
|
||||
// ----------------------------------------------------------------------------
|
||||
// 表盘进程绝大多数时间没在运行,onUpdate 又必须够快,不可能在绘制路径里发
|
||||
// HTTP 请求。Connect IQ 的方案是**后台服务**:系统按定时器唤醒一个独立进程,
|
||||
// 在里面发请求,把结果通过 Background.exit() 交回前台,前台再写进 Storage。
|
||||
//
|
||||
// 每次唤醒发三个请求:
|
||||
// 1. 当前天气 /data/2.5/weather
|
||||
// 2. 短期预报 /data/2.5/forecast?cnt=16(约 48 小时,够算明日/后日高低温)
|
||||
// 3. 空气质量 /data/2.5/air_pollution(AQI / PM2.5 / PM10 / CO)
|
||||
//
|
||||
// ⚠️ 预报接口必须带 cnt 限制条数。后台进程的内存预算非常小(约 32KB),
|
||||
// 完整的 5 天 / 3 小时预报解析出来能有几十 KB,会直接爆掉。
|
||||
//
|
||||
// 响应会被**压平成一个扁平字典**再交回去,键名是短名("temp"/"hi1"…),
|
||||
// 与 Owm.get() 的取值一一对应 —— 这样表盘侧永远不用扛一棵 JSON 树。
|
||||
// ============================================================================
|
||||
(:background)
|
||||
class Fenix8V3Background extends System.ServiceDelegate {
|
||||
|
||||
private var mResult as Dictionary = {};
|
||||
private var mPending = 0;
|
||||
|
||||
function initialize() {
|
||||
ServiceDelegate.initialize();
|
||||
}
|
||||
|
||||
// 系统按定时器唤醒后台进程时调用。
|
||||
// 注意这里是**独立进程**,内存预算比前台小得多,而且不能碰 UI。
|
||||
function onTemporalEvent() as Void {
|
||||
if (!Owm.enabled()) {
|
||||
Background.exit(null);
|
||||
return;
|
||||
}
|
||||
var lat = Application.Storage.getValue("lastLat");
|
||||
var lon = Application.Storage.getValue("lastLon");
|
||||
if (lat == null || lon == null) {
|
||||
// 表盘一拿到位置就会存进 Storage(见 Fields.Ctx.where)。
|
||||
// 在那之前没有坐标可问,直接退出。
|
||||
Background.exit(null);
|
||||
return;
|
||||
}
|
||||
mResult = {};
|
||||
mPending = 3; // 当前天气 + 短预报 + 空气质量
|
||||
// 一律用 units=metric 请求,单位换算统一放到 Fields 里按用户设置做,
|
||||
// 这样切换单位不用重新联网。
|
||||
var common = {"lat" => lat, "lon" => lon,
|
||||
"appid" => Owm.key(), "units" => "metric"};
|
||||
var opts = {
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
Communications.makeWebRequest(
|
||||
"https://api.openweathermap.org/data/2.5/weather",
|
||||
common, opts, method(:onCurrent));
|
||||
var fc = {"lat" => lat, "lon" => lon, "appid" => Owm.key(),
|
||||
"units" => "metric", "cnt" => 16};
|
||||
Communications.makeWebRequest(
|
||||
"https://api.openweathermap.org/data/2.5/forecast",
|
||||
fc, opts, method(:onForecast));
|
||||
// 空气质量走独立接口。它在免费档里是开放的(One Call 才要订阅),
|
||||
// 所以 AQI / PM2.5 / PM10 / CO 这几项不用付费也能有。
|
||||
Communications.makeWebRequest(
|
||||
"https://api.openweathermap.org/data/2.5/air_pollution",
|
||||
{"lat" => lat, "lon" => lon, "appid" => Owm.key()},
|
||||
opts, method(:onAir));
|
||||
}
|
||||
|
||||
// 当前天气的回调。OWM 的 JSON 是嵌套的,逐层取值时每层都要判空 ——
|
||||
// 缺字段是常态(比如没下雨就没有 rain 节点)。
|
||||
function onCurrent(code as Number, body as Dictionary?) as Void {
|
||||
if (code == 200 && body != null) {
|
||||
var main = body.get("main") as Dictionary?;
|
||||
if (main != null) {
|
||||
put("temp", main.get("temp"));
|
||||
put("feels", main.get("feels_like"));
|
||||
put("tmin", main.get("temp_min"));
|
||||
put("tmax", main.get("temp_max"));
|
||||
put("pressure", main.get("pressure"));
|
||||
put("humidity", main.get("humidity"));
|
||||
}
|
||||
put("visibility", body.get("visibility"));
|
||||
var wind = body.get("wind") as Dictionary?;
|
||||
if (wind != null) {
|
||||
put("wind", wind.get("speed"));
|
||||
put("winddeg", wind.get("deg"));
|
||||
put("gust", wind.get("gust"));
|
||||
}
|
||||
var clouds = body.get("clouds") as Dictionary?;
|
||||
if (clouds != null) { put("clouds", clouds.get("all")); }
|
||||
var rain = body.get("rain") as Dictionary?;
|
||||
if (rain != null) { put("rain", rain.get("1h")); }
|
||||
var snow = body.get("snow") as Dictionary?;
|
||||
if (snow != null) { put("snow", snow.get("1h")); }
|
||||
var wx = body.get("weather") as Array?;
|
||||
if (wx != null && wx.size() > 0) {
|
||||
var w0 = wx[0] as Dictionary;
|
||||
put("short", w0.get("main"));
|
||||
put("long", w0.get("description"));
|
||||
}
|
||||
put("city", body.get("name"));
|
||||
put("dt", Time.now().value());
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
// 预报返回的是 3 小时粒度的列表。这里按「距今天几天」把条目分到
|
||||
// 明天 / 后天两个桶里,各自取最高最低温;降水概率取明天的最大值。
|
||||
function onForecast(code as Number, body as Dictionary?) as Void {
|
||||
if (code == 200 && body != null) {
|
||||
var list = body.get("list") as Array?;
|
||||
if (list != null) {
|
||||
var today = dayNumber(Time.now().value());
|
||||
var lo1 = null; var hi1 = null; var lo2 = null; var hi2 = null;
|
||||
var pop1 = null;
|
||||
var arr = list;
|
||||
for (var i = 0; i < arr.size(); i++) {
|
||||
var e = arr[i] as Dictionary;
|
||||
var dt = e.get("dt");
|
||||
if (dt == null) { continue; }
|
||||
var day = dayNumber(dt as Number) - today;
|
||||
var m = e.get("main") as Dictionary?;
|
||||
if (m == null) { continue; }
|
||||
var tmin = m.get("temp_min");
|
||||
var tmax = m.get("temp_max");
|
||||
if (day == 1) {
|
||||
if (lo1 == null || tmin < lo1) { lo1 = tmin; }
|
||||
if (hi1 == null || tmax > hi1) { hi1 = tmax; }
|
||||
var pop = e.get("pop");
|
||||
if (pop != null && (pop1 == null || pop > pop1)) { pop1 = pop; }
|
||||
} else if (day == 2) {
|
||||
if (lo2 == null || tmin < lo2) { lo2 = tmin; }
|
||||
if (hi2 == null || tmax > hi2) { hi2 = tmax; }
|
||||
}
|
||||
}
|
||||
put("lo1", lo1); put("hi1", hi1);
|
||||
put("lo2", lo2); put("hi2", hi2);
|
||||
put("pop", pop1);
|
||||
}
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
// 空气质量的回调。响应形如 { list: [ { main: {aqi}, components: {...} } ] }。
|
||||
function onAir(code as Number, body as Dictionary?) as Void {
|
||||
if (code == 200 && body != null) {
|
||||
var list = body.get("list") as Array?;
|
||||
if (list != null && list.size() > 0) {
|
||||
var e = list[0] as Dictionary;
|
||||
var main = e.get("main") as Dictionary?;
|
||||
if (main != null) { put("aqi", main.get("aqi")); }
|
||||
var comp = e.get("components") as Dictionary?;
|
||||
if (comp != null) {
|
||||
put("pm2_5", comp.get("pm2_5"));
|
||||
put("pm10", comp.get("pm10"));
|
||||
put("co", comp.get("co"));
|
||||
}
|
||||
}
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
// 距 Unix 纪元的整天数,用来给预报条目分桶。
|
||||
function dayNumber(unix as Number) as Number {
|
||||
return unix / 86400;
|
||||
}
|
||||
|
||||
// 只写入非空值,免得用 null 覆盖字典里已有的内容。
|
||||
function put(k as String, v) as Void {
|
||||
if (v != null) { mResult.put(k, v); }
|
||||
}
|
||||
|
||||
// 两个请求都回来了才算一轮完整数据,谁最后完成谁负责退出服务。
|
||||
// 一条都没拿到就传 null,避免用空字典覆盖掉上一次的好数据。
|
||||
function done() as Void {
|
||||
mPending--;
|
||||
if (mPending > 0) { return; }
|
||||
if (mResult.size() == 0) {
|
||||
Background.exit(null);
|
||||
} else {
|
||||
Background.exit(mResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,387 +1,453 @@
|
||||
import Toybox.Application;
|
||||
import Toybox.Graphics;
|
||||
import Toybox.Lang;
|
||||
import Toybox.Math;
|
||||
import Toybox.ActivityMonitor;
|
||||
import Toybox.Weather;
|
||||
import Toybox.SensorHistory;
|
||||
import Toybox.System;
|
||||
import Toybox.Time;
|
||||
import Toybox.Time.Gregorian;
|
||||
import Toybox.WatchUi;
|
||||
import Themes;
|
||||
import Fields;
|
||||
import Layout;
|
||||
import Icons;
|
||||
import Settings;
|
||||
import Comp;
|
||||
|
||||
// ============================================================================
|
||||
// 表盘主体
|
||||
// ----------------------------------------------------------------------------
|
||||
// 负责:进度环、盖在环上的各个元素、以及 AMOLED 常亮时的简化版画面。
|
||||
//
|
||||
// 分工约定(改代码前先看清楚该动哪个文件):
|
||||
// 位置尺寸 → Layout.mc
|
||||
// 数据取值 → Fields.mc
|
||||
// 配色 → Themes.mc(自动生成)
|
||||
// 设置读取 → Settings.mc
|
||||
// 图标 → Icons.mc
|
||||
// 留在本文件里的只有「按什么顺序画」。
|
||||
// ============================================================================
|
||||
class Fenix8V3View extends WatchUi.WatchFace {
|
||||
|
||||
// ----------------------------------------------------------------- utils
|
||||
function getSetting(name as String, def as Number) as Number {
|
||||
var v = Application.Properties.getValue(name);
|
||||
if (v == null) { return def; }
|
||||
return v as Number;
|
||||
// 常亮显示状态。
|
||||
// AMOLED 屏在息屏常亮时必须大幅减少点亮像素(Garmin 上架审核会卡这一条,
|
||||
// 而且不这么做既费电又有烧屏风险);MIP 屏不受影响,照常全量绘制。
|
||||
private var mSleeping = false;
|
||||
private var mBurnIn = false;
|
||||
|
||||
function initialize() {
|
||||
WatchFace.initialize();
|
||||
}
|
||||
|
||||
function getBool(name as String, def as Boolean) as Boolean {
|
||||
var v = Application.Properties.getValue(name);
|
||||
if (v == null) { return def; }
|
||||
return v as Boolean;
|
||||
// 进入低功耗(息屏)。AMOLED 机型从这一刻起改画简化版面。
|
||||
function onEnterSleep() as Void {
|
||||
mSleeping = true;
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function currentTheme() as Dictionary {
|
||||
var idx = getSetting("Theme", 1); // 1..7
|
||||
var i = idx - 1;
|
||||
if (i < 0) { i = 0; }
|
||||
if (i > 6) { i = 0; }
|
||||
var id = Themes.ORDER[i];
|
||||
return Themes.THEMES[id] as Dictionary;
|
||||
// 抬腕唤醒,恢复完整版面。
|
||||
function onExitSleep() as Void {
|
||||
mSleeping = false;
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
function lerpColor(c1 as Number, c2 as Number, f as Float) as Number {
|
||||
var r1 = (c1 & 0xFF0000) >> 16;
|
||||
var g1 = (c1 & 0x00FF00) >> 8;
|
||||
var b1 = (c1 & 0x0000FF);
|
||||
var r2 = (c2 & 0xFF0000) >> 16;
|
||||
var g2 = (c2 & 0x00FF00) >> 8;
|
||||
var b2 = (c2 & 0x0000FF);
|
||||
var r = (r1 + ((r2 - r1) * f).toNumber()).toNumber();
|
||||
var g = (g1 + ((g2 - g1) * f).toNumber()).toNumber();
|
||||
var b = (b1 + ((b2 - b1) * f).toNumber()).toNumber();
|
||||
return (r << 16) | (g << 8) | b;
|
||||
// 表盘加载时调一次;如果 onUpdate 发现缓存失效(分辨率变了,或资源还没
|
||||
// 加载好)会再调一次。所有「一次性、开销大」的准备工作都放这里。
|
||||
function onLayout(dc as Dc) as Void {
|
||||
Layout.init(dc);
|
||||
Fields.loadStrings();
|
||||
Icons.load();
|
||||
var ds = System.getDeviceSettings();
|
||||
mBurnIn = (ds != null) && (ds has :requiresBurnInProtection)
|
||||
&& ds.requiresBurnInProtection;
|
||||
}
|
||||
|
||||
// interpolate a gradient stop array [[angle,color],...] (angles ascending)
|
||||
function lerpStops(stops as Array<Array<Number>>, t as Number) as Number {
|
||||
var n = stops.size();
|
||||
if (n == 0) { return 0x888888; }
|
||||
var first = stops[0];
|
||||
var last = stops[n - 1];
|
||||
if (t <= first[0]) { return first[1]; }
|
||||
if (t >= last[0]) { return last[1]; }
|
||||
for (var i = 0; i < n - 1; i++) {
|
||||
var a = stops[i];
|
||||
var b = stops[i + 1];
|
||||
if (t >= a[0] && t <= b[0]) {
|
||||
var f = ((t - a[0]) / (b[0] - a[0])).toFloat();
|
||||
return lerpColor(a[1], b[1], f);
|
||||
}
|
||||
}
|
||||
return last[1];
|
||||
}
|
||||
|
||||
// progress 0..1 for a metric index
|
||||
function metricProgress(idx as Number, info as ActivityMonitor.Info) as Float {
|
||||
// ------------------------------- 进度环 -------------------------------
|
||||
// 计算四段弧各自的进度,0..1。
|
||||
// ⚠️ 这里的 idx 是**进度环自己的一张短表**(见 Settings.ringTR 的注释),
|
||||
// 和数据位那套 125 项的字段编号完全是两回事,别混。
|
||||
function ringProgress(idx as Number, info as ActivityMonitor.Info?) as Float {
|
||||
if (info == null) { return 0.0f; }
|
||||
var p = 0.0f;
|
||||
if (idx == 0) { // Steps
|
||||
if (idx == 0) { // 步数:对每日目标
|
||||
if (info.stepGoal != null && info.stepGoal > 0 && info.steps != null) {
|
||||
p = info.steps.toFloat() / info.stepGoal.toFloat();
|
||||
}
|
||||
} else if (idx == 1) { // Calories
|
||||
} else if (idx == 1) { // 卡路里:固定 2000 kcal 为满
|
||||
if (info.calories != null) { p = info.calories.toFloat() / 2000.0f; }
|
||||
} else if (idx == 2) { // Distance (cm -> 10 km cap)
|
||||
} else if (idx == 2) { // 距离:10 km 为满(distance 单位是厘米)
|
||||
if (info.distance != null) { p = info.distance.toFloat() / 1000000.0f; }
|
||||
} else if (idx == 3) { // Floors
|
||||
} else if (idx == 3) { // 楼层:对每日目标,取不到目标就按 10 层
|
||||
if (info.floorsClimbed != null) {
|
||||
var goal = (info.floorsClimbedGoal != null && info.floorsClimbedGoal > 0)
|
||||
? info.floorsClimbedGoal.toFloat() : 10.0f;
|
||||
p = info.floorsClimbed.toFloat() / goal;
|
||||
}
|
||||
} else if (idx == 4) { // Move Bar
|
||||
if (info.moveBarLevel != null) { p = info.moveBarLevel.toFloat(); }
|
||||
} else if (idx == 5) { // 活动分钟:对**每周**目标
|
||||
if ((info has :activeMinutesWeek) && info.activeMinutesWeek != null
|
||||
&& (info has :activeMinutesWeekGoal)
|
||||
&& info.activeMinutesWeekGoal != null
|
||||
&& info.activeMinutesWeekGoal > 0) {
|
||||
p = info.activeMinutesWeek.total.toFloat()
|
||||
/ info.activeMinutesWeekGoal.toFloat();
|
||||
}
|
||||
} else if (idx == 6) { // 身体电量:本身就是 0..100
|
||||
var bb = Fields.latest(SensorHistory.getBodyBatteryHistory({:period => 1}));
|
||||
if (bb != null) { p = bb.toFloat() / 100.0; }
|
||||
} else if (idx == 4) {
|
||||
// ⚠️ 动动条是**整数等级**(0..MOVE_BAR_LEVEL_MAX,通常是 5),
|
||||
// 不是 0..1 的比例。初版直接当比例用,结果等级≥1 就被钳成满环,
|
||||
// 中间状态全丢了。
|
||||
if (info.moveBarLevel != null) {
|
||||
var span = (ActivityMonitor.MOVE_BAR_LEVEL_MAX
|
||||
- ActivityMonitor.MOVE_BAR_LEVEL_MIN).toFloat();
|
||||
if (span > 0) {
|
||||
p = (info.moveBarLevel - ActivityMonitor.MOVE_BAR_LEVEL_MIN) / span;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p > 1.0f) { p = 1.0f; }
|
||||
if (p < 0.0f) { p = 0.0f; }
|
||||
return p;
|
||||
}
|
||||
|
||||
// value string for a top complication type
|
||||
function complicationValue(typeIdx as Number, info as ActivityMonitor.Info,
|
||||
weather as Weather.CurrentConditions, stats as System.Stats) as String {
|
||||
if (typeIdx == 0) { // Weather temp
|
||||
if (weather != null && weather.temperature != null) {
|
||||
return weather.temperature.format("%d") + "\u00B0";
|
||||
}
|
||||
return "--";
|
||||
} else if (typeIdx == 1) { // Calories
|
||||
return info != null && info.calories != null ? info.calories.format("%d") : "0";
|
||||
} else if (typeIdx == 2) { // Steps
|
||||
return info != null && info.steps != null ? info.steps.format("%d") : "0";
|
||||
} else if (typeIdx == 3) { // Distance km
|
||||
if (info != null && info.distance != null) {
|
||||
return (info.distance.toFloat() / 100000.0).format("%.1f");
|
||||
}
|
||||
return "0.0";
|
||||
} else if (typeIdx == 4) { // Floors
|
||||
return info != null && info.floorsClimbed != null ? info.floorsClimbed.format("%d") : "0";
|
||||
} else { // Battery
|
||||
var pct = 0;
|
||||
if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
|
||||
return pct.format("%d") + "%";
|
||||
}
|
||||
// 画一根刻度:环形扇区,用四点多边形填充。
|
||||
// 扇区两条弧边其实是曲线,但 3° 弧的弓高不到 0.1 像素,用直边完全够精确,
|
||||
// 而 fillPolygon 比 drawArc 行为更确定(各机型的 drawArc 端点样式不一致)。
|
||||
function tick(dc as Dc, ang as Float, hw as Float) as Void {
|
||||
var a0 = ang - hw;
|
||||
var a1 = ang + hw;
|
||||
var ro = Layout.R_OUT * Layout.scale;
|
||||
var ri = Layout.R_IN * Layout.scale;
|
||||
dc.fillPolygon([[Layout.polarX(a0, ro), Layout.polarY(a0, ro)],
|
||||
[Layout.polarX(a1, ro), Layout.polarY(a1, ro)],
|
||||
[Layout.polarX(a1, ri), Layout.polarY(a1, ri)],
|
||||
[Layout.polarX(a0, ri), Layout.polarY(a0, ri)]]);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- ring
|
||||
function drawTick(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
|
||||
w as Number, ang as Number, col as Number, mirror as Boolean) as Void {
|
||||
var eff = mirror ? -ang : ang;
|
||||
var rad = Math.toRadians(eff);
|
||||
var sin = Math.sin(rad);
|
||||
var cos = Math.cos(rad);
|
||||
var ix = (cx + sin * rIn).toNumber();
|
||||
var iy = (cy - cos * rIn).toNumber();
|
||||
var ox = (cx + sin * rOut).toNumber();
|
||||
var oy = (cy - cos * rOut).toNumber();
|
||||
dc.setPenWidth(w);
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawLine(ix, iy, ox, oy);
|
||||
}
|
||||
|
||||
function drawArcTop(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
|
||||
w as Number, angles as Array<Number>, stops as Array<Array<Number>>,
|
||||
off as Number, mirror as Boolean, lit as Number, anchorOuter as Boolean) as Void {
|
||||
var n = angles.size();
|
||||
// 通用画弧。点亮方向与设计稿一致:
|
||||
// 两条顶弧:从 9 点 / 3 点那一端朝 12 点方向填充(fromEnd=true)
|
||||
// 两条底弧:从 6 点朝两侧填充(fromEnd=false)
|
||||
// mirror 用来把同一组角度镜像到左半边,这样左右弧共用一份角度表和配色表。
|
||||
function arc(dc as Dc, ramp as Array<Number>, off as Number, lit as Number,
|
||||
phase as Float, pitch as Float, hw as Float, n as Number,
|
||||
base as Float, mirror as Boolean, fromEnd as Boolean) as Void {
|
||||
for (var i = 0; i < n; i++) {
|
||||
var a = angles[i];
|
||||
var on = anchorOuter ? (i >= n - lit) : (i < lit);
|
||||
var col = on ? lerpStops(stops, a) : off;
|
||||
drawTick(dc, cx, cy, rIn, rOut, w, a, col, mirror);
|
||||
var a = phase + pitch * i;
|
||||
var on = fromEnd ? (i >= n - lit) : (i < lit);
|
||||
dc.setColor(on ? ramp[i] : off, Graphics.COLOR_TRANSPARENT);
|
||||
tick(dc, base + (mirror ? -a : a), hw);
|
||||
}
|
||||
}
|
||||
|
||||
function drawArcBottom(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
|
||||
w as Number, botAngles as Array<Number>, stops as Array<Array<Number>>,
|
||||
off as Number, mirror as Boolean, lit as Number) as Void {
|
||||
var n = botAngles.size();
|
||||
for (var i = 0; i < n; i++) {
|
||||
var a = botAngles[i];
|
||||
var on = (i < lit);
|
||||
var ang = (180.0 - a).toNumber();
|
||||
var col = on ? lerpStops(stops, a) : off;
|
||||
drawTick(dc, cx, cy, rIn, rOut, w, ang, col, mirror);
|
||||
function drawRing(dc as Dc, ti as Number, pTR as Float, pTL as Float,
|
||||
pBR as Float, pBL as Float) as Void {
|
||||
// 四段弧共用两份配色表:两条顶弧用 RINGTOP,两条底弧用 RINGBOTTOM。
|
||||
// 左右弧靠 mirror 参数镜像,颜色按刻度下标取,所以左右对称位置同色。
|
||||
var top = Themes.RINGTOP[ti] as Array<Number>;
|
||||
var bot = Themes.RINGBOTTOM[ti] as Array<Number>;
|
||||
var off = Themes.TICKOFF[ti] as Number;
|
||||
|
||||
arc(dc, top, off, (pTR * Layout.TOP_N).toNumber(), Layout.TOP_PH,
|
||||
Layout.TOP_PITCH, Layout.TOP_HW, Layout.TOP_N, 0.0, false, true);
|
||||
arc(dc, top, off, (pTL * Layout.TOP_N).toNumber(), Layout.TOP_PH,
|
||||
Layout.TOP_PITCH, Layout.TOP_HW, Layout.TOP_N, 0.0, true, true);
|
||||
arc(dc, bot, off, (pBL * Layout.BOT_N).toNumber(), Layout.BOT_PH,
|
||||
Layout.BOT_PITCH, Layout.BOT_HW, Layout.BOT_N, 180.0, false, false);
|
||||
arc(dc, bot, off, (pBR * Layout.BOT_N).toNumber(), Layout.BOT_PH,
|
||||
Layout.BOT_PITCH, Layout.BOT_HW, Layout.BOT_N, 180.0, true, false);
|
||||
|
||||
// 四个锚点圆点,压在四段弧的接缝处。
|
||||
dc.setColor(Themes.ANCHORDOT[ti] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
var r = Layout.px(Layout.DOT_R);
|
||||
var rad = Layout.DOT_RAD * Layout.scale;
|
||||
for (var i = 0; i < Layout.DOT_ANG.size(); i++) {
|
||||
var a = Layout.DOT_ANG[i];
|
||||
dc.fillCircle(Layout.polarX(a, rad), Layout.polarY(a, rad), r);
|
||||
}
|
||||
}
|
||||
|
||||
function drawRing(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary,
|
||||
pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void {
|
||||
var rOut = (249.5 * s).toNumber();
|
||||
var rIn = (231.5 * s).toNumber();
|
||||
var tickOff = theme["tickOff"] as Number;
|
||||
var topStops = theme["ringTop"] as Array<Array<Number>>;
|
||||
var botStops = theme["ringBottom"] as Array<Array<Number>>;
|
||||
var topW = (13.0 * s).toNumber();
|
||||
var botW = (9.0 * s).toNumber();
|
||||
// ------------------------------- 各元素 -------------------------------
|
||||
// 顶部中央:电池图标 + 百分比。
|
||||
function drawBattery(dc as Dc, ti as Number, level as Float) as Void {
|
||||
Icons.battery(dc, Layout.BATT_X, Layout.BATT_CY, 1.0,
|
||||
Themes.ACCENT[ti] as Number, level);
|
||||
Layout.text(dc, Layout.BATT_VX, Layout.BATT_CY, Layout.fontBand,
|
||||
(level * 100.0).toNumber().format("%d") + "%",
|
||||
Graphics.TEXT_JUSTIFY_LEFT, Themes.TEXTPRIMARY[ti] as Number);
|
||||
}
|
||||
|
||||
var TOP = [2,6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74,78,82,86];
|
||||
var BOT = [6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66];
|
||||
// 顶部单个数据位:上方图标 + 下方数值。
|
||||
// 数值走 fittedText,太长会自动降字号,不会冲出表盘。
|
||||
function drawTopSlot(dc as Dc, ti as Number, id as Number, isLeft as Boolean,
|
||||
ctx as Fields.Ctx, level as Float) as Void {
|
||||
var x = isLeft ? Layout.LEFT_X : Layout.RIGHT_X;
|
||||
Icons.forField(dc, id, x, Themes.ACCENT[ti] as Number, level);
|
||||
Layout.fittedText(dc, x, Layout.VALUE_CY, Layout.fontComp,
|
||||
Fields.value(id, ctx), Graphics.TEXT_JUSTIFY_CENTER,
|
||||
Themes.TEXTPRIMARY[ti] as Number, Layout.TOP_MAX_W);
|
||||
}
|
||||
|
||||
var litTR = (pTR * 22.0).toNumber();
|
||||
var litTL = (pTL * 22.0).toNumber();
|
||||
var litBR = (pBR * 21.0).toNumber();
|
||||
var litBL = (pBL * 21.0).toNumber();
|
||||
|
||||
drawArcTop(dc, cx, cy, rIn, rOut, topW, TOP, topStops, tickOff, false, litTR, true);
|
||||
drawArcTop(dc, cx, cy, rIn, rOut, topW, TOP, topStops, tickOff, true, litTL, true);
|
||||
drawArcBottom(dc, cx, cy, rIn, rOut, botW, BOT, botStops, tickOff, true, litBL);
|
||||
drawArcBottom(dc, cx, cy, rIn, rOut, botW, BOT, botStops, tickOff, false, litBR);
|
||||
|
||||
// anchor dots
|
||||
var dotR = (9.5 * s).toNumber();
|
||||
var dotRad = (240.5 * s).toNumber();
|
||||
var dotCol = theme["anchorDot"] as Number;
|
||||
var dotAngles = [0.0, 93.3, 180.0, 266.7];
|
||||
dc.setColor(dotCol, Graphics.COLOR_TRANSPARENT);
|
||||
for (var i = 0; i < dotAngles.size(); i++) {
|
||||
var rad = Math.toRadians(dotAngles[i]);
|
||||
var dx = (cx + Math.sin(rad) * dotRad).toNumber();
|
||||
var dy = (cy - Math.cos(rad) * dotRad).toNumber();
|
||||
dc.fillCircle(dx, dy, dotR);
|
||||
// 日期带:整条横幅底色 + 蓝牙符号 + 星期 + 月日 + 上午/下午。
|
||||
// 24 小时制下不显示上午/下午(跟随手表设置)。
|
||||
function drawDateBand(dc as Dc, ti as Number, ds as System.DeviceSettings?) as Void {
|
||||
var fill = Themes.BANDFILL[ti] as Number;
|
||||
dc.setColor(fill, fill);
|
||||
dc.fillRectangle(0, Layout.px(Layout.BAND_Y), Layout.width,
|
||||
Layout.px(Layout.BAND_H));
|
||||
var col = Themes.BANDTEXT[ti] as Number;
|
||||
// 手机断连时把蓝牙符号**压暗**而不是隐藏 —— 否则手机一走远日期带就
|
||||
// 少一块,整条带子的视觉重心跟着变,很干扰。
|
||||
Icons.bluetooth(dc, (ds != null && !ds.phoneConnected)
|
||||
? blend(col, fill, 0.6) : col);
|
||||
// ⚠️ 必须用 FORMAT_SHORT,不能用 FORMAT_MEDIUM。
|
||||
// 只有短格式的 day_of_week / month 返回的是**数字**;中格式返回的是
|
||||
// 本地化字符串,拿去做数组下标会直接抛类型异常 —— 初版就是这么崩的,
|
||||
// 而且是每次更新都崩。
|
||||
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
Layout.text(dc, Layout.DOW_CX, Layout.BAND_CY, Layout.fontBand,
|
||||
Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, col);
|
||||
Layout.text(dc, Layout.MD_CX, Layout.BAND_CY, Layout.fontBand,
|
||||
Fields.monthDay(g), Graphics.TEXT_JUSTIFY_CENTER, col);
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
Layout.text(dc, Layout.AMPM_R, Layout.BAND_CY, Layout.fontBand,
|
||||
Fields.meridiem(g), Graphics.TEXT_JUSTIFY_RIGHT, col);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- complications
|
||||
function drawBattery(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary,
|
||||
stats as System.Stats) as Void {
|
||||
var y = (53.5 * s).toNumber();
|
||||
var pct = 0;
|
||||
if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
|
||||
var txt = pct.format("%d") + "%";
|
||||
var gw = (30.0 * s).toNumber();
|
||||
var gh = (16.0 * s).toNumber();
|
||||
var gx = (cx - 52.0 * s).toNumber();
|
||||
var gy = (y - gh / 2).toNumber();
|
||||
dc.setPenWidth(2);
|
||||
dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawRectangle(gx, gy, gw, gh);
|
||||
dc.fillRectangle((gx + gw).toNumber(), (gy + gh * 0.28).toNumber(),
|
||||
(3.0 * s).toNumber(), (gh * 0.44).toNumber());
|
||||
var fillW = ((gw - 4) * (pct.toFloat() / 100.0)).toNumber();
|
||||
dc.fillRectangle((gx + 2).toNumber(), (gy + 2).toNumber(), fillW, (gh - 4).toNumber());
|
||||
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText((cx + 16.0 * s).toNumber(), y, Graphics.FONT_SMALL, txt,
|
||||
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
// 中央时间。小时和分钟各自带竖向渐变(gradText 内部会分带多次绘制),
|
||||
// 中间的冒号是两个实心方块,位置写死在设计稿坐标上。
|
||||
function drawTime(dc as Dc, ti as Number, ds as System.DeviceSettings?) as Void {
|
||||
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
var h = g.hour;
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
Layout.gradText(dc, Layout.HOURS_R, Layout.TIME_CY, Layout.fontTime,
|
||||
h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT,
|
||||
Themes.HOURS[ti] as Array<Number>);
|
||||
Layout.gradText(dc, Layout.MINUTES_L, Layout.TIME_CY, Layout.fontTime,
|
||||
g.min.format("%02d"), Graphics.TEXT_JUSTIFY_LEFT,
|
||||
Themes.MINUTES[ti] as Array<Number>);
|
||||
dc.setColor(Themes.COLON[ti] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillRectangle(Layout.px(Layout.COLON_X), Layout.px(Layout.COLON_Y1),
|
||||
Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H));
|
||||
dc.fillRectangle(Layout.px(Layout.COLON_X), Layout.px(Layout.COLON_Y2),
|
||||
Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H));
|
||||
}
|
||||
|
||||
function drawComplicationIcon(dc as Dc, x as Number, y as Number, r as Number, col as Number,
|
||||
typeIdx as Number) as Void {
|
||||
dc.setPenWidth(2);
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
if (typeIdx == 0) { // sun
|
||||
dc.fillCircle(x, y, (r * 0.45).toNumber());
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var rad = Math.toRadians(i * 45.0);
|
||||
var x1 = (x + Math.sin(rad) * r * 0.6).toNumber();
|
||||
var y1 = (y - Math.cos(rad) * r * 0.6).toNumber();
|
||||
var x2 = (x + Math.sin(rad) * r).toNumber();
|
||||
var y2 = (y - Math.cos(rad) * r).toNumber();
|
||||
dc.drawLine(x1, y1, x2, y2);
|
||||
// 底部两格:各自「数值 + 小标签」。
|
||||
// 标签跟随所选字段自动变化,不需要单独配置。
|
||||
function drawBottom(dc as Dc, ti as Number, ctx as Fields.Ctx) as Void {
|
||||
var value = Themes.TEXTPRIMARY[ti] as Number;
|
||||
var label = Themes.ACCENT[ti] as Number;
|
||||
var left = Settings.bottomLeft();
|
||||
var right = Settings.bottomRight();
|
||||
Layout.fittedText(dc, Layout.DIST_R, Layout.BOT_VAL_CY, Layout.fontComp,
|
||||
Fields.value(left, ctx), Graphics.TEXT_JUSTIFY_RIGHT,
|
||||
value, Layout.BOT_MAX_W);
|
||||
Layout.fittedText(dc, Layout.DIST_R, Layout.BOT_LAB_CY, Layout.fontComp,
|
||||
Fields.label(left), Graphics.TEXT_JUSTIFY_RIGHT,
|
||||
label, Layout.BOT_MAX_W);
|
||||
Layout.fittedText(dc, Layout.STEP_L, Layout.BOT_VAL_CY, Layout.fontComp,
|
||||
Fields.value(right, ctx), Graphics.TEXT_JUSTIFY_LEFT,
|
||||
value, Layout.BOT_MAX_W);
|
||||
Layout.fittedText(dc, Layout.STEP_L, Layout.BOT_LAB_CY, Layout.fontComp,
|
||||
Fields.label(right), Graphics.TEXT_JUSTIFY_LEFT,
|
||||
label, Layout.BOT_MAX_W);
|
||||
}
|
||||
|
||||
// ------------------------------- 颜色工具 ------------------------------
|
||||
// 两色按比例混合,f=0 取 c1,f=1 取 c2。
|
||||
function blend(c1 as Number, c2 as Number, f as Float) as Number {
|
||||
var r1 = (c1 >> 16) & 0xFF;
|
||||
var g1 = (c1 >> 8) & 0xFF;
|
||||
var b1 = c1 & 0xFF;
|
||||
var r = r1 + (((((c2 >> 16) & 0xFF) - r1) * f)).toNumber();
|
||||
var g = g1 + (((((c2 >> 8) & 0xFF) - g1) * f)).toNumber();
|
||||
var b = b1 + ((((c2 & 0xFF) - b1) * f)).toNumber();
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
// 整体压暗到 f 倍(常亮模式用)。
|
||||
function dim(col as Number, f as Float) as Number {
|
||||
var r = (((col >> 16) & 0xFF) * f).toNumber();
|
||||
var g = (((col >> 8) & 0xFF) * f).toNumber();
|
||||
var b = ((col & 0xFF) * f).toNumber();
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
// ------------------------------- 常亮模式 ------------------------------
|
||||
// 低功耗画面。相比正常画面砍掉的东西:
|
||||
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
|
||||
// - **整个进度环**
|
||||
// - 时间、冒号、日期压暗到 38%
|
||||
//
|
||||
// 度量口径要注意:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,和实际
|
||||
// 功耗对不上;AMOLED 功耗更接近各子像素亮度之和(平均亮度)。
|
||||
// 本实现两个口径都留了余量,实测数字见 README 的常亮小节。
|
||||
// - 四个数据位、电池(全部隐藏)
|
||||
// 保留的时间和日期压暗到 55%。
|
||||
//
|
||||
// 另外整个画面按分钟在 ±4 像素内游走,避免同一个像素长期点亮导致烧屏。
|
||||
// pTR/pTL/pBR/pBL 现在用不上(常亮下不画环),保留形参是为了以后要把环
|
||||
// 加回来时不必改调用点。
|
||||
function drawAmbient(dc as Dc, ti as Number, ds as System.DeviceSettings?,
|
||||
pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void {
|
||||
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
var ox = ((g.min % 5) - 2) * 2.0 / Layout.scale; // ±4 设备像素,换算回设计像素
|
||||
var oy = ((g.min / 5) % 5 - 2) * 2.0 / Layout.scale;
|
||||
|
||||
// 进度环在常亮下不画 —— 它是点亮像素数的大头。
|
||||
|
||||
var h = g.hour;
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
var hours = Themes.HOURS[ti] as Array<Number>;
|
||||
var mins = Themes.MINUTES[ti] as Array<Number>;
|
||||
Layout.text(dc, Layout.HOURS_R + ox, Layout.TIME_CY + oy, Layout.fontTime,
|
||||
h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT,
|
||||
dim(hours[hours.size() / 2], 0.38));
|
||||
Layout.text(dc, Layout.MINUTES_L + ox, Layout.TIME_CY + oy, Layout.fontTime,
|
||||
g.min.format("%02d"), Graphics.TEXT_JUSTIFY_LEFT,
|
||||
dim(mins[mins.size() / 2], 0.38));
|
||||
dc.setColor(dim(Themes.COLON[ti] as Number, 0.38), Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillRectangle(Layout.px(Layout.COLON_X + ox), Layout.px(Layout.COLON_Y1 + oy),
|
||||
Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H));
|
||||
dc.fillRectangle(Layout.px(Layout.COLON_X + ox), Layout.px(Layout.COLON_Y2 + oy),
|
||||
Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H));
|
||||
|
||||
var band = dim(Themes.ACCENT[ti] as Number, 0.38);
|
||||
Layout.text(dc, Layout.DOW_CX + ox, Layout.BAND_CY + oy, Layout.fontBand,
|
||||
Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, band);
|
||||
Layout.text(dc, Layout.MD_CX + ox, Layout.BAND_CY + oy, Layout.fontBand,
|
||||
Fields.monthDay(g), Graphics.TEXT_JUSTIFY_CENTER, band);
|
||||
}
|
||||
|
||||
// ------------------------------- 秒针刷新 ------------------------------
|
||||
// 表盘正常只有每分钟一次的 onUpdate,秒数根本走不动。
|
||||
// 这里实现 onPartialUpdate(系统每秒回调一次),但要非常克制:
|
||||
// - 只有当真的有数据位选了「秒」时才做事,否则直接返回
|
||||
// - 只重绘那一格,用 setClip 把绘制限制在格子的包围盒内
|
||||
// (每秒重绘整个表盘的功耗预算是不够的)
|
||||
// - 常亮的 AMOLED 机型直接跳过:那时数据位本来就不显示
|
||||
function isSeconds(id as Number) as Boolean {
|
||||
return id == 850 || id == 869;
|
||||
}
|
||||
|
||||
function wantsSeconds() as Boolean {
|
||||
if (Settings.showTop()
|
||||
&& (isSeconds(Settings.leftTop()) || isSeconds(Settings.rightTop()))) {
|
||||
return true;
|
||||
}
|
||||
return Settings.showBottom()
|
||||
&& (isSeconds(Settings.bottomLeft()) || isSeconds(Settings.bottomRight()));
|
||||
}
|
||||
|
||||
// 重绘单个格子:先把它的包围盒涂黑,再把新值画回去。
|
||||
function repaintSlot(dc as Dc, ti as Number, ctx as Fields.Ctx, id as Number,
|
||||
x as Numeric, cyDesign as Numeric, just as Number,
|
||||
maxW as Float, col as Number) as Void {
|
||||
var half = maxW / 2.0;
|
||||
var x0 = x - half;
|
||||
if (just == Graphics.TEXT_JUSTIFY_RIGHT) { x0 = x - maxW; }
|
||||
else if (just == Graphics.TEXT_JUSTIFY_LEFT) { x0 = x; }
|
||||
var h = Layout.CAP_COMP * 1.6;
|
||||
dc.setClip(Layout.px(x0), Layout.px(cyDesign - h / 2),
|
||||
Layout.px(maxW), Layout.px(h));
|
||||
// 全屏涂黑。设计稿的底色就是纯黑,AMOLED 上黑像素也最省电。
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
|
||||
dc.clear();
|
||||
Layout.fittedText(dc, x, cyDesign, Layout.fontComp, Fields.value(id, ctx),
|
||||
just, col, maxW);
|
||||
dc.clearClip();
|
||||
}
|
||||
|
||||
function onPartialUpdate(dc as Dc) as Void {
|
||||
if (mBurnIn || !Layout.ready() || !Fields.ready()) { return; }
|
||||
if (!wantsSeconds()) { return; }
|
||||
var ti = Settings.themeIndex(Themes.NAMES.size());
|
||||
var ctx = new Fields.Ctx(System.getDeviceSettings(), Settings.tempUnit());
|
||||
var col = Themes.TEXTPRIMARY[ti] as Number;
|
||||
if (Settings.showTop()) {
|
||||
if (isSeconds(Settings.leftTop())) {
|
||||
repaintSlot(dc, ti, ctx, Settings.leftTop(), Layout.LEFT_X,
|
||||
Layout.VALUE_CY, Graphics.TEXT_JUSTIFY_CENTER,
|
||||
Layout.TOP_MAX_W, col);
|
||||
}
|
||||
if (isSeconds(Settings.rightTop())) {
|
||||
repaintSlot(dc, ti, ctx, Settings.rightTop(), Layout.RIGHT_X,
|
||||
Layout.VALUE_CY, Graphics.TEXT_JUSTIFY_CENTER,
|
||||
Layout.TOP_MAX_W, col);
|
||||
}
|
||||
}
|
||||
if (Settings.showBottom()) {
|
||||
if (isSeconds(Settings.bottomLeft())) {
|
||||
repaintSlot(dc, ti, ctx, Settings.bottomLeft(), Layout.DIST_R,
|
||||
Layout.BOT_VAL_CY, Graphics.TEXT_JUSTIFY_RIGHT,
|
||||
Layout.BOT_MAX_W, col);
|
||||
}
|
||||
if (isSeconds(Settings.bottomRight())) {
|
||||
repaintSlot(dc, ti, ctx, Settings.bottomRight(), Layout.STEP_L,
|
||||
Layout.BOT_VAL_CY, Graphics.TEXT_JUSTIFY_LEFT,
|
||||
Layout.BOT_MAX_W, col);
|
||||
}
|
||||
} else if (typeIdx == 1) { // flame
|
||||
dc.fillPolygon([[x, (y - r).toNumber()],
|
||||
[(x + r * 0.8).toNumber(), (y + r * 0.6).toNumber()],
|
||||
[(x - r * 0.8).toNumber(), (y + r * 0.6).toNumber()]]);
|
||||
} else if (typeIdx == 2) { // steps
|
||||
dc.drawLine((x - r * 0.5).toNumber(), (y - r * 0.3).toNumber(),
|
||||
(x + r * 0.5).toNumber(), (y - r * 0.3).toNumber());
|
||||
dc.drawLine((x - r * 0.3).toNumber(), y, (x + r * 0.6).toNumber(), y);
|
||||
} else if (typeIdx == 3) { // distance dot
|
||||
dc.fillCircle(x, y, (r * 0.5).toNumber());
|
||||
} else if (typeIdx == 4) { // floors chevron
|
||||
dc.fillPolygon([[x, (y - r).toNumber()],
|
||||
[(x + r * 0.7).toNumber(), (y + r * 0.2).toNumber()],
|
||||
[(x - r * 0.7).toNumber(), (y + r * 0.2).toNumber()]]);
|
||||
} else { // battery
|
||||
dc.drawRectangle((x - r * 0.6).toNumber(), (y - r * 0.4).toNumber(),
|
||||
(r * 1.2).toNumber(), (r * 0.8).toNumber());
|
||||
}
|
||||
}
|
||||
|
||||
function drawTopComplication(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary,
|
||||
typeIdx as Number, isLeft as Boolean, info as ActivityMonitor.Info,
|
||||
weather as Weather.CurrentConditions, stats as System.Stats) as Void {
|
||||
var sideX = isLeft ? (156.0 * s).toNumber() : (343.0 * s).toNumber();
|
||||
var iconY = (90.0 * s).toNumber();
|
||||
var valY = (130.5 * s).toNumber();
|
||||
var valStr = complicationValue(typeIdx, info, weather, stats);
|
||||
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(sideX, valY, Graphics.FONT_MEDIUM, valStr,
|
||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
drawComplicationIcon(dc, sideX, iconY, (13.0 * s).toNumber(), theme["accent"] as Number, typeIdx);
|
||||
}
|
||||
|
||||
function drawBluetooth(dc as Dc, cx as Number, cy as Number, r as Number, col as Number) as Void {
|
||||
dc.setPenWidth(3);
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawLine(cx, (cy - r).toNumber(), cx, (cy + r).toNumber());
|
||||
dc.drawLine(cx, (cy - r).toNumber(), (cx + r * 0.7).toNumber(), (cy - r * 0.3).toNumber());
|
||||
dc.drawLine((cx + r * 0.7).toNumber(), (cy - r * 0.3).toNumber(), cx, cy);
|
||||
dc.drawLine(cx, cy, (cx + r * 0.7).toNumber(), (cy + r * 0.3).toNumber());
|
||||
dc.drawLine((cx + r * 0.7).toNumber(), (cy + r * 0.3).toNumber(), cx, (cy + r).toNumber());
|
||||
}
|
||||
|
||||
function drawDateBand(dc as Dc, w as Number, cx as Number, s as Float, theme as Dictionary,
|
||||
ds as System.DeviceSettings) as Void {
|
||||
var by = (286.5 * s).toNumber();
|
||||
var bh = (44.0 * s).toNumber();
|
||||
dc.setColor(theme["bandFill"] as Number, theme["bandFill"] as Number);
|
||||
dc.fillRectangle(0, by, w, bh);
|
||||
var txtCol = theme["bandText"] as Number;
|
||||
var yc = (308.5 * s).toNumber();
|
||||
drawBluetooth(dc, (36.5 * s).toNumber(), yc, (10.0 * s).toNumber(), txtCol);
|
||||
var gi = Gregorian.info(Time.now(), Time.FORMAT_MEDIUM);
|
||||
var DOW = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
||||
var MON = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
||||
var dow = DOW[gi.day_of_week - 1];
|
||||
var md = MON[gi.month - 1] + " " + gi.day.format("%d");
|
||||
dc.setColor(txtCol, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText((177.5 * s).toNumber(), yc, Graphics.FONT_SMALL, dow,
|
||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
dc.drawText((297.5 * s).toNumber(), yc, Graphics.FONT_SMALL, md,
|
||||
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
var ampm = gi.hour >= 12 ? "PM" : "AM";
|
||||
dc.drawText((465.0 * s).toNumber(), yc, Graphics.FONT_SMALL, ampm,
|
||||
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
}
|
||||
|
||||
function drawTime(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary) as Void {
|
||||
var gi = Gregorian.info(Time.now(), Time.FORMAT_MEDIUM);
|
||||
var hh = gi.hour;
|
||||
if (hh == 0) { hh = 12; } else if (hh > 12) { hh = hh - 12; }
|
||||
var hStr = hh.format("%d");
|
||||
var mStr = gi.min.format("%02d");
|
||||
var ty = (220.0 * s).toNumber();
|
||||
dc.setColor(theme["hours"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText((211.0 * s).toNumber(), ty, Graphics.FONT_NUMBER_HOT, hStr,
|
||||
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
dc.setColor(theme["minutes"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText((272.0 * s).toNumber(), ty, Graphics.FONT_NUMBER_HOT, mStr,
|
||||
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
// colon: two blocks centred on x=250
|
||||
dc.setColor(theme["colon"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
var cw = (24.0 * s).toNumber();
|
||||
var ch = (20.0 * s).toNumber();
|
||||
var cxc = (238.0 * s).toNumber();
|
||||
dc.fillRectangle(cxc, (184.0 * s).toNumber(), cw, ch);
|
||||
dc.fillRectangle(cxc, (235.0 * s).toNumber(), cw, ch);
|
||||
}
|
||||
|
||||
function drawBottom(dc as Dc, cx as Number, s as Float, theme as Dictionary,
|
||||
info as ActivityMonitor.Info) as Void {
|
||||
var valY = (366.0 * s).toNumber();
|
||||
var labY = (410.5 * s).toNumber();
|
||||
var leftX = (234.0 * s).toNumber();
|
||||
var rightX = (266.0 * s).toNumber();
|
||||
var dist = info != null && info.distance != null
|
||||
? (info.distance.toFloat() / 100000.0).format("%.1f") : "0.0";
|
||||
var steps = info != null && info.steps != null ? info.steps.format("%d") : "0";
|
||||
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(leftX, valY, Graphics.FONT_MEDIUM, dist,
|
||||
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(leftX, labY, Graphics.FONT_SMALL, "DST",
|
||||
Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(rightX, valY, Graphics.FONT_MEDIUM, steps,
|
||||
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(rightX, labY, Graphics.FONT_SMALL, "STEP",
|
||||
Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- main
|
||||
// ------------------------------- 主绘制 --------------------------------
|
||||
// 每分钟调用一次(以及唤醒、设置变更时)。
|
||||
function onUpdate(dc as Dc) as Void {
|
||||
var w = dc.getWidth();
|
||||
var h = dc.getHeight();
|
||||
var s = w / 500.0;
|
||||
var cx = w / 2;
|
||||
var cy = h / 2;
|
||||
if (!Layout.ready() || Layout.width != dc.getWidth()
|
||||
|| !Fields.ready() || !Icons.ready()) {
|
||||
onLayout(dc);
|
||||
}
|
||||
if (dc has :setAntiAlias) { dc.setAntiAlias(true); }
|
||||
|
||||
var theme = currentTheme();
|
||||
var info = ActivityMonitor.getInfo();
|
||||
var stats = System.getSystemStats();
|
||||
// 每次绘制只构造一个 Ctx,四个数据位共用 —— 天气、用户档案这些
|
||||
// 开销大的查询因此每帧最多做一次,且只在真有格子要用时才做。
|
||||
var ti = Settings.themeIndex(Themes.NAMES.size());
|
||||
var ds = System.getDeviceSettings();
|
||||
var weather = Weather.getCurrentConditions();
|
||||
var ctx = new Fields.Ctx(ds, Settings.tempUnit());
|
||||
Comp.beginFrame(); // 让第三方 App 列表这一帧只枚举一次
|
||||
var info = ctx.info();
|
||||
var stats = ctx.stats();
|
||||
var level = 0.0f;
|
||||
if (stats != null && stats.battery != null) { level = stats.battery / 100.0; }
|
||||
if (level > 1.0f) { level = 1.0f; }
|
||||
if (level < 0.0f) { level = 0.0f; }
|
||||
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
|
||||
dc.clear();
|
||||
|
||||
var pTR = metricProgress(getSetting("RingTR", 0), info);
|
||||
var pTL = metricProgress(getSetting("RingTL", 1), info);
|
||||
var pBR = metricProgress(getSetting("RingBR", 4), info);
|
||||
var pBL = metricProgress(getSetting("RingBL", 3), info);
|
||||
drawRing(dc, cx, cy, s, theme, pTR, pTL, pBR, pBL);
|
||||
var pTR = ringProgress(Settings.ringTR(), info);
|
||||
var pTL = ringProgress(Settings.ringTL(), info);
|
||||
var pBR = ringProgress(Settings.ringBR(), info);
|
||||
var pBL = ringProgress(Settings.ringBL(), info);
|
||||
|
||||
if (getBool("ShowTop", true)) {
|
||||
drawBattery(dc, cx, cy, s, theme, stats);
|
||||
drawTopComplication(dc, cx, cy, s, theme, getSetting("LeftTop", 1), true, info, weather, stats);
|
||||
drawTopComplication(dc, cx, cy, s, theme, getSetting("RightTop", 0), false, info, weather, stats);
|
||||
// 常亮模式走另一条绘制路径,画完直接返回。
|
||||
if (mSleeping && mBurnIn) {
|
||||
drawAmbient(dc, ti, ds, pTR, pTL, pBR, pBL);
|
||||
return;
|
||||
}
|
||||
|
||||
drawTime(dc, cx, cy, s, theme);
|
||||
drawRing(dc, ti, pTR, pTL, pBR, pBL);
|
||||
|
||||
if (getBool("ShowBand", true)) {
|
||||
drawDateBand(dc, w, cx, s, theme, ds);
|
||||
if (Settings.showTop()) {
|
||||
drawBattery(dc, ti, level);
|
||||
drawTopSlot(dc, ti, Settings.leftTop(), true, ctx, level);
|
||||
drawTopSlot(dc, ti, Settings.rightTop(), false, ctx, level);
|
||||
}
|
||||
|
||||
if (getBool("ShowBottom", true)) {
|
||||
drawBottom(dc, cx, s, theme, info);
|
||||
drawTime(dc, ti, ds);
|
||||
|
||||
if (Settings.showBand()) {
|
||||
drawDateBand(dc, ti, ds);
|
||||
}
|
||||
|
||||
if (Settings.showBottom()) {
|
||||
drawBottom(dc, ti, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
source/FieldTable.mc
Normal file
@@ -0,0 +1,31 @@
|
||||
// ⚠️ 本文件由 tools/gen_fields.py 自动生成,请勿手改。
|
||||
// 要加字段:在生成器的 FIELDS 表里加一行,跑一遍生成器,
|
||||
// 再到 source/Fields.mc 对应分类的函数里补一个分支。
|
||||
//
|
||||
// 字段编号沿用 https://watchface.io/docs/datafields,只收录 Connect IQ
|
||||
// 表盘在本机能算出来的那些。
|
||||
import Toybox.Lang;
|
||||
|
||||
module FieldTable {
|
||||
|
||||
// 全部字段 id,顺序与 FieldLabels 资源串(逗号分隔的标签表)严格一致 ——
|
||||
// Fields.label() 就是靠这个下标去取对应标签的。
|
||||
const IDS = [
|
||||
0,860,880,864,865,851,872,852,855,870,856,853,868,858,859,850,869,873,861,862,863,
|
||||
735,1,14,707,2,603,150,152,154,151,3,4,5,6,9,10,219,220,211,212,213,214,203,204,200,
|
||||
828,832,836,840,618,276,12,635,11,623,8,15,281,742,13,278,277,250,251,257,253,255,
|
||||
254,256,252,615,616,733,712,717,700,731,706,732,710,709,708,711,714,715,718,719,720,
|
||||
721,306,639,300,301,302,305,304,308,307,310,309,312,313,314,315,612,626,627,628,629,
|
||||
630,631,632,633,634,641,619,620,695,696,697,698,699,500,515,503,504,505,507,502,501,
|
||||
506,509,510,511,518,512,513,532,536,537,530,534,522,535,523,538,539,540,541,542,516,
|
||||
517,703,704,705
|
||||
];
|
||||
|
||||
// 由字段 id 反查它在 IDS 里的下标;找不到返回 0(对应「关闭」)。
|
||||
function indexOf(id as Number) as Number {
|
||||
for (var i = 0; i < IDS.size(); i++) {
|
||||
if (IDS[i] == id) { return i; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
933
source/Fields.mc
Normal file
@@ -0,0 +1,933 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Math;
|
||||
import Toybox.System;
|
||||
import Toybox.Time;
|
||||
import Toybox.Time.Gregorian;
|
||||
import Toybox.Application;
|
||||
import Toybox.ActivityMonitor;
|
||||
import Toybox.Activity;
|
||||
import Toybox.UserProfile;
|
||||
import Toybox.SensorHistory;
|
||||
import Toybox.Weather;
|
||||
import Toybox.Position;
|
||||
import Toybox.WatchUi;
|
||||
import FieldTable;
|
||||
import Settings;
|
||||
import Solar;
|
||||
import Owm;
|
||||
import Comp;
|
||||
|
||||
// ============================================================================
|
||||
// 数据字段
|
||||
// ----------------------------------------------------------------------------
|
||||
// 四个数据位能显示的全部内容,共 125 项。
|
||||
//
|
||||
// 字段编号沿用 https://watchface.io/docs/datafields,两边可以直接对照。
|
||||
// 驱动设置项和标签文案的那张表在 tools/gen_fields.py —— 加字段要改那里,
|
||||
// 然后在本文件对应分类的函数里补一个分支。
|
||||
//
|
||||
// 只收录**手表本机能算出来**的字段。没做的几类及原因见 README 第 7 节,简单说:
|
||||
// - 按运动分类的周/月累计:CIQ 只有按天的总计,没有分运动的历史
|
||||
// - StormGlass 潮汐、第三方 App 复杂功能:需要额外 key 或用户装了对应 App
|
||||
// - 日历、计时器:CIQ 不开放日历,表盘也拿不到触摸输入
|
||||
//
|
||||
// 组织方式:value() 按分类依次尝试 7 个函数,每个函数不认识编号就返回 null。
|
||||
// 之所以不按编号区间派发,是因为编号是**交错**的 —— 1/2/3 是活动,8/11/12 是
|
||||
// 身体,14 又回到活动。
|
||||
// ============================================================================
|
||||
module Fields {
|
||||
|
||||
const DASH = "--";
|
||||
|
||||
var mDow as Array<String>?;
|
||||
var mMon as Array<String>?;
|
||||
var mLabels as Array<String>?;
|
||||
var mMeridiem as Array<String>?;
|
||||
var mDateFmt as String = "$1$ $2$";
|
||||
|
||||
function loadStrings() as Void {
|
||||
mDow = splitCsv(WatchUi.loadResource(Rez.Strings.DowNames) as String);
|
||||
mMon = splitCsv(WatchUi.loadResource(Rez.Strings.MonNames) as String);
|
||||
mLabels = splitCsv(WatchUi.loadResource(Rez.Strings.FieldLabels) as String);
|
||||
mMeridiem = splitCsv(WatchUi.loadResource(Rez.Strings.Meridiem) as String);
|
||||
mDateFmt = WatchUi.loadResource(Rez.Strings.DateFormat) as String;
|
||||
}
|
||||
|
||||
function ready() as Boolean { return mDow != null; }
|
||||
|
||||
// Monkey C 没有 String.split,只能自己按逗号切。
|
||||
function splitCsv(str as String) as Array<String> {
|
||||
var out = [] as Array<String>;
|
||||
var rest = str;
|
||||
var cut = rest.find(",");
|
||||
while (cut != null) {
|
||||
out.add(rest.substring(0, cut) as String);
|
||||
rest = rest.substring(cut + 1, rest.length()) as String;
|
||||
cut = rest.find(",");
|
||||
}
|
||||
out.add(rest);
|
||||
return out;
|
||||
}
|
||||
|
||||
function dow(g as Gregorian.Info) as String {
|
||||
return mDow == null ? "" : mDow[g.day_of_week - 1];
|
||||
}
|
||||
|
||||
function month(g as Gregorian.Info) as String {
|
||||
return mMon == null ? "" : mMon[g.month - 1];
|
||||
}
|
||||
|
||||
function monthDay(g as Gregorian.Info) as String {
|
||||
return Lang.format(mDateFmt, [month(g), g.day.format("%d")]);
|
||||
}
|
||||
|
||||
// 按用户填的格式串拼时间/日期。
|
||||
//
|
||||
// 用单字母占位符,因为设置项里能填的长度有限,而且这样中英文都好写:
|
||||
// h 小时(12/24 跟随手表) H 小时补零
|
||||
// m 分钟补零 s 秒补零
|
||||
// a 上午/下午(24 小时制下为空)
|
||||
// D 日 M 月(数字) N 月(名称)
|
||||
// Y 四位年 y 两位年 w 星期
|
||||
// 其它字符原样输出,所以 "D/M" → "10/9","w N D" → "周四 9月 10"。
|
||||
function applyFormat(fmt as String, g as Gregorian.Info, c as Ctx) as String {
|
||||
var out = "";
|
||||
var chars = fmt.toCharArray();
|
||||
for (var i = 0; i < chars.size(); i++) {
|
||||
var ch = chars[i];
|
||||
if (ch == 'h') {
|
||||
var hh = g.hour;
|
||||
if (!c.is24) {
|
||||
if (hh == 0) { hh = 12; } else if (hh > 12) { hh = hh - 12; }
|
||||
}
|
||||
out += hh.format("%d");
|
||||
} else if (ch == 'H') {
|
||||
out += g.hour.format("%02d");
|
||||
} else if (ch == 'm') {
|
||||
out += g.min.format("%02d");
|
||||
} else if (ch == 's') {
|
||||
out += g.sec.format("%02d");
|
||||
} else if (ch == 'a') {
|
||||
out += c.is24 ? "" : meridiem(g);
|
||||
} else if (ch == 'D') {
|
||||
out += g.day.format("%d");
|
||||
} else if (ch == 'M') {
|
||||
out += g.month.format("%d");
|
||||
} else if (ch == 'N') {
|
||||
out += month(g);
|
||||
} else if (ch == 'Y') {
|
||||
out += g.year.format("%d");
|
||||
} else if (ch == 'y') {
|
||||
out += (g.year % 100).format("%02d");
|
||||
} else if (ch == 'w') {
|
||||
out += dow(g);
|
||||
} else {
|
||||
out += ch.toString();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 距目标日期还有几天。目标日期是设置里填的 YYYY-MM-DD 字符串。
|
||||
// 解析失败(没填/格式不对)返回 null,字段显示 "--"。
|
||||
function daysUntil(target as String, g as Gregorian.Info) as Number? {
|
||||
if (target.length() < 8) { return null; }
|
||||
var parts = splitOn(target, '-');
|
||||
if (parts.size() < 3) { return null; }
|
||||
var y = parts[0].toNumber();
|
||||
var mo = parts[1].toNumber();
|
||||
var d = parts[2].toNumber();
|
||||
if (y == null || mo == null || d == null) { return null; }
|
||||
// 都换算成「儒略日式」的绝对天数再相减,省得处理跨月跨年。
|
||||
return absDay(y, mo, d) - absDay(g.year, g.month, g.day);
|
||||
}
|
||||
|
||||
function absDay(y as Number, m as Number, d as Number) as Number {
|
||||
var yy = y;
|
||||
var mm = m;
|
||||
if (mm <= 2) { yy = yy - 1; mm = mm + 12; }
|
||||
var a = yy / 100;
|
||||
var b = 2 - a + a / 4;
|
||||
return (365.25 * (yy + 4716)).toNumber() + (30.6001 * (mm + 1)).toNumber()
|
||||
+ d + b - 1524;
|
||||
}
|
||||
|
||||
function splitOn(str as String, sep as Char) as Array<String> {
|
||||
var out = [] as Array<String>;
|
||||
var cur = "";
|
||||
var chars = str.toCharArray();
|
||||
for (var i = 0; i < chars.size(); i++) {
|
||||
if (chars[i] == sep) { out.add(cur); cur = ""; }
|
||||
else { cur += chars[i].toString(); }
|
||||
}
|
||||
out.add(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 英文出 "10",中文出 "10日"。
|
||||
// 后缀带在本地化的日期格式串里,所以这里把月份传空串即可。
|
||||
function dayOnly(g as Gregorian.Info) as String {
|
||||
return Lang.format(mDateFmt, ["", g.day.format("%d")]);
|
||||
}
|
||||
|
||||
function meridiem(g as Gregorian.Info) as String {
|
||||
return mMeridiem == null ? "" : mMeridiem[g.hour >= 12 ? 1 : 0];
|
||||
}
|
||||
|
||||
// 底部格子数值下方的小标签。按字段编号查 FieldTable 的下标,
|
||||
// 再到本地化的标签表里取词,所以中英文会自动跟随系统语言。
|
||||
function label(id as Number) as String {
|
||||
if (mLabels == null) { return ""; }
|
||||
var i = FieldTable.indexOf(id);
|
||||
return i < mLabels.size() ? mLabels[i] : "";
|
||||
}
|
||||
|
||||
// 每次绘制构造一个,传给所有数据位共用。
|
||||
// 目的是让开销大的查询(天气预报、传感器历史、用户档案)**每次绘制最多做
|
||||
// 一次,且只在真的有格子要用时才做** —— 四个格子可能都要天气,但只查一次;
|
||||
// 没有格子用到 UserProfile 时就完全不去碰它。
|
||||
class Ctx {
|
||||
var greg as Gregorian.Info;
|
||||
var is24 as Boolean = true;
|
||||
var metric as Boolean = true;
|
||||
var fahrenheit as Boolean = false;
|
||||
|
||||
private var mInfo as ActivityMonitor.Info?;
|
||||
private var mStats as System.Stats?;
|
||||
private var mDs as System.DeviceSettings?;
|
||||
private var mAct as Activity.Info?;
|
||||
private var mWx as Weather.CurrentConditions?;
|
||||
private var mFc as Array<Weather.DailyForecast>?;
|
||||
private var mProf as UserProfile.Profile?;
|
||||
private var mGotAct = false;
|
||||
private var mGotWx = false;
|
||||
private var mGotFc = false;
|
||||
private var mGotProf = false;
|
||||
|
||||
function initialize(ds as System.DeviceSettings?, tempUnit as Number) {
|
||||
mDs = ds;
|
||||
greg = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
|
||||
if (ds != null) {
|
||||
is24 = ds.is24Hour;
|
||||
metric = (ds.distanceUnits != System.UNIT_STATUTE);
|
||||
fahrenheit = (tempUnit == 2)
|
||||
|| (tempUnit == 0 && ds.temperatureUnits == System.UNIT_STATUTE);
|
||||
}
|
||||
if (tempUnit == 1) { fahrenheit = false; }
|
||||
}
|
||||
|
||||
function ds() as System.DeviceSettings? { return mDs; }
|
||||
|
||||
function info() as ActivityMonitor.Info? {
|
||||
if (mInfo == null) { mInfo = ActivityMonitor.getInfo(); }
|
||||
return mInfo;
|
||||
}
|
||||
|
||||
function stats() as System.Stats? {
|
||||
if (mStats == null) { mStats = System.getSystemStats(); }
|
||||
return mStats;
|
||||
}
|
||||
|
||||
function act() as Activity.Info? {
|
||||
if (!mGotAct) { mAct = Activity.getActivityInfo(); mGotAct = true; }
|
||||
return mAct;
|
||||
}
|
||||
|
||||
function wx() as Weather.CurrentConditions? {
|
||||
if (!mGotWx) { mWx = Weather.getCurrentConditions(); mGotWx = true; }
|
||||
return mWx;
|
||||
}
|
||||
|
||||
function forecast() as Weather.DailyForecast? {
|
||||
if (!mGotFc) { mFc = Weather.getDailyForecast(); mGotFc = true; }
|
||||
if (mFc != null && mFc.size() > 0) { return mFc[0]; }
|
||||
return null;
|
||||
}
|
||||
|
||||
// 本项目已经被这个 API 家族坑过三次(Properties.getValue、
|
||||
// deleteTemporalEvent、Complication.unit),一律按「声明了权限也可能抛」
|
||||
// 来防。取不到就当作没有用户档案,相关字段显示 "--"。
|
||||
function profile() as UserProfile.Profile? {
|
||||
if (!mGotProf) {
|
||||
try {
|
||||
mProf = UserProfile.getProfile();
|
||||
} catch (e) {
|
||||
mProf = null;
|
||||
}
|
||||
mGotProf = true;
|
||||
}
|
||||
return mProf;
|
||||
}
|
||||
|
||||
// 日出/日落/晨昏蒙影都要位置,而两个实时来源都会间歇性为空:
|
||||
// - 天气在同步之前没有观测点坐标
|
||||
// - currentLocation 要有东西刚定过位才有值
|
||||
// 所以每次拿到位置就缓存进 Storage,两个来源都空时用缓存兜底,
|
||||
// 免得这些字段一会儿有一会儿变 "--"。
|
||||
function where() as Position.Location? {
|
||||
var w = wx();
|
||||
if (w != null && (w has :observationLocationPosition)
|
||||
&& w.observationLocationPosition != null) {
|
||||
return remember(w.observationLocationPosition);
|
||||
}
|
||||
var a = act();
|
||||
if (a != null && a.currentLocation != null) {
|
||||
return remember(a.currentLocation);
|
||||
}
|
||||
var lat = Application.Storage.getValue("lastLat");
|
||||
var lon = Application.Storage.getValue("lastLon");
|
||||
if (lat != null && lon != null) {
|
||||
return new Position.Location({:latitude => lat as Double,
|
||||
:longitude => lon as Double,
|
||||
:format => :degrees});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function remember(loc as Position.Location) as Position.Location {
|
||||
var d = loc.toDegrees();
|
||||
Application.Storage.setValue("lastLat", d[0]);
|
||||
Application.Storage.setValue("lastLon", d[1]);
|
||||
return loc;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------- 格式化工具 ----------------------------
|
||||
function num(v as Numeric?) as String {
|
||||
return v == null ? DASH : v.format("%d");
|
||||
}
|
||||
|
||||
function one(v as Float?) as String {
|
||||
return v == null ? DASH : v.format("%.1f");
|
||||
}
|
||||
|
||||
function clock(m as Time.Moment?, c as Ctx) as String {
|
||||
if (m == null) { return DASH; }
|
||||
return clockInfo(Gregorian.info(m, Time.FORMAT_SHORT), c);
|
||||
}
|
||||
|
||||
function clockInfo(g as Gregorian.Info, c as Ctx) as String {
|
||||
var h = g.hour;
|
||||
if (!c.is24) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
return h.format("%d") + ":" + g.min.format("%02d");
|
||||
}
|
||||
|
||||
function offsetClock(minutes as Number, c as Ctx) as String {
|
||||
var m = Time.now().add(new Time.Duration(minutes * 60));
|
||||
return clock(m, c);
|
||||
}
|
||||
|
||||
function temp(celsius as Numeric?, c as Ctx) as String {
|
||||
if (celsius == null) { return DASH; }
|
||||
var t = celsius.toFloat();
|
||||
if (c.fahrenheit) { t = t * 9.0 / 5.0 + 32.0; }
|
||||
return t.format("%d") + "°";
|
||||
}
|
||||
|
||||
// ActivityMonitor 的距离单位是**厘米**,先换算成公里再按单位制转换。
|
||||
function dist(cm as Numeric?, c as Ctx) as String {
|
||||
if (cm == null) { return DASH; }
|
||||
var km = cm.toFloat() / 100000.0;
|
||||
return (c.metric ? km : km * 0.621371).format("%.1f");
|
||||
}
|
||||
|
||||
function metres(m as Numeric?, c as Ctx) as String {
|
||||
if (m == null) { return DASH; }
|
||||
var v = m.toFloat();
|
||||
return (c.metric ? v : v * 3.28084).format("%d");
|
||||
}
|
||||
|
||||
function rounded(v as Numeric?) as String {
|
||||
if (v == null) { return DASH; }
|
||||
var n = v.toNumber();
|
||||
if (n < 1000) { return n.format("%d"); }
|
||||
return (n / 1000.0).format("%.1f") + "k";
|
||||
}
|
||||
|
||||
// 从 SensorHistory 迭代器里取最新一条读数;没有则 null。
|
||||
function latest(iter) as Numeric? {
|
||||
if (iter == null) { return null; }
|
||||
var s = iter.next();
|
||||
if (s == null) { return null; }
|
||||
return s.data;
|
||||
}
|
||||
|
||||
// 历史累计。
|
||||
// ActivityMonitor.getHistory() 返回过去若干天(最新在前),**不含今天**。
|
||||
// 把它加起来是本机唯一能做出「本周累计 / 近 7 天累计」的路子;
|
||||
// 按运动项目细分的那些仍然做不到,那需要手机侧与 Garmin Connect 的集成。
|
||||
//
|
||||
// which:0 步数 / 1 卡路里 / 2 距离(厘米) / 3 楼层 / 4 活动分钟
|
||||
// days :往前折算几天,今天总是算在内
|
||||
function sumHistory(c as Ctx, which as Number, days as Number) as Numeric? {
|
||||
var today = todayValue(c, which);
|
||||
if (today == null) { return null; }
|
||||
var total = today;
|
||||
if (days <= 0) { return total; }
|
||||
var hist = ActivityMonitor.getHistory();
|
||||
if (hist == null) { return total; }
|
||||
var n = days < hist.size() ? days : hist.size();
|
||||
for (var k = 0; k < n; k++) {
|
||||
var v = sampleValue(hist[k], which);
|
||||
if (v != null) { total = total + v; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function todayValue(c as Ctx, which as Number) as Numeric? {
|
||||
var i = c.info();
|
||||
if (i == null) { return null; }
|
||||
return sampleValue(i, which);
|
||||
}
|
||||
|
||||
function sampleValue(d, which as Number) as Numeric? {
|
||||
if (which == 0) { return (d has :steps) ? d.steps : null; }
|
||||
if (which == 1) { return (d has :calories) ? d.calories : null; }
|
||||
if (which == 2) { return (d has :distance) ? d.distance : null; }
|
||||
if (which == 3) { return (d has :floorsClimbed) ? d.floorsClimbed : null; }
|
||||
if (which == 4) {
|
||||
if (!(d has :activeMinutesDay) || d.activeMinutesDay == null) { return null; }
|
||||
return d.activeMinutesDay.total;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 距本周一过了几天,本周累计据此决定往前取多少天。
|
||||
// day_of_week 是 1=周日,所以周日要特判成 6。
|
||||
function daysIntoWeek(g as Gregorian.Info) as Number {
|
||||
var dow = g.day_of_week; // 1 = Sunday
|
||||
return dow == 1 ? 6 : dow - 2;
|
||||
}
|
||||
|
||||
function dayOfYear(g as Gregorian.Info) as Number {
|
||||
var cum = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
var d = cum[g.month - 1] + g.day;
|
||||
var y = g.year;
|
||||
var leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
||||
if (leap && g.month > 2) { d = d + 1; }
|
||||
return d;
|
||||
}
|
||||
|
||||
// ISO 8601 周数:周一为一周之始,含当年第一个周四的那周为第 1 周。
|
||||
function isoWeek(g as Gregorian.Info) as Number {
|
||||
var dow = g.day_of_week - 1; // 0 = Sunday
|
||||
var iso = dow == 0 ? 7 : dow; // 1 = Monday .. 7 = Sunday
|
||||
var w = (dayOfYear(g) - iso + 10) / 7;
|
||||
if (w < 1) { return 52; }
|
||||
if (w > 52) { return 1; }
|
||||
return w;
|
||||
}
|
||||
|
||||
// 月龄:距 2000-01-06 那次新月的天数,对朔望月(29.530588853 天)取余。
|
||||
function moonAge(g as Gregorian.Info) as Float {
|
||||
var y = g.year;
|
||||
var m = g.month;
|
||||
if (m <= 2) { y = y - 1; m = m + 12; }
|
||||
var a = y / 100;
|
||||
var b = 2 - a + a / 4;
|
||||
var jd = (365.25 * (y + 4716)).toNumber() + (30.6001 * (m + 1)).toNumber()
|
||||
+ g.day + b - 1524.5 + g.hour / 24.0;
|
||||
// ⚠️ Monkey C 的 % 不支持浮点,只能手写取余。
|
||||
var syn = 29.530588853;
|
||||
var x = jd - 2451550.1;
|
||||
var age = x - (x / syn).toNumber() * syn;
|
||||
if (age < 0) { age = age + syn; }
|
||||
return age.toFloat();
|
||||
}
|
||||
|
||||
function windDir(bearingDeg as Numeric?) as String {
|
||||
if (bearingDeg == null) { return DASH; }
|
||||
var pts = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
|
||||
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
|
||||
var b = bearingDeg.toFloat();
|
||||
while (b < 0) { b = b + 360.0; }
|
||||
var i = ((b + 11.25) / 22.5).toNumber() % 16;
|
||||
return pts[i];
|
||||
}
|
||||
|
||||
// ------------------------------- 分类取值 ------------------------------
|
||||
// ---- 日期时间类 ----
|
||||
// 不属于本类的编号返回 null,交给 value() 试下一个分类。
|
||||
function dateTime(id as Number, c as Ctx) as String? {
|
||||
if (id == 860) { return clockInfo(c.greg, c); } // 时间
|
||||
if (id == 880) { // UTC 时间
|
||||
var utc = Gregorian.utcInfo(Time.now(), Time.FORMAT_SHORT);
|
||||
return clockInfo(utc, c);
|
||||
}
|
||||
// 第二时区 1
|
||||
if (id == 864) { return offsetClock(Settings.number("AltOffset1", 0), c); }
|
||||
// 第二时区 2
|
||||
if (id == 865) { return offsetClock(Settings.number("AltOffset2", 0), c); }
|
||||
if (id == 855) { return c.greg.day.format("%d"); } // 日
|
||||
if (id == 868) { return c.greg.month.format("%d"); } // 月份数字
|
||||
if (id == 859) { return isoWeek(c.greg).format("%d"); } // 周数 (ISO)
|
||||
// 周数
|
||||
if (id == 873) { return ((dayOfYear(c.greg) + 6) / 7).format("%d"); }
|
||||
// 秒数要动起来依赖设备支持 onPartialUpdate;
|
||||
// view 只在有格子选了秒时才去请求那个回调。
|
||||
if (id == 850) { return c.greg.sec.format("%d"); } // 秒
|
||||
if (id == 869) { return c.greg.sec.format("%02d"); } // 秒 (前导零)
|
||||
// 自定义格式:格式串来自设置,占位符说明见 applyFormat
|
||||
if (id == 861) { return applyFormat(Settings.text("TimeFmt"), c.greg, c); }
|
||||
if (id == 862) { return applyFormat(Settings.text("DateFmt1"), c.greg, c); }
|
||||
if (id == 863) { return applyFormat(Settings.text("DateFmt2"), c.greg, c); }
|
||||
if (id == 735) { // 事件倒计时
|
||||
var left = daysUntil(Settings.text("CountdownDate"), c.greg);
|
||||
if (left == null) { return DASH; }
|
||||
return left.format("%d");
|
||||
} // 秒 (前导零)
|
||||
if (id == 870) { return dow(c.greg); } // 星期简写
|
||||
if (id == 856) { return dow(c.greg); } // 星期全称
|
||||
if (id == 853) { return month(c.greg); } // 月份
|
||||
if (id == 852) { return monthDay(c.greg); } // 月 日
|
||||
if (id == 872) { return dow(c.greg) + " " + dayOnly(c.greg); } // 星期 日
|
||||
if (id == 851) { return dow(c.greg) + " " + monthDay(c.greg); } // 星期 月 日
|
||||
if (id == 858) { return meridiem(c.greg); } // 上午/下午
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- 活动类 ----
|
||||
// 步数、卡路里、距离、楼层、活动分钟,以及它们的本周/近 7 天累计。
|
||||
function activity(id as Number, c as Ctx) as String? {
|
||||
var i = c.info();
|
||||
if (id == 1) { return i == null ? DASH : num(i.steps); } // 步数
|
||||
if (id == 14) { return i == null ? DASH : rounded(i.steps); } // 步数 (取整)
|
||||
if (id == 707) { // 剩余步数
|
||||
if (i == null || i.steps == null || i.stepGoal == null) { return DASH; }
|
||||
var left = i.stepGoal - i.steps;
|
||||
return left > 0 ? left.format("%d") : "0";
|
||||
}
|
||||
if (id == 2) { return i == null ? DASH : num(i.calories); } // 卡路里
|
||||
if (id == 603) { // 活动卡路里
|
||||
if (i == null || i.calories == null) { return DASH; }
|
||||
var p = c.profile();
|
||||
if (p == null || p.weight == null || p.height == null) { return num(i.calories); }
|
||||
return num(i.calories); // device reports total only
|
||||
}
|
||||
if (id == 150 || id == 152 || id == 154) { // 活动分钟 / 中强度分钟 / 高强度分钟
|
||||
if (i == null || !(i has :activeMinutesDay) || i.activeMinutesDay == null) {
|
||||
return DASH;
|
||||
}
|
||||
var am = i.activeMinutesDay;
|
||||
if (id == 152) { return num(am.moderate); } // 中强度分钟
|
||||
if (id == 154) { return num(am.vigorous); } // 高强度分钟
|
||||
return num(am.total);
|
||||
}
|
||||
if (id == 151) { // 本周活动分钟
|
||||
if (i == null || !(i has :activeMinutesWeek) || i.activeMinutesWeek == null) {
|
||||
return DASH;
|
||||
}
|
||||
return num(i.activeMinutesWeek.total);
|
||||
}
|
||||
// 距离
|
||||
if (id == 3) { return i == null ? DASH : dist(i.distance, c); }
|
||||
// 上楼层数
|
||||
if (id == 4) { return i == null ? DASH : num(i.floorsClimbed); }
|
||||
// ⚠️ ActivityMonitor.Info 上有些成员**在所有设备上都能编译通过,但只有
|
||||
// 部分设备/固件运行时才真的有**。轮椅计数器就是典型:在 fenix 8 上直接抛
|
||||
// "Symbol Not Found",用户一选这个字段表盘就崩。
|
||||
// 所以凡是非通用成员,用之前一律先 has 检查。
|
||||
if (id == 5) { // 下楼层数
|
||||
return (i == null || !(i has :floorsDescended)) ? DASH
|
||||
: num(i.floorsDescended);
|
||||
}
|
||||
if (id == 6) { // 爬升高度
|
||||
return (i == null || !(i has :metersClimbed)) ? DASH
|
||||
: metres(i.metersClimbed, c);
|
||||
}
|
||||
if (id == 9) { // 推行距离
|
||||
return (i == null || !(i has :pushDistance)) ? DASH
|
||||
: dist(i.pushDistance, c);
|
||||
}
|
||||
if (id == 10) { // 推行次数
|
||||
return (i == null || !(i has :pushes)) ? DASH : num(i.pushes);
|
||||
}
|
||||
if (id == 219 || id == 211 || id == 213 || id == 203) { // 本周步数 / 本周卡路里 / 本周距离 / 本周楼层
|
||||
var which = (id == 219) ? 0 : ((id == 211) ? 1 : ((id == 213) ? 2 : 3));
|
||||
var back = daysIntoWeek(c.greg);
|
||||
var v = sumHistory(c, which, back);
|
||||
if (v == null) { return DASH; }
|
||||
return (id == 213) ? dist(v, c) : rounded(v);
|
||||
}
|
||||
// 近 7 天步数 / 近 7 天卡路里 / 近 7 天距离 / 近 7 天楼层 / 近 7 天活动分钟
|
||||
if (id == 828 || id == 832 || id == 836 || id == 840) {
|
||||
// 近 28 天。getHistory 能返回多少天由固件决定,sumHistory 内部会按
|
||||
// 实际条数截断,所以拿不到 28 天时是「有多少算多少」而不是报错。
|
||||
var w28 = (id == 828) ? 0 : ((id == 832) ? 1 : ((id == 836) ? 2 : 3));
|
||||
var v28 = sumHistory(c, w28, 27);
|
||||
if (v28 == null) { return DASH; }
|
||||
return (id == 836) ? dist(v28, c) : rounded(v28);
|
||||
}
|
||||
if (id == 220 || id == 212 || id == 214 || id == 204 || id == 200) {
|
||||
var which = (id == 220) ? 0 : ((id == 212) ? 1
|
||||
: ((id == 214) ? 2 : ((id == 204) ? 3 : 4)));
|
||||
var v = sumHistory(c, which, 6);
|
||||
if (v == null) { return DASH; }
|
||||
return (id == 214) ? dist(v, c) : rounded(v);
|
||||
}
|
||||
if (id == 8 || id == 15) { // 活动条 / 活动条 (反向)
|
||||
if (i == null || i.moveBarLevel == null) { return DASH; }
|
||||
var lvl = i.moveBarLevel;
|
||||
// 活动条 (反向)
|
||||
if (id == 15) { lvl = ActivityMonitor.MOVE_BAR_LEVEL_MAX - lvl; }
|
||||
return lvl.format("%d");
|
||||
}
|
||||
if (id == 13) { // 恢复时间
|
||||
return (i == null || !(i has :timeToRecovery)) ? DASH
|
||||
: num(i.timeToRecovery);
|
||||
}
|
||||
if (id == 11) { // 呼吸频率
|
||||
return (i == null || !(i has :respirationRate)) ? DASH
|
||||
: num(i.respirationRate);
|
||||
}
|
||||
if (id == 12) { // 压力指数
|
||||
return (i == null || !(i has :stressScore)) ? DASH : num(i.stressScore);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- 身体类 ----
|
||||
// 心率、压力、血氧、身体电量,以及从用户档案推出来的体重/BMI/VO2max。
|
||||
function body(id as Number, c as Ctx) as String? {
|
||||
if (id == 618) { // 心率
|
||||
var a = c.act();
|
||||
if (a != null && a.currentHeartRate != null) { return num(a.currentHeartRate); }
|
||||
var h = ActivityMonitor.getHeartRateHistory(1, true);
|
||||
if (h != null) {
|
||||
var s = h.next();
|
||||
if (s != null && s.heartRate != null
|
||||
&& s.heartRate != ActivityMonitor.INVALID_HR_SAMPLE) {
|
||||
return num(s.heartRate);
|
||||
}
|
||||
}
|
||||
return DASH;
|
||||
}
|
||||
if (id == 276) { // 静息心率
|
||||
var p = c.profile();
|
||||
return (p == null || !(p has :restingHeartRate)) ? DASH
|
||||
: num(p.restingHeartRate);
|
||||
}
|
||||
if (id == 635) { // 血氧
|
||||
if (!(SensorHistory has :getOxygenSaturationHistory)) { return DASH; }
|
||||
return num(latest(SensorHistory.getOxygenSaturationHistory({:period => 1})));
|
||||
}
|
||||
if (id == 623) { // 身体电量
|
||||
if (!(SensorHistory has :getBodyBatteryHistory)) { return DASH; }
|
||||
return num(latest(SensorHistory.getBodyBatteryHistory({:period => 1})));
|
||||
}
|
||||
if (id == 281 || id == 742) { // 体重 / BMI
|
||||
var p = c.profile();
|
||||
if (p == null || !(p has :weight) || p.weight == null) { return DASH; }
|
||||
var kg = p.weight.toFloat() / 1000.0;
|
||||
if (id == 281) { // 体重
|
||||
return (c.metric ? kg : kg * 2.20462).format("%.1f");
|
||||
}
|
||||
if (!(p has :height) || p.height == null || p.height <= 0) { return DASH; }
|
||||
var m = p.height.toFloat() / 100.0;
|
||||
return (kg / (m * m)).format("%.1f");
|
||||
}
|
||||
if (id == 277 || id == 278) { // 最大摄氧量 (骑) / 最大摄氧量 (跑)
|
||||
var p = c.profile();
|
||||
if (p == null) { return DASH; }
|
||||
if (id == 278) { // 最大摄氧量 (跑)
|
||||
return (p has :vo2maxRunning) ? num(p.vo2maxRunning) : DASH;
|
||||
}
|
||||
return (p has :vo2maxCycling) ? num(p.vo2maxCycling) : DASH;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- 系统类 ----
|
||||
// 电量、闹钟、通知、勿扰、蓝牙、太阳能强度。
|
||||
function system(id as Number, c as Ctx) as String? {
|
||||
var st = c.stats();
|
||||
if (id == 250) { // 电量
|
||||
return st == null || st.battery == null ? DASH
|
||||
: st.battery.format("%d") + "%";
|
||||
}
|
||||
if (id == 251) { // 剩余天数
|
||||
if (st == null || !(st has :batteryInDays) || st.batteryInDays == null) {
|
||||
return DASH;
|
||||
}
|
||||
return st.batteryInDays.format("%d") + "d";
|
||||
}
|
||||
if (id == 257) { // 电量 / 天数
|
||||
var pct = (st == null || st.battery == null) ? DASH : st.battery.format("%d");
|
||||
var d = (st == null || !(st has :batteryInDays) || st.batteryInDays == null)
|
||||
? DASH : st.batteryInDays.format("%d");
|
||||
return pct + "/" + d;
|
||||
}
|
||||
if (id == 252) { // 太阳能强度
|
||||
if (st == null || !(st has :solarIntensity) || st.solarIntensity == null) {
|
||||
return DASH;
|
||||
}
|
||||
return st.solarIntensity.format("%d") + "%";
|
||||
}
|
||||
var ds = c.ds();
|
||||
// 闹钟
|
||||
if (id == 253) { return ds == null ? DASH : num(ds.alarmCount); }
|
||||
// 通知
|
||||
if (id == 255) { return ds == null ? DASH : num(ds.notificationCount); }
|
||||
if (id == 254) { // 勿扰模式
|
||||
return ds == null ? DASH : (ds.doNotDisturb ? "ON" : "OFF");
|
||||
}
|
||||
if (id == 256) { // 蓝牙
|
||||
return ds == null ? DASH : (ds.phoneConnected ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- 环境类 ----
|
||||
// 海拔、气压、各类太阳事件(含晨昏蒙影与黄金/蓝调时刻)、月相。
|
||||
function environment(id as Number, c as Ctx) as String? {
|
||||
if (id == 615) { // 海拔
|
||||
var a = c.act();
|
||||
return (a == null || !(a has :altitude)) ? DASH : metres(a.altitude, c);
|
||||
}
|
||||
if (id == 616 || id == 733) { // 海平面气压 / 环境气压
|
||||
var a = c.act();
|
||||
if (a == null) { return DASH; }
|
||||
var pa = null;
|
||||
if (id == 616 && (a has :meanSeaLevelPressure)) { // 海平面气压
|
||||
pa = a.meanSeaLevelPressure;
|
||||
} else if (id == 733 && (a has :ambientPressure)) { // 环境气压
|
||||
pa = a.ambientPressure;
|
||||
}
|
||||
if (pa == null) { return DASH; }
|
||||
return (pa.toFloat() / 100.0).format("%d");
|
||||
}
|
||||
if (id == 712 || id == 717 || id == 700 || id == 731) { // 日出 / 日落 / 下一太阳事件 / 距太阳事件
|
||||
var loc = c.where();
|
||||
if (loc == null) { return DASH; }
|
||||
var now = Time.now();
|
||||
var rise = Weather.getSunrise(loc, now);
|
||||
var set = Weather.getSunset(loc, now);
|
||||
if (id == 712) { return clock(rise, c); } // 日出
|
||||
if (id == 717) { return clock(set, c); } // 日落
|
||||
if (rise == null || set == null) { return DASH; }
|
||||
var next = (now.compare(rise) < 0) ? rise
|
||||
: ((now.compare(set) < 0) ? set : rise);
|
||||
if (id == 700) { return clock(next, c); } // 下一太阳事件
|
||||
var secs = next.compare(now);
|
||||
if (secs < 0) { secs = secs + 86400; }
|
||||
return (secs / 3600).format("%d") + "h" + ((secs % 3600) / 60).format("%02d");
|
||||
}
|
||||
if (Solar.owns(id)) {
|
||||
return clock(Solar.byId(id, c.where()), c);
|
||||
}
|
||||
if (id == 706) { return moonAge(c.greg).format("%.1f"); } // 月龄
|
||||
if (id == 732) { // 月球照明
|
||||
var age = moonAge(c.greg);
|
||||
var lit = (1.0 - Math.cos(2.0 * Math.PI * age / 29.530588853)) / 2.0;
|
||||
return (lit * 100.0).format("%d") + "%";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- Garmin 原生天气 ----
|
||||
// 手表同步下来的当前天气与今日预报。与 OWM 那套完全独立。
|
||||
function weather(id as Number, c as Ctx) as String? {
|
||||
var w = c.wx();
|
||||
// 气温
|
||||
if (id == 306) { return w == null ? DASH : temp(w.temperature, c); }
|
||||
if (id == 300) { // 体感温度
|
||||
return (w == null || !(w has :feelsLikeTemperature)) ? DASH
|
||||
: temp(w.feelsLikeTemperature, c);
|
||||
}
|
||||
if (id == 305) { // 湿度
|
||||
return (w == null || !(w has :relativeHumidity)) ? DASH
|
||||
: num(w.relativeHumidity) + "%";
|
||||
}
|
||||
if (id == 304) { // 降水概率
|
||||
if (w == null || !(w has :precipitationChance)) { return DASH; }
|
||||
return num(w.precipitationChance) + "%";
|
||||
}
|
||||
if (id == 308) { // 风速
|
||||
if (w == null || !(w has :windSpeed) || w.windSpeed == null) { return DASH; }
|
||||
var kmh = w.windSpeed.toFloat() * 3.6;
|
||||
return (c.metric ? kmh : kmh * 0.621371).format("%d");
|
||||
}
|
||||
if (id == 307) { // 风向角
|
||||
return (w == null || !(w has :windBearing)) ? DASH : num(w.windBearing);
|
||||
}
|
||||
if (id == 310) { // 风向
|
||||
return (w == null || !(w has :windBearing)) ? DASH : windDir(w.windBearing);
|
||||
}
|
||||
if (id == 309) { // 天气更新时间
|
||||
if (w == null || !(w has :observationTime) || w.observationTime == null) {
|
||||
return DASH;
|
||||
}
|
||||
return clock(w.observationTime, c);
|
||||
}
|
||||
if (id == 312) { // 海平面气压
|
||||
if (w == null || !(w has :pressure) || w.pressure == null) { return DASH; }
|
||||
return (w.pressure.toFloat() / 100.0).format("%d");
|
||||
}
|
||||
if (id == 313) { // 紫外线指数
|
||||
return (w == null || !(w has :uvIndex)) ? DASH : num(w.uvIndex);
|
||||
}
|
||||
if (id == 314) { // 能见度
|
||||
if (w == null || !(w has :visibility) || w.visibility == null) { return DASH; }
|
||||
var km = w.visibility.toFloat() / 1000.0;
|
||||
return (c.metric ? km : km * 0.621371).format("%.1f");
|
||||
}
|
||||
if (id == 315) { // 露点
|
||||
// CIQ 没有露点字段,用 Magnus 公式从温度和湿度反推,
|
||||
// 误差在零点几度以内,够用。
|
||||
if (w == null || w.temperature == null
|
||||
|| !(w has :relativeHumidity) || w.relativeHumidity == null) {
|
||||
return DASH;
|
||||
}
|
||||
var t = w.temperature.toFloat();
|
||||
var rh = w.relativeHumidity.toFloat();
|
||||
if (rh <= 0.0) { return DASH; }
|
||||
var g2 = (17.62 * t) / (243.12 + t) + Math.ln(rh / 100.0);
|
||||
return temp(243.12 * g2 / (17.62 - g2), c);
|
||||
}
|
||||
if (id == 301 || id == 302 || id == 639) { // 最高气温 / 最低气温 / 最高/最低气温
|
||||
var f = c.forecast();
|
||||
if (f == null) { return DASH; }
|
||||
var hi = (f has :highTemperature) ? f.highTemperature : null;
|
||||
var lo = (f has :lowTemperature) ? f.lowTemperature : null;
|
||||
if (id == 301) { return temp(hi, c); } // 最高气温
|
||||
if (id == 302) { return temp(lo, c); } // 最低气温
|
||||
return temp(hi, c) + "/" + temp(lo, c);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- OpenWeatherMap ----
|
||||
// 读后台服务上次存下来的数据(本函数不联网,见 Owm.mc / Fenix8V3Background.mc)。
|
||||
// 后台一律用公制请求,单位换算和其他字段一样在这里做。
|
||||
function owm(id as Number, c as Ctx) as String? {
|
||||
if (!Owm.owns(id)) { return null; }
|
||||
if (!Owm.enabled()) { return DASH; }
|
||||
if (id == 500) { return temp(Owm.get("temp"), c); } // OWM 气温
|
||||
if (id == 503) { return temp(Owm.get("feels"), c); } // OWM 体感温度
|
||||
if (id == 504) { return temp(Owm.get("tmax"), c); } // OWM 最高温
|
||||
if (id == 505) { return temp(Owm.get("tmin"), c); } // OWM 最低温
|
||||
if (id == 515) { // OWM 最高/最低
|
||||
return temp(Owm.get("tmax"), c) + "/" + temp(Owm.get("tmin"), c);
|
||||
}
|
||||
if (id == 507) { return str(Owm.get("short")); } // OWM 天气
|
||||
if (id == 502) { return str(Owm.get("long")); } // OWM 天气详述
|
||||
if (id == 501) { // OWM 湿度
|
||||
var h = Owm.get("humidity");
|
||||
return h == null ? DASH : num(h) + "%";
|
||||
}
|
||||
if (id == 506) { return num(Owm.get("pressure")); } // OWM 气压
|
||||
if (id == 509) { // OWM 能见度
|
||||
var v = Owm.get("visibility");
|
||||
if (v == null) { return DASH; }
|
||||
var km = v.toFloat() / 1000.0;
|
||||
return (c.metric ? km : km * 0.621371).format("%.1f");
|
||||
}
|
||||
if (id == 510 || id == 512) { // OWM 风速 / OWM 阵风
|
||||
var w = Owm.get(id == 510 ? "wind" : "gust");
|
||||
if (w == null) { return DASH; }
|
||||
var kmh = w.toFloat() * 3.6;
|
||||
return (c.metric ? kmh : kmh * 0.621371).format("%d");
|
||||
}
|
||||
if (id == 511) { return num(Owm.get("winddeg")); } // OWM 风向角
|
||||
if (id == 518) { return windDir(Owm.get("winddeg")); } // OWM 风向
|
||||
if (id == 513) { // OWM 1h 降雨
|
||||
var r = Owm.get("rain");
|
||||
return r == null ? "0" : r.toFloat().format("%.1f");
|
||||
}
|
||||
if (id == 532) { // OWM 1h 降雪
|
||||
var sn = Owm.get("snow");
|
||||
return sn == null ? "0" : sn.toFloat().format("%.1f");
|
||||
}
|
||||
if (id == 536) { // OWM 云量
|
||||
var cl = Owm.get("clouds");
|
||||
return cl == null ? DASH : num(cl) + "%";
|
||||
}
|
||||
if (id == 537) { // OWM 降水概率
|
||||
var pop = Owm.get("pop");
|
||||
return pop == null ? DASH : (pop.toFloat() * 100.0).format("%d") + "%";
|
||||
}
|
||||
if (id == 530) { // OWM 露点
|
||||
// OWM 免费接口不返回露点,同样用 Magnus 公式从温湿度算。
|
||||
var t = Owm.get("temp");
|
||||
var rh = Owm.get("humidity");
|
||||
if (t == null || rh == null || rh.toFloat() <= 0.0) { return DASH; }
|
||||
var tf = t.toFloat();
|
||||
var gg = (17.62 * tf) / (243.12 + tf) + Math.ln(rh.toFloat() / 100.0);
|
||||
return temp(243.12 * gg / (17.62 - gg), c);
|
||||
}
|
||||
if (id == 534) { return temp(Owm.get("hi1"), c); } // OWM 明日气温
|
||||
if (id == 535) { return temp(Owm.get("hi2"), c); } // OWM 后日气温
|
||||
if (id == 522) { // OWM 明日高低温
|
||||
return temp(Owm.get("hi1"), c) + "/" + temp(Owm.get("lo1"), c);
|
||||
}
|
||||
if (id == 523) { // OWM 后日高低温
|
||||
return temp(Owm.get("hi2"), c) + "/" + temp(Owm.get("lo2"), c);
|
||||
}
|
||||
if (id == 538 || id == 539) { // 空气质量指数 / 描述
|
||||
var aqi = Owm.get("aqi");
|
||||
if (aqi == null) { return DASH; }
|
||||
if (id == 538) { return num(aqi); }
|
||||
// OWM 的 AQI 是 1..5 的等级,不是欧美常见的 0..500 数值。
|
||||
var names = ["", "优", "良", "中", "差", "很差"];
|
||||
var k = aqi.toNumber();
|
||||
return (k >= 1 && k <= 5) ? names[k] : DASH;
|
||||
}
|
||||
if (id == 540) { return oneDp(Owm.get("pm2_5")); } // PM2.5
|
||||
if (id == 541) { return oneDp(Owm.get("pm10")); } // PM10
|
||||
if (id == 542) { return oneDp(Owm.get("co")); } // 一氧化碳
|
||||
if (id == 516) { return str(Owm.get("city")); } // OWM 城市
|
||||
if (id == 517) { // OWM 更新时间
|
||||
var dt = Owm.get("dt");
|
||||
return dt == null ? DASH : clock(new Time.Moment(dt as Number), c);
|
||||
}
|
||||
return DASH;
|
||||
}
|
||||
|
||||
function oneDp(v) as String {
|
||||
return v == null ? DASH : v.toFloat().format("%.1f");
|
||||
}
|
||||
|
||||
function str(v) as String {
|
||||
return v == null ? DASH : v.toString();
|
||||
}
|
||||
|
||||
// ---- 系统 Complication ----
|
||||
// 日历、训练状态、完赛/配速预测、按运动的周距离、以及第三方 App 发布的
|
||||
// 复杂功能。全部由 Comp.mc 经 Toybox.Complications 取得。
|
||||
function complication(id as Number, c as Ctx) as String? {
|
||||
return Comp.value(id);
|
||||
}
|
||||
|
||||
// ---- 自定义文字 ----
|
||||
// 用户在设置里自己填的三段文字。
|
||||
function custom(id as Number, c as Ctx) as String? {
|
||||
if (id == 703) { return Settings.text("Custom1"); } // 自定义文字 1
|
||||
if (id == 704) { return Settings.text("Custom2"); } // 自定义文字 2
|
||||
if (id == 705) { return Settings.text("Custom3"); } // 自定义文字 3
|
||||
return null;
|
||||
}
|
||||
|
||||
// 取某个字段的显示文本。
|
||||
// 按分类依次尝试,而不是按编号区间派发 —— 编号是交错的(1/2/3 活动、
|
||||
// 8/11/12 身体、14 又是活动),区间派发行不通。
|
||||
function value(id as Number, c as Ctx) as String {
|
||||
var v = dateTime(id, c);
|
||||
if (v == null) { v = activity(id, c); }
|
||||
if (v == null) { v = body(id, c); }
|
||||
if (v == null) { v = system(id, c); }
|
||||
if (v == null) { v = environment(id, c); }
|
||||
if (v == null) { v = weather(id, c); }
|
||||
if (v == null) { v = complication(id, c); }
|
||||
if (v == null) { v = owm(id, c); }
|
||||
if (v == null) { v = custom(id, c); }
|
||||
return v == null ? "" : v;
|
||||
}
|
||||
}
|
||||
155
source/Icons.mc
Normal file
@@ -0,0 +1,155 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Graphics;
|
||||
import Toybox.WatchUi;
|
||||
import Layout;
|
||||
|
||||
// ============================================================================
|
||||
// 图标
|
||||
// ----------------------------------------------------------------------------
|
||||
// 顶部两个数据位上方的图标,加上电池和蓝牙符号。
|
||||
//
|
||||
// 设计稿里只有三个图标:日出、天气、电池。
|
||||
//
|
||||
// 前两个是位图描出来的矢量轮廓,最细的特征只有 2–3 个设计像素宽。一开始我用
|
||||
// 圆、线段、多边形去「重画」它们,在 454px 上看着还行,一到 280px(Enduro 3)
|
||||
// 就糊成一团 —— 因为 s=0.56 时那些细节不足 1 个物理像素。
|
||||
// 中间还试过做一版「低细节」图元,结果更糟:日出变成三条横杠。
|
||||
//
|
||||
// 现在的做法是:用 tools/gen_icons.py 把**设计稿的路径**按每种屏宽各光栅化一
|
||||
// 张位图,运行时用 drawBitmap2(:tintColor) 按主题色染色。这样每台设备拿到的
|
||||
// 都是为它的像素网格专门渲染的图。
|
||||
//
|
||||
// 其余图标(步数、卡路里、楼梯、心形…)设计稿里根本没有,所以保持图元绘制,
|
||||
// 尺寸同样基于 500 设计画布。
|
||||
// ============================================================================
|
||||
module Icons {
|
||||
|
||||
// 三张位图在 onLayout 时一次性加载(loadResource 开销不小,不能放 onUpdate)
|
||||
var mSunrise = null;
|
||||
var mSunset = null;
|
||||
var mWeather = null;
|
||||
|
||||
// 加载图标位图。资源按屏宽分目录,monkey.jungle 里为每台设备指定了
|
||||
// resources-icons-<屏宽>,所以这里拿到的自动就是对的尺寸。
|
||||
function load() as Void {
|
||||
mSunrise = WatchUi.loadResource(Rez.Drawables.IconSunrise);
|
||||
mSunset = WatchUi.loadResource(Rez.Drawables.IconSunset);
|
||||
mWeather = WatchUi.loadResource(Rez.Drawables.IconWeather);
|
||||
}
|
||||
|
||||
function ready() as Boolean { return mSunrise != null; }
|
||||
|
||||
// ⚠️ drawBitmap2 的 :tintColor 拒绝**调色板化**的位图,运行时会抛
|
||||
// "Source must not use a color palette"。所以生成的 drawables.xml 里每个
|
||||
// bitmap 都带 packingFormat="png",保住 alpha 通道不被压成调色板。
|
||||
function blit(dc as Graphics.Dc, bmp, cxDesign as Numeric, col as Number) as Void {
|
||||
if (bmp == null) { return; }
|
||||
dc.drawBitmap2(Layout.px(cxDesign) - bmp.getWidth() / 2,
|
||||
Layout.px(Layout.ICON_CY) - bmp.getHeight() / 2,
|
||||
bmp, {:tintColor => col});
|
||||
}
|
||||
|
||||
// 电池:外框 + 内部电量条 + 右侧触点。
|
||||
// 电量条长度跟随真实电量(设计稿画的是 40% 的状态)。
|
||||
// 画法是「先整块填充,再用黑色掏空内部,最后按比例填回电量条」。
|
||||
function battery(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
|
||||
k as Float, col as Number, level as Float) as Void {
|
||||
var x0 = Layout.px(x);
|
||||
var y0 = Layout.px(cyDesign - Layout.BATT_H * k / 2);
|
||||
var w = Layout.px(Layout.BATT_W * k);
|
||||
var h = Layout.px(Layout.BATT_H * k);
|
||||
var wall = Layout.px(Layout.BATT_WALL * k);
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillRoundedRectangle(x0, y0, w, h, Layout.px(3.4 * k));
|
||||
dc.fillRectangle(x0 + w, Layout.px(cyDesign - Layout.BATT_TH * k / 2),
|
||||
Layout.px(Layout.BATT_TW * k), Layout.px(Layout.BATT_TH * k));
|
||||
var iw = w - 2 * wall;
|
||||
var ih = h - 2 * wall;
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillRectangle(x0 + wall, y0 + wall, iw, ih);
|
||||
var fill = (iw * level).toNumber();
|
||||
if (fill > 0) {
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillRectangle(x0 + wall, y0 + wall, fill, ih);
|
||||
}
|
||||
}
|
||||
|
||||
// 标准蓝牙符号,用一条折线一笔画成(六段,中间的竖线会被走两次)。
|
||||
function bluetooth(dc as Graphics.Dc, col as Number) as Void {
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.setPenWidth(Layout.pen(3.2));
|
||||
var l = Layout.BT_CX - Layout.BT_HW;
|
||||
var r = Layout.BT_CX + Layout.BT_HW;
|
||||
var t = Layout.BT_CY - Layout.BT_HH;
|
||||
var b = Layout.BT_CY + Layout.BT_HH;
|
||||
var qt = Layout.BT_CY - Layout.BT_HH / 2;
|
||||
var qb = Layout.BT_CY + Layout.BT_HH / 2;
|
||||
Layout.line(dc, l, qt, r, qb);
|
||||
Layout.line(dc, r, qb, Layout.BT_CX, b);
|
||||
Layout.line(dc, Layout.BT_CX, b, Layout.BT_CX, t);
|
||||
Layout.line(dc, Layout.BT_CX, t, r, qt);
|
||||
Layout.line(dc, r, qt, l, qb);
|
||||
}
|
||||
|
||||
// 按字段编号决定上方画哪个图标。
|
||||
// 日期、周数、自定义文字这类字段没有合适的图标,就不画 —— 光有数值也读得懂,
|
||||
// 硬配一个反而干扰。
|
||||
function forField(dc as Graphics.Dc, id as Number, xDesign as Numeric,
|
||||
col as Number, level as Float) as Void {
|
||||
if (id == 712 || id == 700) {
|
||||
blit(dc, mSunrise, xDesign, col);
|
||||
return;
|
||||
}
|
||||
if (id == 717 || id == 731) {
|
||||
blit(dc, mSunset, xDesign, col);
|
||||
return;
|
||||
}
|
||||
if (id == 306 || id == 639 || id == 300 || id == 301 || id == 302
|
||||
|| id == 304 || id == 305 || id == 307 || id == 308
|
||||
|| id == 309 || id == 310) {
|
||||
blit(dc, mWeather, xDesign, col);
|
||||
return;
|
||||
}
|
||||
if (id == 250 || id == 251 || id == 257) {
|
||||
battery(dc, xDesign - 14.4, Layout.ICON_CY, 1.0, col, level);
|
||||
return;
|
||||
}
|
||||
var r = 13.0;
|
||||
var cyd = Layout.ICON_CY;
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.setPenWidth(Layout.pen(3.0));
|
||||
if (id == 2 || id == 603) { // 卡路里:火焰
|
||||
dc.fillPolygon([[Layout.px(xDesign), Layout.px(cyd - r)],
|
||||
[Layout.px(xDesign + r * 0.72), Layout.px(cyd + r * 0.25)],
|
||||
[Layout.px(xDesign + r * 0.40), Layout.px(cyd + r * 0.85)],
|
||||
[Layout.px(xDesign - r * 0.40), Layout.px(cyd + r * 0.85)],
|
||||
[Layout.px(xDesign - r * 0.72), Layout.px(cyd + r * 0.25)]]);
|
||||
} else if (id == 1 || id == 14 || id == 707) { // 步数:两个脚印
|
||||
dc.fillRoundedRectangle(Layout.px(xDesign - r * 0.85), Layout.px(cyd - r * 0.75),
|
||||
Layout.px(r * 0.62), Layout.px(r * 1.15),
|
||||
Layout.px(r * 0.3));
|
||||
dc.fillRoundedRectangle(Layout.px(xDesign + r * 0.22), Layout.px(cyd - r * 0.35),
|
||||
Layout.px(r * 0.62), Layout.px(r * 1.15),
|
||||
Layout.px(r * 0.3));
|
||||
} else if (id == 3 || id == 9 || id == 615) { // 距离/海拔:箭头
|
||||
dc.fillPolygon([[Layout.px(xDesign), Layout.px(cyd - r)],
|
||||
[Layout.px(xDesign + r * 0.75), Layout.px(cyd + r * 0.8)],
|
||||
[Layout.px(xDesign), Layout.px(cyd + r * 0.35)],
|
||||
[Layout.px(xDesign - r * 0.75), Layout.px(cyd + r * 0.8)]]);
|
||||
} else if (id == 618 || id == 276 || id == 623) { // 心率/静息/体能:心形
|
||||
dc.fillCircle(Layout.px(xDesign - r * 0.38), Layout.px(cyd - r * 0.28),
|
||||
Layout.px(r * 0.44));
|
||||
dc.fillCircle(Layout.px(xDesign + r * 0.38), Layout.px(cyd - r * 0.28),
|
||||
Layout.px(r * 0.44));
|
||||
dc.fillPolygon([[Layout.px(xDesign - r * 0.78), Layout.px(cyd - r * 0.12)],
|
||||
[Layout.px(xDesign + r * 0.78), Layout.px(cyd - r * 0.12)],
|
||||
[Layout.px(xDesign), Layout.px(cyd + r * 0.85)]]);
|
||||
} else if (id == 4 || id == 5 || id == 6) { // 楼层:台阶
|
||||
for (var i = 0; i < 3; i++) {
|
||||
dc.fillRectangle(Layout.px(xDesign - r * 0.85 + r * 0.58 * i),
|
||||
Layout.px(cyd + r * 0.6 - r * 0.55 * i),
|
||||
Layout.px(r * 0.62), Layout.px(r * 0.4));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
247
source/Layout.mc
Normal file
@@ -0,0 +1,247 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Graphics;
|
||||
import Toybox.Math;
|
||||
|
||||
// ============================================================================
|
||||
// 布局与文字绘制
|
||||
// ----------------------------------------------------------------------------
|
||||
// 本文件里所有的数字都是**设计稿 500×500 画布上的坐标**,全部来自
|
||||
// design/watchface-*.svg 的实测:
|
||||
// - 进度环的几何参数取自 design/rebuild.py(原作者从位图逆向出的常量)
|
||||
// - 各元素位置取自 SVG 描边路径的墨迹包围盒,不是目测的
|
||||
//
|
||||
// 绘制时统一乘以 s = 屏宽 / 500,所以同一套数字可以覆盖
|
||||
// 240px(fenix 7S)到 454px(venu 3)全部 17 款设备。
|
||||
//
|
||||
// ⚠️ 使用前必须先由 view 的 onLayout 调用一次 init()。
|
||||
// ============================================================================
|
||||
module Layout {
|
||||
|
||||
// ------------------------------- 进度环 -------------------------------
|
||||
// 刻度是**等角宽的环形扇区**,不是从圆心发射的线段 —— 所以画的时候用四点
|
||||
// 多边形填充,而不是 drawLine 描边。这点很关键:用线段画会导致刻度在内圈
|
||||
// 挤、外圈疏,与设计稿对不上。
|
||||
const R_IN = 231.09; // 刻度带内边缘半径
|
||||
const R_OUT = 249.10; // 刻度带外边缘半径
|
||||
const TOP_N = 21; // 每条顶弧的刻度数
|
||||
const TOP_PH = 6.500; // 第 0 根刻度的角度(自 12 点起顺时针,度)
|
||||
const TOP_PITCH = 4.000;
|
||||
const TOP_HW = 1.554; // 刻度角宽 3.108° 的一半
|
||||
const BOT_N = 21; // 每条底弧的刻度数
|
||||
const BOT_PH = 6.005; // 底弧角度自 6 点起量
|
||||
const BOT_PITCH = 3.000;
|
||||
const BOT_HW = 1.043;
|
||||
const DOT_RAD = 240.10; // 四个锚点圆点所在半径(正好是刻度带中线)
|
||||
const DOT_R = 9.00;
|
||||
const DOT_ANG = [0.0, 93.15, 180.0, 266.85];
|
||||
|
||||
// ------------------------------- 各元素 -------------------------------
|
||||
const BATT_X = 192.70; // 电池外框左边缘
|
||||
const BATT_W = 26.20; // 只是外框宽度,右侧的触点另算
|
||||
const BATT_H = 17.81;
|
||||
const BATT_WALL = 2.20; // 外框线宽
|
||||
const BATT_TW = 2.53; // 右侧触点宽
|
||||
const BATT_TH = 9.20;
|
||||
const BATT_VX = 232.00; // 百分比文字左边缘
|
||||
const BATT_CY = 53.20;
|
||||
|
||||
const LEFT_X = 157.00; // 顶部两个数据位的中心 x(居中对齐)
|
||||
const RIGHT_X = 343.70;
|
||||
const ICON_CY = 89.50; // 与图标位图自身的中心对齐,别随意改
|
||||
const VALUE_CY = 131.06;
|
||||
|
||||
const TIME_CY = 221.10;
|
||||
const HOURS_R = 211.40; // 小时数字右边缘(右对齐基准)
|
||||
const MINUTES_L = 276.02; // 分钟数字左边缘(左对齐基准)
|
||||
const COLON_X = 237.88;
|
||||
const COLON_W = 24.24;
|
||||
const COLON_H = 21.30;
|
||||
const COLON_Y1 = 183.68;
|
||||
const COLON_Y2 = 234.57;
|
||||
|
||||
const BAND_Y = 286.30;
|
||||
const BAND_H = 44.19;
|
||||
const BAND_CY = 308.10;
|
||||
const BT_CX = 36.88; // 蓝牙符号中心
|
||||
const BT_CY = 306.99;
|
||||
const BT_HW = 8.89;
|
||||
const BT_HH = 12.92;
|
||||
const DOW_CX = 178.05;
|
||||
const MD_CX = 298.28;
|
||||
const AMPM_R = 465.89;
|
||||
|
||||
const BOT_VAL_CY = 366.76;
|
||||
const BOT_LAB_CY = 410.88;
|
||||
const DIST_R = 234.30; // 左下格(数值+标签)的右对齐基准
|
||||
const STEP_L = 267.40; // 右下格(数值+标签)的左对齐基准
|
||||
|
||||
// 设计稿里三档文字的**字面高度**(cap height,大写字母/数字的高度,
|
||||
// 不含上下留白)。字号选择就是按这个来匹配的。
|
||||
const CAP_BAND = 25.34; // 日期带、电量百分比
|
||||
const CAP_COMP = 31.50; // 四个数据位、底部行
|
||||
const CAP_TIME = 98.70; // 时间大数字
|
||||
|
||||
// 数值最多占多宽,超了就降一档字号。
|
||||
// 顶部两格是居中对齐,这个值是它能横跨的总宽度;
|
||||
// 底部两格是边对齐,各占表盘中间往外的一半。
|
||||
// 有了这个,用户就算把 6 个汉字的长日期放进底部格也不会压到进度环上。
|
||||
const TOP_MAX_W = 185.0;
|
||||
const BOT_MAX_W = 150.0;
|
||||
|
||||
// ⚠️ Connect IQ 只暴露 ascent / descent / fontHeight,**没有字面高度接口**。
|
||||
// 而 ascent 里包含了数字和大写字母永远用不到的变音符号空间,直接拿 ascent
|
||||
// 当字面高度会导致选出来的字号明显偏小(第一版就是这么错的,时间数字只有
|
||||
// 设计稿的 57%)。
|
||||
//
|
||||
// 下面两个比例是**实测**出来的:在模拟器里渲染,然后从截图上量墨迹高度。
|
||||
// fenix847mm:FONT_XTINY ascent 29 → 大写实际高 22px → 0.76
|
||||
// FONT_NUMBER_MILD ascent 82 → 数字实际高 58px → 0.71
|
||||
// 各机型内置字体不完全相同(Roboto / RobotoCondensed / Yantramanav /
|
||||
// 各机型位图字体),比例会有几个百分点误差,可以接受。
|
||||
const CAP_OF_TEXT = 0.76;
|
||||
const CAP_OF_NUM = 0.71;
|
||||
|
||||
// ------------------------------- 运行时状态 ----------------------------
|
||||
var scale = 1.0; // 设备像素 / 设计像素
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var cx = 0;
|
||||
var cy = 0;
|
||||
var fontBand = Graphics.FONT_SMALL;
|
||||
var fontComp = Graphics.FONT_MEDIUM;
|
||||
var fontTime = Graphics.FONT_NUMBER_HOT;
|
||||
|
||||
// 文字字号按从小到大排列,这样放不下的值可以沿着这张表往下逐档缩。
|
||||
var textFonts = [Graphics.FONT_XTINY, Graphics.FONT_TINY, Graphics.FONT_SMALL,
|
||||
Graphics.FONT_MEDIUM, Graphics.FONT_LARGE];
|
||||
|
||||
function init(dc as Graphics.Dc) as Void {
|
||||
width = dc.getWidth();
|
||||
height = dc.getHeight();
|
||||
scale = width / 500.0;
|
||||
cx = width / 2;
|
||||
cy = height / 2;
|
||||
var nums = [Graphics.FONT_NUMBER_MILD, Graphics.FONT_NUMBER_MEDIUM,
|
||||
Graphics.FONT_NUMBER_HOT, Graphics.FONT_NUMBER_THAI_HOT];
|
||||
fontBand = pickFont(textFonts, CAP_BAND * scale, CAP_OF_TEXT);
|
||||
fontComp = pickFont(textFonts, CAP_COMP * scale, CAP_OF_TEXT);
|
||||
fontTime = pickFont(nums, CAP_TIME * scale, CAP_OF_NUM);
|
||||
}
|
||||
|
||||
function ready() as Boolean { return width > 0; }
|
||||
|
||||
// 设计像素 → 设备像素。整个文件里凡是坐标都要过这一层。
|
||||
function px(v as Numeric) as Number {
|
||||
return Math.round(v * scale).toNumber();
|
||||
}
|
||||
|
||||
function capHeight(font as Graphics.FontDefinition, ratio as Float) as Float {
|
||||
return Graphics.getFontAscent(font) * ratio;
|
||||
}
|
||||
|
||||
// 选**字面高度最接近**目标的那一档,而不是「不超过目标的最大档」。
|
||||
// 后者在设计尺寸恰好卡在两档字体之间时会白白小一号。
|
||||
function pickFont(cands as Array<Graphics.FontDefinition>, targetCap as Float,
|
||||
ratio as Float) as Graphics.FontDefinition {
|
||||
var best = cands[0];
|
||||
var bestErr = -1.0;
|
||||
for (var i = 0; i < cands.size(); i++) {
|
||||
var err = capHeight(cands[i], ratio) - targetCap;
|
||||
if (err < 0) { err = -err; }
|
||||
if (bestErr < 0 || err < bestErr) { bestErr = err; best = cands[i]; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// 值太长时(比如 12 个字符的自定义文字、6 个汉字的长日期)按设计稿字号会
|
||||
// 冲出表盘。这里沿字号表往下找,直到能塞进槽位宽度为止。
|
||||
function fitFont(dc as Graphics.Dc, str as String,
|
||||
font as Graphics.FontDefinition, maxDesignW as Float)
|
||||
as Graphics.FontDefinition {
|
||||
var limit = px(maxDesignW);
|
||||
var f = font;
|
||||
var i = textFonts.size() - 1;
|
||||
while (i >= 0) {
|
||||
if (textFonts[i] == f) { break; }
|
||||
i--;
|
||||
}
|
||||
if (i < 0) { return f; } // 不是文字字号表里的(比如数字字体),不动它
|
||||
while (i >= 0) {
|
||||
if (dc.getTextWidthInPixels(str, textFonts[i]) <= limit) {
|
||||
return textFonts[i];
|
||||
}
|
||||
i--;
|
||||
}
|
||||
return textFonts[0];
|
||||
}
|
||||
|
||||
// TEXT_JUSTIFY_VCENTER 居中的是**字体盒子**,而大写字母/数字只占据
|
||||
// 「基线往上 capHeight」这一段,在盒子里是偏上的。
|
||||
// 所以要往下推一个差值,才能让实际墨迹的中心落在 cy 上。
|
||||
function inkY(dc as Graphics.Dc, font as Graphics.FontDefinition,
|
||||
cyDesign as Numeric, ratio as Float) as Number {
|
||||
var fh = dc.getFontHeight(font);
|
||||
var asc = Graphics.getFontAscent(font);
|
||||
return px(cyDesign) + ((fh / 2.0) - asc + capHeight(font, ratio) / 2.0).toNumber();
|
||||
}
|
||||
|
||||
function text(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
|
||||
font as Graphics.FontDefinition, str as String,
|
||||
just as Number, col as Number) as Void {
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(px(x), inkY(dc, font, cyDesign, CAP_OF_TEXT), font, str,
|
||||
just | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
}
|
||||
|
||||
// 同上,但会在字符串超出槽位宽度时自动降字号。
|
||||
function fittedText(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
|
||||
font as Graphics.FontDefinition, str as String,
|
||||
just as Number, col as Number, maxDesignW as Float) as Void {
|
||||
text(dc, x, cyDesign, fitFont(dc, str, font, maxDesignW), str, just, col);
|
||||
}
|
||||
|
||||
// Monkey C 没法给文字填渐变。这里的做法是:**同一串数字画 N 遍**,每遍用
|
||||
// dc.setClip 把绘制限制在一条横向色带内,色带颜色取自主题的渐变停止点。
|
||||
//
|
||||
// 首尾两条色带故意向外延伸到屏幕边缘,保证字形上下不会有没画到的缝隙
|
||||
// (字体实际墨迹高度和我们估算的 capHeight 总有几像素出入)。
|
||||
function gradText(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric,
|
||||
font as Graphics.FontDefinition, str as String,
|
||||
just as Number, ramp as Array<Number>) as Void {
|
||||
var n = ramp.size();
|
||||
var y = inkY(dc, font, cyDesign, CAP_OF_NUM);
|
||||
var ink = capHeight(font, CAP_OF_NUM).toNumber();
|
||||
var top = px(cyDesign) - ink / 2;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var y0 = top + (ink * i) / n;
|
||||
var y1 = top + (ink * (i + 1)) / n;
|
||||
if (i == 0) { y0 = 0; }
|
||||
if (i == n - 1) { y1 = height; }
|
||||
if (y1 <= y0) { continue; }
|
||||
dc.setClip(0, y0, width, y1 - y0);
|
||||
dc.setColor(ramp[i], Graphics.COLOR_TRANSPARENT);
|
||||
dc.drawText(px(x), y, font, str, just | Graphics.TEXT_JUSTIFY_VCENTER);
|
||||
}
|
||||
dc.clearClip();
|
||||
}
|
||||
|
||||
// 设计稿的角度自 12 点起顺时针为正;屏幕 y 轴向下为正,所以是 cy - cos。
|
||||
function polarX(ang as Float, r as Float) as Number {
|
||||
return Math.round(cx + Math.sin(Math.toRadians(ang)) * r).toNumber();
|
||||
}
|
||||
|
||||
function polarY(ang as Float, r as Float) as Number {
|
||||
return Math.round(cy - Math.cos(Math.toRadians(ang)) * r).toNumber();
|
||||
}
|
||||
|
||||
function line(dc as Graphics.Dc, x1 as Numeric, y1 as Numeric,
|
||||
x2 as Numeric, y2 as Numeric) as Void {
|
||||
dc.drawLine(px(x1), px(y1), px(x2), px(y2));
|
||||
}
|
||||
|
||||
// 线宽在小屏上会被 round 成 0 直接消失,这里兜一个最小值 2。
|
||||
function pen(v as Numeric) as Number {
|
||||
var w = px(v);
|
||||
return w < 2 ? 2 : w;
|
||||
}
|
||||
}
|
||||