Files
fenix8v3-watchface/source/Fenix8V3View.mc
ericwyuan d9d45d983b 接入系统 Complication,补齐倒计时/自定义格式/28天累计/空气质量
字段 125 → 156 项。

Toybox.Complications(source/Comp.mc)
  这条路推翻了我之前几个「表盘做不到」的判断 —— 当时只查了 ActivityMonitor /
  UserProfile / Weather,漏了系统把这些作为 complication 发布出来:
    - 下一个日程(CIQ 确实没有日历 API,但有 CALENDAR_EVENTS complication)
    - 训练状态、完赛预测 ×4、配速预测 ×4、最近高尔夫得分
    - 本周跑步 / 骑行距离(按运动分类的,按天历史做不出来)
    - 第三方 App 1–5(CGM、补水、行情这类 App 发布的)
  需要 ComplicationSubscriber 权限。

其余补齐
  - 近 28 天累计:步数 / 卡路里 / 距离 / 楼层
  - 事件倒计时:设置里填 YYYY-MM-DD,显示还剩几天
  - 自定义时间格式、日期格式 1/2:单字母占位符(h/H/m/s/a/D/M/N/Y/y/w)
  - OWM 空气质量:AQI 等级 + 描述 + PM2.5 + PM10 + CO,走
    /data/2.5/air_pollution —— 那个接口在免费档就开放,不需要 One Call 订阅

运行时踩的坑
  - Complication.unit 不保证是 String(周跑步距离返回的就不是),直接调
    .length() 抛 Symbol Not Found。unit 和 shortLabel 都改成先 instanceof 判型。
  - 浮点 value 直接 toString 印成 "0.000000",统一格式化成一位小数。
  - 第三方 App 槽位每取一次就要走完整个迭代器,四个格子都选 App 时会走四遍。
    改成每帧只枚举一次,由 view 在绘制开始时调 Comp.beginFrame() 失效。

测试
  全字段验证的 harness 本身会触发看门狗(一次 onUpdate 里算 156 个字段),
  改成分批每帧 12 个。实际使用一帧只算 4 个,不受影响。
  156 项全部可渲染,零运行时错误;17 款设备全部编译通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 09:03:21 +08:00

474 lines
23 KiB
MonkeyC
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Toybox.Graphics;
import Toybox.Lang;
import Toybox.Math;
import Toybox.ActivityMonitor;
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 {
// 常亮显示状态。
// AMOLED 屏在息屏常亮时必须大幅减少点亮像素Garmin 上架审核会卡这一条,
// 而且不这么做既费电又有烧屏风险MIP 屏不受影响,照常全量绘制。
private var mSleeping = false;
private var mBurnIn = false;
function initialize() {
WatchFace.initialize();
}
// 进入低功耗息屏。AMOLED 机型从这一刻起改画简化版面。
function onEnterSleep() as Void {
mSleeping = true;
WatchUi.requestUpdate();
}
// 抬腕唤醒,恢复完整版面。
function onExitSleep() as Void {
mSleeping = false;
WatchUi.requestUpdate();
}
// 表盘加载时调一次;如果 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;
}
// ------------------------------- 进度环 -------------------------------
// 计算四段弧各自的进度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) { // 步数:对每日目标
if (info.stepGoal != null && info.stepGoal > 0 && info.steps != null) {
p = info.steps.toFloat() / info.stepGoal.toFloat();
}
} else if (idx == 1) { // 卡路里:固定 2000 kcal 为满
if (info.calories != null) { p = info.calories.toFloat() / 2000.0f; }
} else if (idx == 2) { // 距离10 km 为满distance 单位是厘米)
if (info.distance != null) { p = info.distance.toFloat() / 1000000.0f; }
} 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 == 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;
}
// 画一根刻度:环形扇区,用四点多边形填充。
// 扇区两条弧边其实是曲线,但 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)]]);
}
// 通用画弧。点亮方向与设计稿一致:
// 两条顶弧:从 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 = 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 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 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);
}
// 顶部单个数据位:上方图标 + 下方数值。
// 数值走 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);
}
// 日期带:整条横幅底色 + 蓝牙符号 + 星期 + 月日 + 上午/下午。
// 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);
}
}
// 中央时间。小时和分钟各自带竖向渐变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 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 取 c1f=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;
}
// ------------------------------- 常亮模式 ------------------------------
// 低功耗画面。相比正常画面砍掉的东西:
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
// - 未点亮的刻度(只画有数据的那部分,且压暗到 45%
// - 四个数据位、电池(全部隐藏)
// 保留的时间和日期压暗到 55%。
//
// 另外整个画面按分钟在 ±4 像素内游走,避免同一个像素长期点亮导致烧屏。
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 top = Themes.RINGTOP[ti] as Array<Number>;
var bot = Themes.RINGBOTTOM[ti] as Array<Number>;
var litTR = (pTR * Layout.TOP_N).toNumber();
var litTL = (pTL * Layout.TOP_N).toNumber();
var litBR = (pBR * Layout.BOT_N).toNumber();
var litBL = (pBL * Layout.BOT_N).toNumber();
for (var i = 0; i < Layout.TOP_N; i++) {
var a = Layout.TOP_PH + Layout.TOP_PITCH * i;
if (i >= Layout.TOP_N - litTR) {
dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, a, Layout.TOP_HW);
}
if (i >= Layout.TOP_N - litTL) {
dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, -a, Layout.TOP_HW);
}
}
for (var i = 0; i < Layout.BOT_N; i++) {
var a = Layout.BOT_PH + Layout.BOT_PITCH * i;
if (i < litBL) {
dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, 180.0 + a, Layout.BOT_HW);
}
if (i < litBR) {
dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, 180.0 - a, Layout.BOT_HW);
}
}
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.55));
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.55));
dc.setColor(dim(Themes.COLON[ti] as Number, 0.55), 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.55);
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);
}
}
}
// ------------------------------- 主绘制 --------------------------------
// 每分钟调用一次(以及唤醒、设置变更时)。
function onUpdate(dc as Dc) as Void {
if (!Layout.ready() || Layout.width != dc.getWidth()
|| !Fields.ready() || !Icons.ready()) {
onLayout(dc);
}
if (dc has :setAntiAlias) { dc.setAntiAlias(true); }
// 每次绘制只构造一个 Ctx四个数据位共用 —— 天气、用户档案这些
// 开销大的查询因此每帧最多做一次,且只在真有格子要用时才做。
var ti = Settings.themeIndex(Themes.NAMES.size());
var ds = System.getDeviceSettings();
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 = ringProgress(Settings.ringTR(), info);
var pTL = ringProgress(Settings.ringTL(), info);
var pBR = ringProgress(Settings.ringBR(), info);
var pBL = ringProgress(Settings.ringBL(), info);
// 常亮模式走另一条绘制路径,画完直接返回。
if (mSleeping && mBurnIn) {
drawAmbient(dc, ti, ds, pTR, pTL, pBR, pBL);
return;
}
drawRing(dc, ti, pTR, pTL, pBR, pBL);
if (Settings.showTop()) {
drawBattery(dc, ti, level);
drawTopSlot(dc, ti, Settings.leftTop(), true, ctx, level);
drawTopSlot(dc, ti, Settings.rightTop(), false, ctx, level);
}
drawTime(dc, ti, ds);
if (Settings.showBand()) {
drawDateBand(dc, ti, ds);
}
if (Settings.showBottom()) {
drawBottom(dc, ti, ctx);
}
}
}