Cơ chế hoạt động của IceorsView.kt
[!NOTE] Đây là bản clone/mini-port của
DrawSurfaceViewNew(tên gốcE1/a.java) từ APK Iceors Color-by-Number gốc. View này render một ảnh "tô màu theo số" dùng SVG paths.
Kiến trúc tổng quan
graph TD
A["IceorsAsset.load()"] -->|parse file <key>b| B["IceorsAsset.Loaded"]
B --> C["IceorsView.setAsset()"]
B -->|contains| D["List<IceorsRegion>"]
B -->|contains| E["List<PaletteEntry>"]
C --> F["fitToView() → set Matrix"]
C --> G["notifyProgress()"]
C --> H["invalidate() → onDraw()"]
1. Data Model — IceorsRegion & IceorsAsset
File format (pipe-delimited)
svgPath | colorHex | strokeWidth | labelPosPacked | fontSize | [labelHex]
Phân loại region (IceorsRegion.Kind)
| Điều kiện | Kind | Ý nghĩa |
|---|---|---|
color == white & strokeWidth == 0 |
(bỏ qua) | Region vô dụng |
strokeWidth != 0 |
STROKE_LINE |
Đường viền trang trí |
(color & 0xFFFFFF) == 0 (đen) |
BLACK_FILL |
Tô đen trang trí |
| Còn lại | FILLABLE |
Region user có thể tô |
Palette
- Mỗi màu duy nhất trong các
FILLABLEregion → 1 PaletteEntry (index 1-based, theo thứ tự xuất hiện) - Mỗi
IceorsRegionfillable cópaletteIndextrỏ tới bucket palette tương ứng
2. Rendering Pipeline — onDraw()
View vẽ theo 5 lớp (từ dưới lên):
graph BT
L1["① White background"] --> L2["② Fillable regions"]
L2 --> L3["③ Active palette highlight"]
L3 --> L4["④ Decorations (stroke/black)"]
L4 --> L5["⑤ Palette index numbers"]
L5 --> L6["⑥ Hint flash (nếu có)"]
Lớp ①: Nền trắng
canvas.drawColor(Color.WHITE)
Lớp ②: Fillable regions
- Nếu
completed→ vẽ bằng màu palette gốc - Nếu chưa
completed→ vẽ bằng placeholder xám (#E6E6E6)
Lớp ③: Active palette highlight
- Chỉ vẽ lên các region chưa completed mà có
paletteIndex == activePaletteIndex - Dùng
BitmapShadervớiTileMode.REPEAT→ tạo pattern ô caro checker 4×4 px, màu 25% alpha của palette color - Mục đích: highlight những ô user cần tô tiếp theo
Lớp ④: Decorations
STROKE_LINE→ vẽ đường viền đen vớistrokeWidthtừ dataBLACK_FILL→ tô đặc màu đen
Lớp ⑤: Số palette
- Chỉ vẽ cho region chưa completed
- Visibility gate:
fontSize × zoom ≥ 12dp→ số chỉ hiện khi zoom đủ gần - Text size cố định: luôn là
9dptrên màn hình (không phụ thuộc kích thước region) - Vị trí:
(labelX, labelY)decode từ packed coordinatey * canvasW + x
Lớp ⑥: Hint flash (tùy chọn)
- Amber pulse animation (sin wave 2 chu kỳ trong 900ms)
- Vẽ fill + stroke với alpha dao động
3. Transform & Zoom — matrixView
Toàn bộ canvas sử dụng 1 Matrix duy nhất matrixView cho cả pan + zoom:
flowchart LR
FIT["fitToView()"] -->|"setScale + translate"| M["matrixView"]
PINCH["ScaleGestureDetector"] -->|"postScale()"| M
DRAG["GestureDetector.onScroll"| -->|"postTranslate()"| M
M -->|"canvas.concat()"| DRAW["onDraw()"]
M -->|"invert → mapPoints()"| TAP["handleTap()"]
fitToView()
- Tính
fitScale = min(viewWidth / canvasSize, viewHeight / canvasSize) - Căn giữa canvas trong view
- Gọi khi
setAsset()vàonSizeChanged()
Zoom constraints
- Min:
fitScale × 1.0(không zoom out quá mức fit) - Max:
fitScale × 8.0
4. User Interaction
Tap → Tô màu — handleTap()
sequenceDiagram
participant U as User
participant V as IceorsView
participant R as IceorsRegion
U->>V: tap(screenX, screenY)
V->>V: invert matrixView → canvas coords (tx, ty)
loop Mỗi fillable region
V->>R: completed? paletteIndex match? contains(tx,ty)?
alt Tất cả đúng
R->>R: completed = true
V->>V: notifyProgress()
V->>V: invalidate()
end
end
Hit-testing: Duyệt O(n) qua tất cả fillable regions:
- Skip nếu
completed - Skip nếu
paletteIndex != activePaletteIndex - Kiểm tra
bounds.contains()(fast reject bằng RectF) - Kiểm tra
Region.contains()(pixel-accurate theo path)
[!TIP] App gốc dùng index bitmap (O(1) lookup bằng
(-2) - getPixel(x,y)) thay vì duyệt O(n). Đây là optimization tiềm năng cho tương lai.
Pinch → Zoom
ScaleGestureDetector→matrixView.postScale()quanh focus point- Clamp trong
[fitScale × 1, fitScale × 8]
Drag → Pan
GestureDetector.onScroll→matrixView.postTranslate(-dx, -dy)
5. Hint System — requestHint()
sequenceDiagram
participant Caller
participant V as IceorsView
Caller->>V: requestHint()
V->>V: Filter uncompleted regions matching active palette
V->>V: Pick random candidate
V->>V: focusOn(target) — pan+zoom to center region
V->>V: Start amber flash animation (900ms)
Note over V: postDelayed(900ms)
V->>V: target.completed = true
V->>V: notifyProgress() + invalidate()
focusOn(): Nếu zoom hiện tại <fitScale × 2, tự zoom in để region dễ thấy. Sau đó pan để region nằm giữa view.
6. Progress Tracking — notifyProgress()
Hai callback:
| Callback | Data | Mô tả |
|---|---|---|
onProgressChanged |
(completed, total) |
Tổng số region đã tô / tổng |
onPaletteProgressChanged |
Map<Int, IntArray> |
Per-palette: [done, total] cho mỗi bucket |
7. Tóm tắt luồng chính
flowchart TD
START["IceorsAsset.load(file)"] --> PARSE["Parse pipe-delimited lines<br/>→ classify regions → build palette"]
PARSE --> SETASSET["view.setAsset(loaded)"]
SETASSET --> FIT["fitToView() — calc fit Matrix"]
SETASSET --> DRAW["invalidate() → onDraw()<br/>5-layer rendering"]
USER_TAP["User taps region"] --> HIT["handleTap()<br/>invert matrix → hit-test O(n)"]
HIT -->|match| FILL["region.completed = true"]
FILL --> NOTIFY["notifyProgress()"]
FILL --> REDRAW["invalidate() → onDraw()"]
USER_PALETTE["User chọn palette"] --> SELECT["selectPaletteIndex()"]
SELECT --> PATTERN["updateActivePattern()<br/>rebuild BitmapShader"]
SELECT --> REDRAW
USER_HINT["User dùng hint"] --> HINT["requestHint()"]
HINT --> FOCUS["focusOn(random region)"]
HINT --> FLASH["Amber flash 900ms"]
FLASH --> FILL