Files
ZXCLI ee6859877a android+docs: debug 日志默认关闭;操作手册升级为插图版
debug 日志:DebugLog 增加总开关 enabled(默认 false),log() 直接返回、
init() 不装崩溃钩子、startFile() 不建文件。要在设备上排查时改成 true 重新构建
即可,其它代码不动。关闭后崩溃仍按安卓常规方式可见(logcat AndroidRuntime +
系统弹窗),只是不再有落在相册目录旁的 debug_*.log。

真机烟雾测试(小米 22041211AC,相机插着):出流正常、拍照成功(相册 19→20,
新增 MAG160C_20260912_153522.jpg)、崩溃 0、App 自身日志行数 0、无新日志文件。

插图版手册 docs/android_app/app_manual.md + manual_images/:
- 02_markers:标记结构解剖(准星/读数/引线/描边字),真机照片放大 3 倍并标注
- 03/04:横屏 960x720 与竖屏 720x960 的真实照片
- 05/06/07:设置、分析(含底部测量面板)、相册 真机截图
- 01_live_schematic:实时界面改为**自绘示意图**,因为实时 OSD 文字按握持角度预旋转
  (用户要求的可读性设计),手册拍摄时手机横放,截图里文字全是侧躺的无法阅读;
  其余界面文字本就水平,直接用截图。

图由 manual_images/MakeFigures.java 生成(裁剪缩放/放大标注/示意图绘制),
界面改动后重跑即可;原始截图存为 JPEG(PNG→JPEG 省约 8MB)作为可再生输入。
.class 已加入 .gitignore。

README 阅读顺序加入操作手册、结构表更新、里程碑补记根因级修复。105 项测试全绿。
2026-09-12 15:36:34 +08:00

266 lines
12 KiB
Java
Raw Permalink 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 java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
/**
* Derive the manual's figures: scale/crop real screenshots, and build an annotated
* schematic of the live screen. Kept as a tool so the figures can be regenerated
* after a UI change instead of being stale hand-edited PNGs.
*
* Usage: MakeFigures <outDir> <imageDir>
*/
public class MakeFigures {
static File outDir;
public static void main(String[] args) throws Exception {
outDir = new File(args[0]);
File imgs = new File(args[1]);
outDir.mkdirs();
// --- real screenshots: scale to a readable width and crop the chrome-free part
// cropped to the rows: the full-screen shots are mostly empty on a 9:20 display
scale(new File(imgs, "01_settings_raw.jpg"), "05_settings.png", 780, 0, 60, 1440, 1720);
scale(new File(imgs, "05_analysis_raw.jpg"), "06_analysis.png", 700, 0, 60, 1440, 3040);
scale(new File(imgs, "06_gallery_raw.jpg"), "07_gallery.png", 780, 0, 60, 1440, 1260);
// --- reference photos (already the real saved files)
scale(new File(imgs, "MAG160C_20260912_145010.jpg"), "03_photo_landscape.png", 900, 0, 0, 0, 0);
scale(new File(imgs, "MAG160C_20260912_141530.jpg"), "04_photo_portrait.png", 560, 0, 0, 0, 0);
// --- marker anatomy: zoom on the MAX reticle of the landscape photo
BufferedImage lp = ImageIO.read(new File(imgs, "MAG160C_20260912_141530.jpg"));
markerAnatomy(lp);
// --- live screen schematic (the real screen cannot be screenshotted for the
// manual: its OSD text is pre-rotated to the current grip, so a landscape-held
// phone yields sideways labels in the picture)
liveSchematic();
}
// ---------------------------------------------------------------- helpers
static BufferedImage scaled(BufferedImage s, int w) {
int h = Math.max(1, (int) Math.round(s.getHeight() * (w / (double) s.getWidth())));
BufferedImage o = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = o.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(s, 0, 0, w, h, null);
g.dispose();
return o;
}
static void scale(File src, String out, int w, int x, int y, int cw, int ch) throws Exception {
BufferedImage s = ImageIO.read(src);
if (cw > 0) s = s.getSubimage(x, y, cw, ch);
BufferedImage o = scaled(s, w);
ImageIO.write(o, "png", new File(outDir, out));
System.out.println("wrote " + out + " " + o.getWidth() + "x" + o.getHeight());
}
static Font font(int size, int style) {
// a CJK-capable family; falls back to the platform default if absent
return new Font("Microsoft YaHei", style, size);
}
/** Zoomed crop around the MAX reticle, with labelled callouts. */
static void markerAnatomy(BufferedImage photo) {
// Pt1 on the portrait reference photo (720x960): the reticle sits at
// (249,147) and the label to its right, so this window holds the whole glyph
// plus its readout.
int cx = 300, cy = 150;
int cw = 420, ch = 260;
int x = Math.max(0, cx - cw / 2), y = Math.max(0, cy - ch / 2);
BufferedImage crop = photo.getSubimage(x, Math.min(y, photo.getHeight() - ch), cw, ch);
int zoom = 3;
BufferedImage big = new BufferedImage(cw * zoom, ch * zoom, BufferedImage.TYPE_INT_RGB);
Graphics2D g = big.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.drawImage(crop, 0, 0, cw * zoom, ch * zoom, null);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setFont(font(26, Font.BOLD));
// callouts: the reticle, the label, the leader
int rx = (cx - x) * zoom, ry = (cy - y) * zoom;
callout(g, "① 方形准星", rx - 150, ry - 120, rx, ry, new Color(0x4FC3F7));
callout(g, "② MAX/MIN 读数", rx + 150, ry + 120, rx + 210, ry, new Color(0x81C784));
g.setColor(new Color(0xFFD54F));
g.setFont(font(24, Font.BOLD));
g.drawString("③ 引线(读数连到自己的准星)", 24, ch * zoom - 60);
g.drawString("④ 白字 + 黑描边(无底板)", 24, ch * zoom - 24);
g.dispose();
try {
ImageIO.write(big, "png", new File(outDir, "02_markers.png"));
System.out.println("wrote 02_markers.png " + big.getWidth() + "x" + big.getHeight());
} catch (Exception e) {
e.printStackTrace();
}
}
static void callout(Graphics2D g, String text, int lx, int ly, int tx, int ty, Color c) {
g.setColor(c);
g.setStroke(new BasicStroke(3f));
g.drawLine(lx, ly, tx, ty);
g.fillOval(tx - 7, ty - 7, 14, 14);
FontMetrics fm = g.getFontMetrics();
int w = fm.stringWidth(text);
int bx = lx < tx ? lx - w - 12 : lx + 12;
g.setColor(new Color(0, 0, 0, 190));
g.fillRoundRect(bx - 6, ly - fm.getAscent() - 4, w + 12, fm.getHeight(), 8, 8);
g.setColor(c);
g.drawString(text, bx, ly);
}
/**
* The live screen as a labelled diagram. Positions mirror the real layout
* (top control bar glued to the portrait top edge, image below it, capture row,
* bottom navigation).
*/
static void liveSchematic() throws Exception {
int W = 720, H = 1280;
BufferedImage im = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);
Graphics2D g = im.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setColor(new Color(0x101014));
g.fillRect(0, 0, W, H);
// top bar
int barH = 96;
g.setColor(new Color(0x5A5A50));
g.fillRect(0, 24, W, barH);
String[] top = {"FFC", "1×", "追踪", "铁虹", "画中画"};
for (int i = 0; i < top.length; i++) {
int cx = 78 + i * 142;
g.setColor(new Color(0xD8D8C8));
g.setStroke(new BasicStroke(4f));
g.drawOval(cx - 16, 24 + barH / 2 - 26, 32, 32);
g.setFont(font(22, Font.PLAIN));
FontMetrics fm = g.getFontMetrics();
g.drawString(top[i], cx - fm.stringWidth(top[i]) / 2, 24 + barH - 8);
}
// image area with a marker, mimicking the real look
int imgX = 8, imgY = 24 + barH + 16, imgW = W - 16, imgH = 830;
GradientPaint gp = new GradientPaint(
imgX, imgY, new Color(0x1A0733),
imgX + imgW, imgY + imgH, new Color(0xB25A00));
g.setPaint(gp);
g.fillRect(imgX, imgY, imgW, imgH);
g.setColor(new Color(0x5E1580));
g.fillOval(imgX + 40, imgY + 250, 380, 320);
g.setColor(new Color(0xFFE066));
g.fillOval(imgX + 380, imgY + 520, 260, 240);
// a probe marker drawn the way the app draws it
int mx = imgX + 420, my = imgY + 400;
g.setColor(Color.WHITE);
g.setStroke(new BasicStroke(3f));
g.drawRect(mx - 12, my - 12, 24, 24);
g.drawLine(mx - 30, my, mx - 12, my);
g.drawLine(mx + 12, my, mx + 30, my);
g.drawLine(mx, my - 30, mx, my - 12);
g.drawLine(mx, my + 12, mx, my + 30);
g.drawLine(mx + 30, my, mx + 62, my);
g.setFont(font(30, Font.BOLD));
drawOutlined(g, "Pt1 25.0℃", mx + 62, my + 10);
// a max marker
int ax = imgX + 90, ay = imgY + 170;
g.setStroke(new BasicStroke(3f));
g.setColor(Color.WHITE);
g.drawRect(ax - 12, ay - 12, 24, 24);
g.drawLine(ax + 12, ay, ax + 30, ay);
g.drawLine(ax, ay - 30, ax, ay - 12);
g.drawLine(ax, ay + 12, ax, ay + 30);
g.drawLine(ax - 30, ay, ax - 12, ay);
g.drawLine(ax + 30, ay, ax + 62, ay);
g.setFont(font(30, Font.BOLD));
drawOutlined(g, "MAX 41.5℃", ax + 62, ay + 10);
// centre readout, upper-left of the image
g.setFont(font(30, Font.BOLD));
drawOutlined(g, "中心 26.4℃", imgX + 20, imgY + 40);
// colour bar along the right edge
int barX = imgX + imgW - 34, barW = 22;
for (int i = 0; i < 120; i++) {
float t = i / 119f;
g.setColor(new Color(
(int) (255 * Math.min(1, t * 1.6)),
(int) (255 * Math.max(0, t * 1.6 - 0.6)),
(int) (255 * Math.max(0, 1 - t * 2.2))));
g.fillRect(barX, imgY + 40 + i * 6, barW, 7);
}
// capture row
int rowY = imgY + imgH + 30;
g.setColor(new Color(0x1B1B20));
g.fillRect(0, rowY - 10, W, 210);
circle(g, 200, rowY + 60, 44, new Color(0x555555), "相册");
circle(g, 360, rowY + 60, 62, Color.WHITE, "拍照");
circle(g, 520, rowY + 60, 44, new Color(0xE53935), "录像");
// bottom navigation
int navY = H - 118;
g.setColor(new Color(0x2A2A22));
g.fillRect(0, navY, W, 118);
String[] nav = {"实时", "相册", "分析", "设置"};
for (int i = 0; i < nav.length; i++) {
int cx = 90 + i * 180;
g.setColor(i == 0 ? new Color(0xD8C88A) : new Color(0x9A9A9A));
g.setStroke(new BasicStroke(4f));
g.drawOval(cx - 16, navY + 22, 32, 32);
g.setFont(font(24, Font.PLAIN));
FontMetrics fm = g.getFontMetrics();
g.drawString(nav[i], cx - fm.stringWidth(nav[i]) / 2, navY + 92);
}
// numbered callouts
g.setFont(font(26, Font.BOLD));
badge(g, 1, 100, 40);
badge(g, 2, 242, 40);
badge(g, 3, 384, 40);
badge(g, 4, 526, 40);
badge(g, 5, 420, rowY + 20);
badge(g, 6, 130, navY + 24);
g.dispose();
ImageIO.write(im, "png", new File(outDir, "01_live_schematic.png"));
System.out.println("wrote 01_live_schematic.png " + W + "x" + H);
}
static final String[] CIRCLED = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"};
static void badge(Graphics2D g, int n, int x, int y) {
String s = CIRCLED[n - 1];
g.setColor(new Color(0xE53935));
g.fillOval(x - 20, y - 20, 40, 40);
g.setColor(Color.WHITE);
FontMetrics fm = g.getFontMetrics();
g.drawString(s, x - fm.stringWidth(s) / 2, y + 10);
}
static void circle(Graphics2D g, int cx, int cy, int r, Color c, String label) {
g.setColor(c);
g.fillOval(cx - r, cy - r, r * 2, r * 2);
if (c == Color.WHITE) {
g.setColor(new Color(0x222222));
g.setStroke(new BasicStroke(3f));
g.drawOval(cx - r, cy - r, r * 2, r * 2);
}
g.setColor(Color.WHITE);
g.setFont(font(24, Font.PLAIN));
FontMetrics fm = g.getFontMetrics();
g.drawString(label, cx - fm.stringWidth(label) / 2, cy + r + 34);
}
/** White text with a dark outline — the same trick the app's markers use. */
static void drawOutlined(Graphics2D g, String s, int x, int y) {
Shape shape = g.getFont().createGlyphVector(g.getFontRenderContext(), s).getOutline(x, y);
g.setColor(new Color(0, 0, 0, 220));
g.setStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.draw(shape);
g.setColor(Color.WHITE);
g.fill(shape);
}
}