Implement 7_UX_BUTTONS: Restructured selections horizontal timeline layers to overlap behind translated anchor buttons, added long-click haptic feedback edit shortcut and custom sliders dialog to save custom JSON presets

This commit is contained in:
2026-07-05 17:27:10 +07:00
parent edd4c2946d
commit 4a33140c67
8 changed files with 876 additions and 121 deletions
+144
View File
@@ -0,0 +1,144 @@
# 🎨 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`.
```xml
<!-- 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`.
```kotlin
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ẻ.