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)); } } } }