Files

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:

  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.

<!-- 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)

  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.