- Split render into FieldTable/Fields/Icons/Layout/Settings modules - Add Fenix8V3Background, Owm (weather), Solar modules - Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg) - Tune README/manifest/strings/settings
472 lines
23 KiB
MonkeyC
472 lines
23 KiB
MonkeyC
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;
|
||
|
||
// ============================================================================
|
||
// 表盘主体
|
||
// ----------------------------------------------------------------------------
|
||
// 负责:进度环、盖在环上的各个元素、以及 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 取 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;
|
||
}
|
||
|
||
// ------------------------------- 常亮模式 ------------------------------
|
||
// 低功耗画面。相比正常画面砍掉的东西:
|
||
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
|
||
// - 未点亮的刻度(只画有数据的那部分,且压暗到 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());
|
||
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);
|
||
}
|
||
}
|
||
}
|