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 trongFrameLayout, nó nằm ở lớp dưới. NútbtnMainFramenằm ở lớp trên (ConstraintLayout). Khi bạn vuốt cuộn ngangRecyclerView, 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)
-
Bước 1: Hiện nút Edit nhỏ: Nhấn giữ Item. Trong Adapter của Preset, gán sự kiện
setOnLongClickListenercho ô màu. Khi kích hoạt, hiển thị nútbtnSmallEdit(View.VISIBLE) kèm hiệu ứng rung nhẹ (Haptic Feedback) để thông báo cho người dùng. -
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
btnSmallEditnhỏ vừa hiện ra, app sẽ nhân bản (Clone) đối tượngColorPresethiệ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...). -
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
FrameLayoutbao 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
btnSmallEditdựa trên trạng thái click của từng item đơn lẻ.