196 lines
9.1 KiB
Markdown
196 lines
9.1 KiB
Markdown
# 🎨 KẾ HOẠCH PHÁT TRIỂN: BẢNG SLIDERS TÙY CHỈNH MÀU THỜI GIAN THỰC (REAL-TIME OVERLAY)
|
|
|
|
Mục tiêu: Khi bấm nút Edit nhỏ trên một Preset, một bảng chỉnh sửa mờ (Semi-transparent Overlay) sẽ trượt lên từ phía dưới, đè lên kính ngắm camera. Mọi thao tác kéo trượt của người dùng sẽ cập nhật trực tiếp vào bộ xử lý đồ họa phần cứng để thay đổi màu ảnh ngay lập tức (WYSIWYG).
|
|
|
|
---
|
|
|
|
## 📐 1. Cấu Trúc Thành Phần Giao Diện (XML Layout)
|
|
|
|
Để bảng điều khiển này có thể phủ (Overlay) lên hình ảnh camera mà không làm dịch chuyển hay thu nhỏ kính ngắm, chúng ta sẽ định nghĩa nó như một lớp trên cùng nằm trong `FrameLayout` chính của ứng dụng.
|
|
|
|
### 🔹 1.1. Bổ sung giao diện ẩn vào `activity_main.xml`
|
|
|
|
Bảng này mặc định sẽ ẩn đi (`android:visibility="gone"`) và được gom nhóm trong một thẻ `NestedScrollView` để người dùng có thể cuộn lên/xuống nếu có nhiều thông số.
|
|
|
|
```xml
|
|
<!-- Chèn thêm vào phân tầng cao nhất của FrameLayout chứa camera hoặc Layout gốc -->
|
|
<LinearLayout
|
|
android:id="@+id/layoutAdvancedSliders"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:orientation="vertical"
|
|
android:background="#D9000000" <!-- Nền đen mờ 85% để vừa nhìn thấy camera phía sau vừa rõ chữ -->
|
|
android:padding="20dp"
|
|
android:clickable="true"
|
|
android:focusable="true"
|
|
android:visibility="gone"
|
|
app:layout_constraintBottom_toTopOf="@id/panelSelectionContainer">
|
|
|
|
<!-- Thanh tiêu đề & Nút thao tác nhanh -->
|
|
<RelativeLayout
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:layout_marginBottom="16dp">
|
|
<TextView
|
|
android:id="@+id/tvEditPresetName"
|
|
android:layout_width="wrap_content"
|
|
android:layout_height="wrap_content"
|
|
android:text="Chỉnh sửa: Instax Warm"
|
|
android:textColor="#FFFFFF"
|
|
android:textStyle="bold"
|
|
android:textSize="16sp" />
|
|
<ImageButton
|
|
android:id="@+id/btnCancelEdit"
|
|
android:layout_width="32dp"
|
|
android:layout_height="32dp"
|
|
android:layout_alignParentEnd="true"
|
|
android:background="@null"
|
|
android:src="@drawable/ic_close_white" />
|
|
</RelativeLayout>
|
|
|
|
<!-- KHU VỰC CÁC THANH KÉO (SLIDERS) -->
|
|
<!-- 1. Thanh chỉnh VIBRANCE -->
|
|
<TextView
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:text="Vibrance (Bão hòa thông minh)"
|
|
android:textColor="#CCCCCC"
|
|
android:textSize="12sp" />
|
|
<com.google.android.material.slider.Slider
|
|
android:id="@+id/sliderVibrance"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:valueFrom="-1.0"
|
|
android:valueTo="1.0"
|
|
android:value="0.0"
|
|
app:thumbColor="#FF9800"
|
|
app:trackColorActive="#FF9800"
|
|
app:trackColorInactive="#333333" />
|
|
|
|
<!-- 2. Thanh chỉnh CLARITY -->
|
|
<TextView
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:text="Clarity (Độ rõ nét vùng trung tính)"
|
|
android:textColor="#CCCCCC"
|
|
android:textSize="12sp"
|
|
android:layout_marginTop="8dp" />
|
|
<com.google.android.material.slider.Slider
|
|
android:id="@+id/sliderClarity"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:valueFrom="-1.0"
|
|
android:valueTo="1.0"
|
|
android:value="0.0"
|
|
app:thumbColor="#FF9800"
|
|
app:trackColorActive="#FF9800"
|
|
app:trackColorInactive="#333333" />
|
|
|
|
<!-- 3. Thanh chỉnh DEHAZE -->
|
|
<TextView
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:text="Dehaze (Độ trong suốt / Sương mù)"
|
|
android:textColor="#CCCCCC"
|
|
android:textSize="12sp"
|
|
android:layout_marginTop="8dp" />
|
|
<com.google.android.material.slider.Slider
|
|
android:id="@+id/sliderDehaze"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:valueFrom="-1.0"
|
|
android:valueTo="1.0"
|
|
android:value="0.0"
|
|
app:thumbColor="#FF9800"
|
|
app:trackColorActive="#FF9800"
|
|
app:trackColorInactive="#333333" />
|
|
|
|
<!-- NÚT LƯU THÀNH PRESET MỚI -->
|
|
<Button
|
|
android:id="@+id/btnSaveCustomPreset"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:layout_marginTop="20dp"
|
|
android:text="LƯU THÀNH PRESET MỚI"
|
|
android:textColor="#FFFFFF"
|
|
android:backgroundTint="#FF9800"
|
|
android:textStyle="bold" />
|
|
</LinearLayout>
|
|
|
|
```
|
|
|
|
---
|
|
|
|
## 💻 2. Kịch Bản Tương Tác & Đồng Bộ Thời Gian Thực (Kotlin)
|
|
|
|
### 🔹 2.1. Chuẩn bị biến lưu trữ tạm thời (Clone Object)
|
|
|
|
Khi người dùng bấm nút Edit nhỏ từ Item trong RecyclerView, chúng ta không chỉnh sửa trực tiếp lên file gốc mà nhân bản dữ liệu ra một biến tạm:
|
|
|
|
```kotlin
|
|
private var editingPreset: ColorPreset? = null
|
|
|
|
fun openAdvancedSliders(selectedPreset: ColorPreset) {
|
|
// Clone đối tượng thông qua cơ chế sao chép dữ liệu (Deep Copy hoặc JSON String conversion)
|
|
editingPreset = selectedPreset.copy(
|
|
basic = selectedPreset.basic.copy(),
|
|
advanced = selectedPreset.advanced.copy()
|
|
)
|
|
|
|
// Cập nhật giá trị hiện tại lên các thanh Slider trực quan
|
|
binding.sliderVibrance.value = editingPreset!!.basic.vibrance
|
|
binding.sliderClarity.value = editingPreset!!.advanced.clarity
|
|
binding.sliderDehaze.value = editingPreset!!.advanced.dehaze
|
|
|
|
// Hiển thị bảng kèm hoạt họa trượt lên mượt mà (Slide up animation)
|
|
binding.layoutAdvancedSliders.apply {
|
|
visibility = View.VISIBLE
|
|
translationY = this.height.toFloat()
|
|
animate().translationY(0f).setDuration(300).start()
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
### 🔹 2.2. Lắng nghe thay đổi của Sliders và cập nhật Camera (Real-time Pipeline)
|
|
|
|
1. **Bước 1: Người dùng kéo Slider:** Lắng nghe sự kiện.
|
|
Gán trình lắng nghe `addOnChangeListener` cho từng Slider (ví dụ: `sliderVibrance`). Khi người dùng di chuyển ngón tay, giá trị float (`value`) thay đổi liên tục.
|
|
|
|
|
|
2. **Bước 2: Cập nhật thông số vào Object tạm:** Cập nhật Biến số.
|
|
Cập nhật ngay giá trị mới vào biến tạm thời:
|
|
`editingPreset?.basic?.vibrance = value`.
|
|
|
|
|
|
3. **Bước 3: Đẩy thông số vào Khung hình Camera:** Render GPU.
|
|
Gọi hàm `applyLiveFilterToCamera(editingPreset)`. Hàm này sẽ chuyển đổi toàn bộ thông số của `editingPreset` thành một ma trận cấu hình hoặc nạp trực tiếp vào OpenGL Shader (hoặc GPUImage) đang liên kết với `PreviewView` của CameraX. Màn hình camera sẽ thay đổi sắc độ và cấu trúc ảnh ngay lập tức.
|
|
|
|
|
|
---
|
|
|
|
## 💾 3. Quy Trình Đổi Tên Và Lưu File Độc Lập
|
|
|
|
Khi bấm nút **btnSaveCustomPreset**, ứng dụng sẽ thực hiện các bước sau để tránh đè lên preset mặc định:
|
|
|
|
1. **Hiển thị Dialog nhập tên:** Mở một `AlertDialog` tối giản với một ô `EditText` để người dùng đặt tên cho bộ lọc của họ (Ví dụ mặc định gợi ý sẵn: `[Tên cũ] - Custom`).
|
|
2. **Sinh ID duy nhất:** Sử dụng `UUID.randomUUID().toString()` để tạo ID mới cho preset này. Đổi thuộc tính `themeCategory` thành `"User Custom"`.
|
|
3. **Lưu file JSON vật lý:** Sử dụng thư viện GSON để chuyển đổi Object `editingPreset` vừa tạo thành chuỗi JSON và ghi vào thư mục lưu trữ nội bộ của ứng dụng trên điện thoại:
|
|
```kotlin
|
|
val file = File(context.filesDir, "preset_${newId}.json")
|
|
file.writeText(gson.toJson(editingPreset))
|
|
|
|
```
|
|
|
|
|
|
4. **Cập nhật lại thanh trượt ngoài màn hình chính:** Đóng bảng slider nâng cao, nạp lại danh sách preset từ thư mục chứa file để item mới vừa tạo xuất hiện ngay lập tức trên thanh Timeline ngang.
|
|
|
|
---
|
|
|
|
## 📅 4. Các Đầu Việc Cần Làm Tiếp Theo (Checklist)
|
|
|
|
* [ ] **Cài đặt thư viện đồ họa thời gian thực:** Cấu hình thư viện xử lý ảnh (như `GPUImage` cho Android) chạy song song với `PreviewView` của CameraX để nhận các lệnh cập nhật thông số liên tục từ Slider mà không bị giật lag khung hình.
|
|
* [ ] **Thiết kế hoạt họa đóng/mở:** Viết các tệp Animation (`slide_up.xml`, `slide_down.xml`) để tạo hiệu ứng chuyển cảnh mượt mà cho bảng trượt Overlay.
|
|
* [ ] **Kiểm tra giới hạn kéo:** Thiết lập tính năng hiển thị số thực tế (ví dụ: `+0.25`, `-0.10`) ngay cạnh tên Slider khi người dùng đang kéo để họ nhận biết được mức độ tăng giảm chính xác.
|
|
|