- 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
156 lines
8.0 KiB
MonkeyC
156 lines
8.0 KiB
MonkeyC
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));
|
||
}
|
||
}
|
||
}
|
||
}
|