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 项测试全绿。
This commit is contained in:
ZXCLI
2026-09-12 15:36:34 +08:00
parent 6ba90f2bb6
commit ee6859877a
20 changed files with 459 additions and 70 deletions
@@ -0,0 +1,265 @@
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);
}
}