Files
photobooth-app/4_ANDROID_UI.md

234 lines
11 KiB
Markdown

# 📱 KẾ HOẠCH PHÁT TRIỂN: GIAO DIỆN CHỌN FRAME THEO STACK (REAL-TIME OVERLAY)
Mục tiêu: Xây dựng bố cục giao diện gồm khu vực Top Bar điều khiển, Khung camera chính tích hợp Frame Overlay động, danh sách các Stack chủ đề dạng thanh trượt ngang (Timeline), cụm nút Zoom và Control Panel chụp ảnh phía dưới.
---
## 📁 1. Chuẩn Bị Cấu Trúc Dữ Liệu Model (Kotlin)
Để quản lý các Frame được chia theo từng Stack chủ đề như trong file `image_28b4c8.png`, chúng ta định nghĩa một cấu trúc dữ liệu rõ ràng:
```kotlin
data class FrameItem(
val id: String,
val name: String,
val imageFileName: String // Tên file PNG trong suốt nằm trong thư mục assets
)
data class FrameStack(
val id: String,
val themeName: String, // Ví dụ: "Retro", "Vintage", "Summer"
val frames: List<FrameItem> // Danh sách các frame thuộc chủ đề này
)
```
---
## 🛠️ 2. Cấu Trúc XML Bố Cục Giao Diện (`activity_main.xml`)
Bố cục được chia tầng bằng `ConstraintLayout` kết hợp với `ImageView` đóng vai trò làm lớp Overlay đè lên `PreviewView` của CameraX.
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#121212">
<!-- ================= 1. TOP BAR CONTROL ================= -->
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent">
<!-- Các nút AUTO FLASH, TIMER OFF, FRONT CAMERA như trong image_28b4c8.png -->
</LinearLayout>
<!-- ================= 2. KHUNG FRAME CHÍNH (CAMERA + OVERLAY) ================= -->
<FrameLayout
android:id="@+id/cameraContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toBottomOf="@id/topBar">
<!-- Lớp đáy: Kính ngắm CameraX -->
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Lớp đè (Overlay): Hiển thị Khung hình PNG do người dùng chọn -->
<ImageView
android:id="@+id/imgFrameOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:contentDescription="Frame Overlay" />
</FrameLayout>
<!-- ================= 3. TIMELINE DANH SÁCH STACK FRAMES ================= -->
<!-- Thanh trượt ngang hiển thị các Stack 1 Retro, Stack 2 Vintage... -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFrameStacks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="@id/cameraContainer" />
<!-- ================= 4. CỤM NÚT ĐIỀU KHIỂN ZOOM ================= -->
<LinearLayout
android:id="@+id/zoomControls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="#1A1A1A"
android:padding="4dp"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rvFrameStacks">
<!-- Các nút 0.5x, 1x, 3x xếp ngang -->
</LinearLayout>
<!-- ================= 5. CONTROL PANEL CHỤP ẢNH ================= -->
<RelativeLayout
android:id="@+id/controlPanel"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/zoomControls">
<ImageButton
android:id="@+id/btnCapture"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_centerInParent="true"
android:background="@drawable/bg_capture_button"
android:src="@drawable/ic_camera_white" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
```
---
## 💻 3. Kịch Bản Logic Điều Khiển Giao Diện (Kotlin)
Quy trình tương tác khi người dùng thao tác trên giao diện timeline:
1. **Nạp danh sách Stack từ Assets:** Đọc dữ liệu.
Ứng dụng quét thư mục `assets/frames/`, khởi tạo danh sách `List<FrameStack>` gồm các chủ đề kèm ảnh đại diện cho từng Stack rồi đẩy dữ liệu vào `rvFrameStacks`.
2. **Người dùng chọn một Stack:** Sự kiện chạm.
Khi chạm vào một Stack (ví dụ: "Stack 2 Vintage" trong file `image_28b4c8.png`), giao diện có thể bung ra một hàng ngang phụ (Sub-timeline) hiển thị các mẫu khung con bên trong hoặc tự động áp dụng khung mặc định đầu tiên của Stack đó.
3. **Hiển thị Overlay thời gian thực:** Cập nhật UI.
Đọc file ảnh PNG tương ứng từ assets bằng `BitmapFactory.decodeStream()`. Gán bitmap này vào `imgFrameOverlay`. Nhờ lớp `FrameLayout`, khung ảnh ngay lập tức hiển thị đè mịn màng lên luồng preview của camera.
---
## 📅 4. Các Đầu Việc Cần Làm Tiếp Theo (Checklist)
* [ ] **Tạo Adapter cho RecyclerView:** Viết `FrameStackAdapter` để hiển thị các ô vuông "Stack 1 Retro", "Stack 2 Vintage" với viền đỏ bo góc như thiết kế trong ảnh `image_28b4c8.png`.
* [ ] **Xử lý hiệu ứng Selection:** Khi một Stack được bấm chọn, vẽ một viền màu nổi bật (hoặc đổi màu background của ô) để người dùng biết họ đang ở chế độ nào.
* [ ] **Đồng bộ ảnh chụp cuối cùng:** Đảm bảo khi bấm chụp (`btnCapture`), thuật toán gộp ảnh sẽ lấy chính xác ID của frame đang hiển thị trên `imgFrameOverlay` để vẽ đè lên ảnh độ phân giải cao.
## 🛠️ 5. Cấu Trúc Lại XML Giao Diện (`activity_main.xml`)
Chúng ta sẽ chèn thêm một `RecyclerView` thứ hai nằm ngay trên danh sách các Stack để làm thanh chọn khung con.
```xml
<!-- Thay thế phần 3 (TIMELINE DANH SÁCH STACK FRAMES) ở kế hoạch cũ bằng cụm này -->
<LinearLayout
android:id="@+id/timelineContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/cameraContainer">
<!-- TẦNG BỔ SUNG: RecyclerView hiển thị các mẫu khung con bên trong Stack -->
<!-- Mặc định khi mới mở app, thanh này ẩn (android:visibility="gone") -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSubFrames"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="6dp"
android:background="#1E1E1E" />
<!-- TẦNG GỐC (Giống ảnh image_28b4c8.png): Danh sách các Stack chủ đề lớn -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFrameStacks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp" />
</LinearLayout>
```
---
## 💻 6. Cấu Trúc Logic Cập Nhật & Tạo Nút Mặc Định (Kotlin)
### 🔹 6.1. Cập nhật Model Dữ Liệu
Để tạo ra nút "Mặc định", chúng ta sẽ quy ước một đối tượng `FrameItem` đặc biệt với ID là `"NONE"` hoặc `"DEFAULT"`.
```kotlin
// Hàm khởi tạo dữ liệu mẫu cho một Stack luôn đi kèm nút Mặc định ở đầu
fun getFramesForStack(stackId: String): List<FrameItem> {
val subFrames = mutableListOf<FrameItem>()
// Luôn thêm phần tử Mặc định (Không dùng khung) vào vị trí đầu tiên [0]
subFrames.add(FrameItem(id = "DEFAULT", name = "Mặc định", imageFileName = ""))
// Tải các khung thực tế thuộc chủ đề từ Assets
when(stackId) {
"retro" -> {
subFrames.add(FrameItem("r1", "Retro Polaroid", "retro_1.png"))
subFrames.add(FrameItem("r2", "Retro Film 35mm", "retro_2.png"))
}
"vintage" -> {
subFrames.add(FrameItem("v1", "Vintage Wood", "vintage_1.png"))
}
}
return subFrames
}
```
### 🔹 6.2. Kịch Bản Tương Tác Hai Tầng (User Interaction Flow)
1. **Bước 1: Người dùng chạm vào Stack (Tầng 1):** Chọn chủ đề chính.
Khi bấm vào "Stack 2 Vintage" (từ layout `image_28b4c8.png`), app kích hoạt hiển thị `rvSubFrames` (`visibility = View.VISIBLE`). Đồng thời nạp danh sách khung con tương ứng vào thanh này.
2. **Bước 2: Người dùng chọn Khung con (Tầng 2):** Chọn mẫu cụ thể.
Người dùng vuốt và chọn một mẫu khung con cụ thể. Nếu chọn trúng các mẫu như `v1`, `r1`, ảnh PNG tương ứng từ assets sẽ được render đè lên màn hình camera thông qua lớp `imgFrameOverlay`.
3. **Bước 3: Quay về trạng thái Mặc định:** Hủy bỏ bộ lọc.
Nếu người dùng không ưng ý, họ chạm vào phần tử đầu tiên (ô "Mặc định"). Logic code lập tức gọi lệnh `imgFrameOverlay.setImageDrawable(null)`, xóa bỏ hoàn toàn khung overlay hiện tại để camera quay về trạng thái gốc sạch sẽ.
---
## 📅 7. Các Đầu Việc Cần Triển Khai (Checklist)
* [ ] **Thiết kế UI cho ô "Mặc định":** Tạo một file ảnh icon hoặc vẽ một hình tròn có gạch chéo đỏ nhẹ (biểu tượng đóng/bỏ chọn) để làm ảnh đại diện (thumbnail) cho ô Mặc định trong danh sách con.
* [ ] **Quản lý biến trạng thái toàn cục:** Tạo biến `private var currentSelectedFramePath: String? = null`. Nếu biến này bằng `null` (khi chọn mặc định), hàm chụp ảnh cuối cùng sẽ hiểu là chỉ lưu ảnh camera gốc mà không thực hiện gộp layer.
* [ ] **Hiệu ứng thu gọn (Ẩn thanh con):** Cài đặt logic khi người dùng bấm lại vào chính Stack chủ đề đang chọn lần thứ hai, thanh `rvSubFrames` sẽ tự động ẩn đi (`View.GONE`) để giao diện gọn gàng hơn.