11 KiB
📱 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:
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 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:
-
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áchList<FrameStack>gồm các chủ đề kèm ảnh đại diện cho từng Stack rồi đẩy dữ liệu vàorvFrameStacks. -
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 đó. -
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àoimgFrameOverlay. Nhờ lớpFrameLayout, 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 ảnhimage_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ênimgFrameOverlayđể 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.
<!-- 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".
// 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)
-
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. -
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ớpimgFrameOverlay. -
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ằngnull(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
rvSubFramessẽ tự động ẩn đi (View.GONE) để giao diện gọn gàng hơn.