Files
photobooth-app/7_UX_BUTTONS.md
T

7.4 KiB

🎨 KẾ HOẠCH TÁI CẤU TRÚC UX: HOẠT HỌA DI CHUYỂN & DANH SÁCH CUỘN ĐỒNG HÀNG

Mục tiêu: Khi kích hoạt Frame hoặc Preset, nút đại diện sẽ dịch chuyển sang trái làm điểm neo. Danh sách các item sẽ trượt trên cùng một hàng ngang và chui xuống dưới nút neo khi lướt (Swipe). Nhấn giữ preset để mở shortcut chỉnh sửa.


📐 1. Cấu Trúc Lại XML Layout Tuyến Tính (activity_main.xml)

Để đạt được hiệu ứng cuộn "chui xuống dưới nút", nút Neo (Anchor) phải có thứ tự hiển thị nằm trên (Z-index cao hơn) và nằm đè lên RecyclerView. Ta sẽ sử dụng một FrameLayout hoặc ConstraintLayout để làm việc này tại khu vực viền xanh lá của file image_2a8660.png.

<!-- KHU VỰC ĐIỀU KHIỂN DÒNG (Khung viền xanh trong image_2a8660.png) -->
<FrameLayout
    android:id="@+id/panelSelectionContainer"
    android:layout_width="match_parent"
    android:layout_height="70dp"
    android:background="#000000"
    app:layout_constraintTop_toBottomOf="@id/cameraContainer"
    app:layout_constraintBottom_toTopOf="@id/zoomControls">

    <!-- LỚP ĐÁY: Danh sách cuộn ngang dùng chung cho cả Frame và Preset -->
    <!-- Thuộc tính paddingStart giúp phần tử đầu tiên không bị nút Neo che khuất lúc khởi tạo -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvHorizontalTimeline"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:clipToPadding="false"
        android:paddingStart="80dp" 
        android:paddingEnd="16dp"
        android:visibility="gone" />

    <!-- LỚP TRÊN: Chứa 2 nút chức năng chính (Gốc ở giữa, động chuyển sang trái) -->
    <ConstraintLayout
        android:id="@+id/buttonsWrapper"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- Nút Frame -->
        <ImageView
            android:id="@+id/btnMainFrame"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:src="@drawable/ic_frame_default"
            android:background="@null"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/btnMainPreset"
            app:layout_constraintHorizontal_chainStyle="packed"
            android:layout_marginEnd="16dp"/>

        <!-- Nút Presets Màu -->
        <ImageView
            android:id="@+id/btnMainPreset"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:src="@drawable/ic_palette_default"
            android:background="@null"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@id/btnMainFrame"
            app:layout_constraintEnd_toEndOf="parent" />
            
    </ConstraintLayout>
</FrameLayout>


💻 2. Kịch Bản Hoạt Họa & Điều Khiển Trạng Thái (Kotlin)

🔹 2.1. Logic Di Chuyển Nút Ra Cạnh Trái (Animation)

Khi người dùng nhấn vào nút Frame, ta sẽ chạy một Animation dịch chuyển nút này về sát rìa trái màn hình, đồng thời ẩn nút còn lại và hiển thị RecyclerView.

private var isFrameModeActive = false

fun toggleFrameMode(activate: Boolean) {
    if (activate) {
        isFrameModeActive = true
        // 1. Ẩn nút Preset màu đi
        binding.btnMainPreset.animate().alpha(0f).setDuration(200).withEndAction {
            binding.btnMainPreset.visibility = View.GONE
        }
        
        // 2. Dịch chuyển nút Frame sang trái (Dùng TransitionManager để mượt mà)
        TransitionManager.beginDelayedTransition(binding.buttonsWrapper)
        val params = binding.btnMainFrame.layoutParams as ConstraintLayout.LayoutParams
        params.endToStart = ConstraintLayout.LayoutParams.UNSET // Hủy liên kết xích giữa
        params.endToEnd = ConstraintLayout.LayoutParams.UNSET
        binding.btnMainFrame.layoutParams = params
        
        // 3. Hiển thị RecyclerView cùng hàng
        binding.rvHorizontalTimeline.apply {
            adapter = frameAdapter // Đổi sang dữ liệu của Frame
            visibility = View.VISIBLE
            alpha = 0f
            animate().alpha(1f).setDuration(300).start()
        }
    } else {
        // Logic phục hồi về trạng thái cân bằng ở giữa ban đầu như trong ảnh image_2a8660.png
    }
}

Giải thích cơ chế "Chui xuống dưới": Do rvHorizontalTimeline được khai báo trước trong FrameLayout, nó nằm ở lớp dưới. Nút btnMainFrame nằm ở lớp trên (ConstraintLayout). Khi bạn vuốt cuộn ngang RecyclerView, các item bên trong sẽ tự nhiên trượt và chui xuống dưới khu vực nút Frame đang đứng cố định ở góc trái.


🔄 3. Cơ Chế Nhấn Giữ Preset & Nút Chỉnh Sửa Nhỏ (Edit Shortcut)

Để giải quyết yêu cầu nhấn giữ mở nút Edit nhỏ mà không làm thay đổi preset gốc, chúng ta sẽ áp dụng cơ chế Floating Context Button.

🔹 3.1. Thiết kế Giao Diện Nút Edit Nhỏ trong Item của RecyclerView

Trong file layout thiết kế cho từng ô Preset nhỏ (item_preset.xml), ta thêm một nút icon cây bút nhỏ (btnSmallEdit) nằm đè ở góc trên bên phải của ô. Mặc định nút này sẽ ẩn (android:visibility="gone").

🔹 3.2. Code bắt sự kiện Nhấn giữ (OnLongClickListener)

  1. Bước 1: Hiện nút Edit nhỏ: Nhấn giữ Item. Trong Adapter của Preset, gán sự kiện setOnLongClickListener cho ô màu. Khi kích hoạt, hiển thị nút btnSmallEdit (View.VISIBLE) kèm hiệu ứng rung nhẹ (Haptic Feedback) để thông báo cho người dùng.

  2. Bước 2: Mở bảng chỉnh sửa độc lập: Chạm nút Edit. Khi người dùng bấm vào nút btnSmallEdit nhỏ vừa hiện ra, app sẽ nhân bản (Clone) đối tượng ColorPreset hiện tại thành một thực thể tạm thời và mở bảng điều khiển Sliders (Vibrance, Clarity, Dehaze...).

  3. Bước 3: Lưu thành Preset mới: Đặt tên & Ghi file. Sau khi tinh chỉnh xong và ấn Save, một Dialog hiện lên yêu cầu nhập tên mới (Ví dụ: "Instax của Lộc"). App lưu file JSON mới này vào bộ nhớ riêng (context.filesDir) và giữ nguyên vẹn file preset gốc ban đầu.


📅 4. Các Đầu Việc Cần Làm Ngay (Checklist)

  • Cập nhật lại Code Layout: Thiết lập FrameLayout bao bọc bên ngoài đúng như cấu trúc để tạo lớp đè cho hiệu ứng chui dưới nút.
  • Tạo File Hoạt Họa/Transition: Viết logic hoàn chỉnh cho cả việc lật ngược trạng thái (khi người dùng bấm vào nút Neo lần nữa để đóng timeline, đưa 2 nút quay lại vị trí trung tâm cân bằng như ảnh image_2a8660.png).
  • Cập nhật ViewHolder trong Adapter: Thêm tính năng ẩn/hiện nút btnSmallEdit dựa trên trạng thái click của từng item đơn lẻ.