fix: chỉnh sửa button cho phần chỉnh presets

This commit is contained in:
2026-07-05 21:55:40 +07:00
parent b5d7311447
commit 4e3c6f35c7
32 changed files with 1381 additions and 351 deletions
+207
View File
@@ -0,0 +1,207 @@
# 🛠️ KẾ HOẠCH FIX BUG: CHUẨN HÓA LOGIC SLIDERS & FIX LỖI TRÀN KHUNG HIỂN THỊ
Mục tiêu:
1. Khắc phục triệt để lỗi gán nhầm thông số xử lý ảnh (ví dụ: kéo Brightness lại đổi Temperature).
2. Chuyển đổi dải giá trị từ số thập phân nhỏ sang thang đo nguyên trực quan từ `-100` đến `+100` để người dùng thấy rõ sự thay đổi.
3. Cô lập nội dung cuộn bên trong phạm vi bo góc của `MaterialCardView` để không bị tràn đè lên khung viền của bảng.
---
## 📐 1. Khắc Phục Lỗi Giao Diện Tràn Khung Biển (`activity_main.xml`)
**Nguyên nhân lỗi:** Do tệp cấu hình cũ đặt thuộc tính `padding` trực tiếp lên thẻ cha hoặc thẻ `LinearLayout` bên trong mà không giới hạn vùng hiển thị đồ họa của thẻ cuộn `ScrollView`. Khi vuốt, các phần tử chữ và thanh kéo sẽ trượt đè qua các góc bo tròn của hộp Floating Card.
**Giải pháp:** Di chuyển toàn bộ cấu hình `padding` vào bên trong thẻ `ScrollView` và ép cấu trúc cắt vùng đồ họa thừa bằng thuộc tính `android:clipToPadding="true"`.
```xml
<!-- LỚP 3: BẢNG SLIDERS FLOATING (Nằm gọn bên trong ảnh cameraContainer) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardAdvancedSliders"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="24dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
android:visibility="gone"
app:strokeWidth="0dp"
android:backgroundTint="#A6000000">
<!-- FIX LỖI ĐÈ KHUNG: Đưa padding và clipToPadding vào ScrollView -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="vertical"
android:padding="16dp"
android:clipToPadding="true"> <!-- Ép nội dung cuộn phải biến mất khi chạm tới viền bo góc -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Tiêu đề & Nút X (Đóng) -->
<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:textColor="#FFFFFF"
android:textStyle="bold"
android:textSize="15sp" />
<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>
<!-- ================= CẬP NHẬT DẢI GIÁ TRỊ TOÀN BỘ SLIDERS (-100 đến 100) ================= -->
<!-- Brightness -->
<TextView android:id="@+id/tvLabelBrightness" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp"/>
<com.google.android.material.slider.Slider
android:id="@+id/sliderBrightness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="-100.0"
android:valueTo="100.0"
android:value="0.0"
app:thumbColor="#FF9800"
app:trackColorActive="#FF9800"/>
<!-- Temperature -->
<TextView android:id="@+id/tvLabelTemperature" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp"/>
<com.google.android.material.slider.Slider
android:id="@+id/sliderTemperature"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="-100.0"
android:valueTo="100.0"
android:value="0.0"
app:thumbColor="#FF9800"
app:trackColorActive="#FF9800"/>
<!-- Áp dụng tương tự cho tất cả 12 thanh Sliders còn lại cấu hình valueFrom="-100.0" và valueTo="100.0" -->
<!-- Đối với các thuộc tính đi từ 0 (Vignette, Grain, Faded Black) thì cấu hình valueFrom="0.0" và valueTo="100.0" -->
<!-- NÚT LƯU THÀNH PRESET MỚI -->
<Button
android:id="@+id/btnSaveCustomPreset"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="20dp"
android:text="LƯU THÀNH PRESET MỚI"
android:textColor="#FFFFFF"
android:backgroundTint="#FF9800"
android:textStyle="bold" />
</LinearLayout>
</ScrollView>
</com.google.android.material.card.MaterialCardView>
```
---
## 💻 2. Chuẩn Hóa Logic Gán Hàm Độc Lập & Quy Đổi Tỷ Lệ Giá Trị (Kotlin)
**Giải pháp sửa lỗi gán sai thông số:** Quy hoạch lại chính xác từng sự kiện lắng nghe để tránh việc copy-paste mã lệnh bị trùng lặp biến.
**Giải pháp dải giá trị lớn:** Người dùng nhìn thấy và kéo trượt trên thang đo nguyên trực quan từ `-100` đến `+100` (để thấy tác động lớn, rõ rệt trên UI). Tuy nhiên, trước khi gán dữ liệu vào hàm `applyLiveFilterToCamera` để GPU xử lý, ta phải **chia cho 100** để đưa về dải số thực chuẩn hóa (`-1.0` đến `1.0`) giúp lõi phần cứng đồ họa không bị lỗi tính toán (vì Shader/CoreImage nhận giá trị số thực từ `-1` đến `1`).
```kotlin
private fun setupSliderListeners() {
// FIX 1: Chuẩn hóa gán đúng hàm cho Brightness
binding.sliderBrightness.addOnChangeListener { _, value, _ ->
// Hiển thị số nguyên (-100 đến 100) lên nhãn văn bản cho người dùng thấy
binding.tvLabelBrightness.text = String.format("Brightness (Độ sáng): %.0f", value)
// Quy đổi tỉ lệ về dải số thực (-1.0 đến 1.0) để gán cho Object cấu hình
val normalizedValue = value / 100f
editingPreset?.basic?.brightness = normalizedValue
// Đẩy giá trị ĐÚNG của Brightness sang bộ lọc GPU
applyLiveFilterToCamera(editingPreset)
}
// FIX 2: Chuẩn hóa gán đúng hàm cho Temperature (Không bị nhầm với Brightness nữa)
binding.sliderTemperature.addOnChangeListener { _, value, _ ->
binding.tvLabelTemperature.text = String.format("Temperature (Nhiệt màu): %.0f", value)
val normalizedValue = value / 100f
editingPreset?.basic?.temperature = normalizedValue // Gán chuẩn xác cho biến temperature
applyLiveFilterToCamera(editingPreset)
}
// FIX 3: Chuẩn hóa gán đúng hàm cho Vibrance
binding.sliderVibrance.addOnChangeListener { _, value, _ ->
binding.tvLabelVibrance.text = String.format("Vibrance (Bão hòa thông minh): %.0f", value)
val normalizedValue = value / 100f
editingPreset?.basic?.vibrance = normalizedValue // Gán chuẩn xác cho biến vibrance
applyLiveFilterToCamera(editingPreset)
}
// FIX 4: Chuẩn hóa cho hiệu ứng nhiễu hạt Film Grain (Dải giá trị từ 0 đến 100)
binding.sliderGrainAmount.addOnChangeListener { _, value, _ ->
binding.tvLabelGrainAmount.text = String.format("Grain Amount (Mật độ hạt): %.0f%%", value)
val normalizedValue = value / 100f
editingPreset?.grain?.grainAmount = normalizedValue // Gán chuẩn xác cho biến grainAmount
applyLiveFilterToCamera(editingPreset)
}
// Tiến hành rà soát lại tất cả các khối listener còn lại đảm bảo đúng biến nào - hàm đó!
}
```
---
## 🔄 3. Cơ Chế Nạp Lại Giá Trị Gốc Lên Sliders (Quy Đổi Ngược Nhân 100)
Khi người dùng mở bảng chỉnh sửa, chúng ta phải làm một phép tính ngược lại: Lấy giá trị số thực từ tệp JSON, **nhân với 100** để hiển thị đúng vị trí con trỏ trên thanh kéo `-100 -> +100`.
```kotlin
fun openAdvancedSliders(selectedPreset: ColorPreset) {
editingPreset = selectedPreset.deepCopy()
binding.tvEditPresetName.text = "Chỉnh sửa: ${editingPreset!!.name}"
// Đọc giá trị số thực từ JSON, nhân 100 để hiển thị dải số nguyên rõ rệt trên thanh trượt
binding.sliderBrightness.value = editingPreset!!.basic.brightness * 100f
binding.sliderTemperature.value = editingPreset!!.basic.temperature * 100f
binding.sliderVibrance.value = editingPreset!!.basic.vibrance * 100f
binding.sliderClarity.value = editingPreset!!.advanced.clarity * 100f
binding.sliderDehaze.value = editingPreset!!.advanced.dehaze * 100f
binding.sliderGrainAmount.value = editingPreset!!.grain.grainAmount * 100f
binding.sliderGrainSize.value = editingPreset!!.grain.grainSize * 100f
// Cập nhật text nhãn ban đầu
binding.tvLabelBrightness.text = String.format("Brightness (Độ sáng): %.0f", binding.sliderBrightness.value)
binding.tvLabelTemperature.text = String.format("Temperature (Nhiệt màu): %.0f", binding.sliderTemperature.value)
setupSliderListeners()
binding.cardAdvancedSliders.visibility = View.VISIBLE
}
```
---
## 📅 4. Các Đầu Việc Cần Hoàn Thành (Checklist)
* [ ] Thực hiện thay đổi cấu trúc `ScrollView` trong tệp XML để cô lập hoàn toàn nội dung cuộn, giải quyết triệt để lỗi tràn khung hiển thị.
* [ ] Tiến hành cập nhật lại toàn bộ 14 hàm lắng nghe sự kiện `addOnChangeListener`, rà soát kỹ lưỡng từng dòng lệnh để đảm bảo tính độc lập tuyệt đối giữa các thông số.
* [ ] Chạy thử nghiệm kéo trượt mạnh về hai cực đầu mút (`-100``100`) trên thiết bị thật để kiểm tra độ trễ phản hồi hình ảnh và biên độ thay đổi của màu ảnh xem đã đủ rõ rệt chưa.
+87
View File
@@ -0,0 +1,87 @@
# 🛠️ KẾ HOẠCH SỬA LỖI ĐỒ HỌA: CHUẨN HÓA THUẬT TOÁN ĐỔI MÀU THEO KÊNH
Mục tiêu: Quy hoạch lại công thức toán học tính toán 4 chỉ số `alpha`, `r`, `g`, `b` để đảm bảo:
* **Brightness:** Phải tăng/giảm đều cả 3 kênh R, G, B (độ sáng tổng thể).
* **Temperature:** Chỉ dịch chuyển cân bằng giữa kênh R (Ấm) và kênh B (Lạnh).
* **Tint:** Chỉ dịch chuyển cân bằng kênh G (Xanh lá) và tổ hợp R+B (Hồng dâu).
* **Alpha (Độ đậm bộ lọc):** Được điều tiết một cách thông minh bằng Saturation, Vibrance, Contrast, Clarity và Dehaze để tăng giảm độ dày của lớp phủ màu Instax.
---
## 💻 1. Cấu Trúc Mã Nguồn Sau Khi Sửa Đổi (Kotlin)
Hãy thay thế toàn bộ đoạn tính toán màu ARGB cũ bằng khối thuật toán chuẩn hóa và loại bỏ hoàn toàn các dòng khai báo trùng lặp:
```kotlin
val color = if (preset.isEditable) {
// 1. ALPHA (Độ đậm/Mờ của lớp filter): Được quyết định bởi Saturation, Vibrance và hiệu ứng nâng cao
// Quy đổi dải từ -1.0 -> 1.0 sang mức độ trong suốt hợp lý (từ 15 đến 140 trong hệ 255)
val baseAlpha = 60f
val saturationImpact = (preset.basic.saturation + preset.basic.vibrance) * 25f
val contrastImpact = preset.basic.contrast * 15f
val alpha = (baseAlpha + saturationImpact + contrastImpact).toInt().coerceIn(15, 140)
// 2. BASE COLOR (Màu nền mặc định ban đầu trước khi tinh chỉnh)
// Bắt đầu từ một màu trung tính hoặc lấy màu gốc của Theme nếu có
var baseR = 140f
var baseG = 130f
var baseB = 120f
// 3. BRIGHTNESS (Độ sáng): Bắt buộc phải cộng/trừ ĐỀU vào cả 3 kênh R, G, B để dịch chuyển Exposure
val brightnessOffset = preset.basic.brightness * 80f // Nhân hệ số để biên độ thay đổi từ -100 đến +100 rõ rệt
baseR += brightnessOffset
baseG += brightnessOffset
baseB += brightnessOffset
// 4. TEMPERATURE (Nhiệt màu): + là Ấm (Tăng Đỏ, Giảm Lam), - là Lạnh (Giảm Đỏ, Tăng Lam)
val tempOffset = preset.basic.temperature * 50f
baseR += tempOffset
baseB -= tempOffset
// 5. TINT (Sắc độ): + là Ám hồng (Tăng Đỏ & Lam, Giảm Lục), - là Ám xanh (Giảm Đỏ & Lam, Tăng Lục)
val tintOffset = preset.basic.tint * 40f
baseG -= tintOffset
baseR += (tintOffset * 0.5f)
baseB += (tintOffset * 0.5f)
// 6. TONE CURVE & SHADOWS TINT (Ám màu vùng tối đặc trưng của Instax)
baseR += (preset.toneCurve.shadowsTintR * 35f)
baseG += (preset.toneCurve.shadowsTintG * 35f)
baseB += (preset.toneCurve.shadowsTintB * 35f)
// Nâng đáy màu đen (Faded Black) tạo độ sờn ảnh
val fadeOffset = preset.toneCurve.fadedBlackLevel * 30f
baseR += fadeOffset
baseG += fadeOffset
baseB += fadeOffset
// 7. CLARITY & DEHAZE (Điều tiết thêm độ tương phản cục bộ của màu)
val advancedOffset = (preset.advanced.clarity - preset.advanced.dehaze) * 20f
baseR += advancedOffset
baseG += advancedOffset
baseB += advancedOffset
// 8. ÉP BIÊN ĐỘ (COERCE) để đảm bảo giá trị RGB luôn nằm trong giới hạn vật lý 0 -> 255
val r = baseR.toInt().coerceIn(0, 255)
val g = baseG.toInt().coerceIn(0, 255)
val b = baseB.toInt().coerceIn(0, 255)
android.graphics.Color.argb(alpha, r, g, b)
} else {
// Khối xử lý cho các mẫu mặc định cố định không sửa đổi
when (preset.id) {
"instax_faded_warm_01" -> android.graphics.Color.argb(55, 255, 152, 0)
"instax_bw_cool" -> android.graphics.Color.argb(80, 128, 128, 128)
"instax_cool_summer" -> android.graphics.Color.argb(45, 0, 188, 212)
else -> android.graphics.Color.argb(0, 0, 0, 0)
}
}
```
---
+163
View File
@@ -0,0 +1,163 @@
# 🎨 KẾ HOẠCH THAY ĐỔI UX: GIẢI PHÓNG TẦM NHÌN VỚI THÀNH TRƯỢT THƯỚC ĐO TỐI GIẢN
Mục tiêu: Loại bỏ hoàn toàn khối Card View mờ lọt lòng cũ. Khi người dùng nhấn giữ một Preset (hoặc nhấn nút Edit nhỏ), khu vực điều khiển camera phía dưới sẽ biến đổi thành thanh tinh chỉnh thông số 3 tầng dạng nén tối giản bám sát theo sơ đồ ảnh `image.png`.
---
## 📐 1. Tái Cấu Trúc Lại XML Layout Hạ Tầng (`activity_main.xml`)
Bảng Sliders cũ sẽ bị xóa bỏ. Thay vào đó, ta tạo một cụm layout tinh chỉnh mới có tên `layoutTimelineEditor`. Mặc định cụm này sẽ ẩn đi (`android:visibility="gone"`). Khi kích hoạt, nó sẽ đè lên vị trí của bảng chọn Frame/Preset và Control Panel cũ.
```xml
<!-- KHU VỰC ĐIỀU KHIỂN CHỈNH SỬA ẢNH (Thay thế Control Panel khi nhấn giữ preset) -->
<ConstraintLayout
android:id="@+id/layoutTimelineEditor"
android:layout_width="match_parent"
android:layout_height="180dp"
android:background="#000000"
android:visibility="gone"
app:layout_bottom_toBottomOf="parent">
<!-- TẦNG 1: THANH TRƯỢT THƯỚC ĐO ĐỘC ĐÁO (RULER SLIDER) -->
<!-- Hiển thị vạch chia số và kim chỉ vị trí trung tâm 0 như ảnh image_44676a.png -->
<FrameLayout
android:id="@+id/rulerContainer"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent">
<!-- Các vạch thước kẻ nhỏ (Có thể tự vẽ bằng Custom View hoặc dùng ảnh lặp) -->
<ImageView
android:id="@+id/imgRulerTicks"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/bg_ruler_ticks"
android:scaleType="centerCrop" />
<!-- Kim chỉ số 0 màu cam chính giữa cố định -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center">
<TextView
android:id="@+id/tvRulerValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#FF9800"
android:textSize="12sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"/>
<ImageView
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/ic_arrow_down_orange"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</FrameLayout>
<!-- TẦNG 2: PHẦN HIỂN THỊ ICON CÁC THUỘC TÍNH (HORIZONTAL LIST) -->
<!-- Chứa danh sách cuộn ngang các nút: Brightness, Contrast, Saturation... -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAttributeIcons"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/rulerContainer"
app:layout_constraintBottom_toTopOf="@id/layoutEditorActionButtons" />
<!-- TẦNG 3: THANH THAO TÁC HOÀN THÀNH (HỦY / ĐỒNG Ý) -->
<RelativeLayout
android:id="@+id/layoutEditorActionButtons"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
app:layout_constraintBottom_toBottomOf="parent">
<!-- Nút X (Hủy bỏ các thông số vừa chỉnh) -->
<ImageButton
android:id="@+id/btnEditorClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@null"
android:src="@drawable/ic_close_white"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"/>
<!-- Nút Dấu Tích (Đồng ý để lưu preset) -->
<ImageButton
android:id="@+id/btnEditorCheck"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@null"
android:src="@drawable/ic_check_white"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"/>
</RelativeLayout>
</ConstraintLayout>
```
---
## 💻 2. Kịch Bản Thao Tác Cử Chỉ Động (Kotlin Logic)
Hệ thống điều khiển cử chỉ mới sẽ hoạt động dựa trên hai tương tác chính: **Chọn thuộc tính****Bắt sự kiện vuốt trên ảnh**.
### 🔹 2.1. Quản lý Thuộc tính được Chọn (Tầng 2)
Tạo một biến Enum để nhận biết người dùng đang nhấn vào thuộc tính nào trong `rvAttributeIcons`:
```kotlin
enum class EditAttribute { BRIGHTNESS, CONTRAST, SATURATION, VIBRANCE, TEMPERATURE }
private var currentAttribute = EditAttribute.BRIGHTNESS // Mặc định là độ sáng
```
Khi người dùng bấm chọn một icon trong danh sách, icon đó sẽ **đổi sang màu cam** để thông báo trạng thái hoạt động, đồng thời nạp giá trị hiện tại của thuộc tính đó lên kim chỉ thước đo `tvRulerValue`.
### 🔹 2.2. Cơ chế Swipe trên ảnh để đổi giá trị (Gesture Detector)
Đính kèm một bộ lắng nghe cử chỉ vuốt chạm (`OnTouchListener`) trực tiếp vào khung hình Camera chính (`cameraContainer`).
1. **Bước 1: Bắt sự kiện Touch:** Chạm màn hình.
Người dùng đặt ngón tay lên vùng hiển thị ảnh và vuốt sang trái hoặc phải. Hệ thống ghi lại điểm chạm đầu tiên `startX`.
2. **Bước 2: Quy đổi độ dời sang giá trị số:** Tính khoảng cách.
Khi ngón tay di chuyển, tính toán khoảng cách chênh lệch `deltaX = currentX - startX`. Quy đổi khoảng cách pixel này sang dải giá trị nguyên từ `-100` đến `+100`.
3. **Bước 3: Dịch chuyển vạch thước kẻ & Áp bộ lọc:** Cập nhật đồ họa.
Dựa vào giá trị `deltaX`:
* Di chuyển tịnh tiến hình ảnh vạch thước `imgRulerTicks` sang trái hoặc phải để tạo cảm giác trượt thực tế.
* Cập nhật số trên kim chỉ `tvRulerValue`.
* Gán giá trị mới vào thuộc tính đang chọn (ví dụ: `editingPreset.basic.brightness`) và ra lệnh cho GPU render cập nhật màu sắc ngay lập tức trên ảnh.
---
## 💾 3. Lưu Preset Với Tên Mới Khi Ấn Đồng Ý
Khi người dùng nhấn vào nút **Dấu tích (Đồng ý để lưu preset)** ở góc dưới bên phải màn hình chỉnh sửa:
1. App sẽ hiển thị một thông báo popup (Dialog) gọn gàng hỏi tên bộ lọc mới.
2. Tiến hành đóng gói file JSON chứa toàn bộ 14 thông số hoàn chỉnh.
3. Ẩn thanh `layoutTimelineEditor`, mở lại giao diện Control Panel chụp ảnh thông thường, đồng thời tải lại thanh timeline chính để preset mới xuất hiện ngay lập tức.
---
## 📅 4. Các Đầu Việc Cần Làm Ngay (Checklist)
* [ ] **Chuẩn bị Icon Đơn Sắc:** Tìm và sử dụng các icon vector SVG tiêu chuẩn và tối giản cho nút Độ sáng (Mặt trời), Tương phản (Hình tròn chia đôi), Bão hòa (Vòng tròn màu)... để sử dụng cho các nút
* [ ] **Thiết lập Custom View cho Thước Đo:** Code logic dịch chuyển tọa độ X (`translationX`) của hình ảnh vạch thước `imgRulerTicks` đồng bộ theo tỷ lệ ngón tay vuốt trên màn hình.
* [ ] **Gán cử chỉ gỡ lỗi:** Đảm bảo khi người dùng thao tác vuốt trên ảnh, các cử chỉ của hệ thống camera (như zoom bằng 2 ngón tay) tạm thời bị khóa lại để không làm nhiễu loạn quá trình chỉnh màu.
+151
View File
@@ -0,0 +1,151 @@
# 🎨 KẾ HOẠCH BỔ SUNG THUỘC TÍNH EDITOR & HIỂN THỊ TÊN ĐỘNG TRÊN TOP BAR
Mục tiêu: Đưa toàn bộ 14 thông số cấu hình vào `rvAttributeIcons` dạng danh sách cuộn ngang không giới hạn. Đồng bộ hóa việc chọn icon để hiển thị tên tiếng Việt tương ứng lên vùng văn bản phía trên ảnh và gán chính xác sự kiện vuốt trên ảnh cho thuộc tính đó.
---
## 📐 1. Cấu Trúc Lại XML Layout Để Thêm Dải Hiển Thị Tên (`activity_main.xml`)
Bám sát ảnh `image.png`, chúng ta cần thêm một `TextView` nằm ở dải đen phía trên bức ảnh để làm nhiệm vụ hiển thị tên nút đang chọn.
```xml
<!-- DẢI ĐEN PHÍA TRÊN ẢNH CAMERA (Như mô tả trong image.png) -->
<RelativeLayout
android:id="@+id/layoutEditorTitleBar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#000000"
app:layout_bottom_toTopOf="@id/cameraContainer"
app:layout_constraintTop_toBottomOf="@id/topBar">
<!-- Văn bản hiển thị động tên nút đang được chọn -->
<TextView
android:id="@+id/tvCurrentAttributeName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Brightness (Độ sáng)" <!-- Tên đng hin th đây -->
android:textColor="#FF9800" <!-- Màu cam nổi bật hoặc trắng tùy chọn -->
android:textSize="14sp"
android:textStyle="bold" />
</RelativeLayout>
```
---
## 📁 2. Khai Báo Toàn Bộ Bản Đồ Thuộc Tính & Icons (Kotlin Model)
Để quản lý 14 nút thuộc tính một cách khoa học, chúng ta tạo một danh sách Model bao gồm: ID thuộc tính, Tên hiển thị (Tiếng Việt), và File định dạng Icon tương ứng trong `res/drawable`.
### 🔹 2.1. Định nghĩa Data Class cho nút thuộc tính
```kotlin
data class AttributeNode(
val type: EditAttribute,
val displayName: String,
val iconResId: Int
)
enum class EditAttribute {
BRIGHTNESS, CONTRAST, SATURATION, VIBRANCE, TEMPERATURE, TINT, // Nhóm 1: Cơ bản
CLARITY, DEHAZE, VIGNETTE_AMOUNT, // Nhóm 2: Nâng cao
FADED_BLACK_LEVEL, SHADOWS_TINT_R, SHADOWS_TINT_G, SHADOWS_TINT_B, // Nhóm 3: Tone Curve
GRAIN_AMOUNT, GRAIN_SIZE // Nhóm 4: Nhiễu hạt
}
```
### 🔹 2.2. Khởi tạo mảng dữ liệu 14 nút đầy đủ
```kotlin
val editorAttributes = listOf(
AttributeNode(EditAttribute.BRIGHTNESS, "Brightness (Độ sáng)", R.drawable.ic_attr_brightness),
AttributeNode(EditAttribute.CONTRAST, "Contrast (Tương phản)", R.drawable.ic_attr_contrast),
AttributeNode(EditAttribute.SATURATION, "Saturation (Bão hòa màu)", R.drawable.ic_attr_saturation),
AttributeNode(EditAttribute.VIBRANCE, "Vibrance (Bão hòa thông minh)", R.drawable.ic_attr_vibrance),
AttributeNode(EditAttribute.TEMPERATURE, "Temperature (Nhiệt màu)", R.drawable.ic_attr_temperature),
AttributeNode(EditAttribute.TINT, "Tint (Sắc độ hồng/xanh)", R.drawable.ic_attr_tint),
AttributeNode(EditAttribute.CLARITY, "Clarity (Độ rõ nét)", R.drawable.ic_attr_clarity),
AttributeNode(EditAttribute.DEHAZE, "Dehaze (Độ trong suốt / Sương mù)", R.drawable.ic_attr_dehaze),
AttributeNode(EditAttribute.VIGNETTE_AMOUNT, "Vignette Amount (Bo tối góc)", R.drawable.ic_attr_vignette),
AttributeNode(EditAttribute.FADED_BLACK_LEVEL, "Faded Black Level (Độ mờ vùng đen)", R.drawable.ic_attr_fade),
AttributeNode(EditAttribute.SHADOWS_TINT_R, "Shadows Tint R (Sắc Đỏ vùng tối)", R.drawable.ic_attr_tint_r),
AttributeNode(EditAttribute.SHADOWS_TINT_G, "Shadows Tint G (Sắc Lục vùng tối)", R.drawable.ic_attr_tint_g),
AttributeNode(EditAttribute.SHADOWS_TINT_B, "Shadows Tint B (Sắc Lam vùng tối)", R.drawable.ic_attr_tint_b),
AttributeNode(EditAttribute.GRAIN_AMOUNT, "Grain Amount (Mật độ hạt)", R.drawable.ic_attr_grain_amount),
AttributeNode(EditAttribute.GRAIN_SIZE, "Grain Size (Kích thước hạt)", R.drawable.ic_attr_grain_size)
)
```
---
## 💻 3. Xử Lý Logic Tương Tác & Đồng Bộ Trạng Thái Động
Khi gán danh sách này vào `rvAttributeIcons` dưới dạng LayoutManager cuộn ngang (`LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)`), người dùng có thể thoải mái vuốt lướt sang phải để tìm các nút bị ẩn như mũi tên chỉ dẫn trong ảnh `image.png`.
### 🔹 3.1. Sự kiện Click đổi màu và đồng bộ Tên lên Top Bar
Trong sự kiện bấm chọn một Icon thuộc tính bên trong Adapter của RecyclerView:
```kotlin
fun onAttributeSelected(selectedNode: AttributeNode) {
// 1. Lưu lại thuộc tính đang chỉnh sửa hiện tại
currentAttribute = selectedNode.type
// 2. Cập nhật văn bản lên dải đen phía trên ảnh đúng theo thiết kế image_44ee6b.jpg
binding.tvCurrentAttributeName.text = selectedNode.displayName
// 3. Đọc giá trị hiện tại của thuộc tính này trong file JSON tạm thời để nạp lên thước đo
val currentValue = getCurrentAttributeValue(selectedNode.type)
binding.tvRulerValue.text = String.format("%.0f", currentValue * 100f)
// 4. Ra lệnh cho Adapter vẽ lại (Notify) để đổi màu Icon được chọn thành MÀU CAM
attributeAdapter.updateSelection(selectedNode.type)
}
```
### 🔹 3.2. Cập nhật hàm điều phối Vuốt trên ảnh (Swipe Logic Matrix)
Hàm tiếp nhận giá trị `deltaX` khi người dùng vuốt trên màn hình ảnh giờ đây sẽ phân nhánh chính xác sang cả 14 thông số, không còn tình trạng gán nhầm hàm:
```kotlin
private fun updateAttributeBySwipe(newValue: Float) {
// newValue có dải giá trị từ -1.0f đến 1.0f sau khi đã chia cho 100
when (currentAttribute) {
EditAttribute.BRIGHTNESS -> editingPreset?.basic?.brightness = newValue
EditAttribute.CONTRAST -> editingPreset?.basic?.contrast = newValue
EditAttribute.SATURATION -> editingPreset?.basic?.saturation = newValue
EditAttribute.VIBRANCE -> editingPreset?.basic?.vibrance = newValue
EditAttribute.TEMPERATURE -> editingPreset?.basic?.temperature = newValue
EditAttribute.TINT -> editingPreset?.basic?.tint = newValue
EditAttribute.CLARITY -> editingPreset?.advanced?.clarity = newValue
EditAttribute.DEHAZE -> editingPreset?.advanced?.dehaze = newValue
EditAttribute.VIGNETTE_AMOUNT -> editingPreset?.advanced?.vignetteAmount = newValue.coerceIn(0f, 1f)
EditAttribute.FADED_BLACK_LEVEL -> editingPreset?.toneCurve?.fadedBlackLevel = newValue.coerceIn(0f, 1f)
EditAttribute.SHADOWS_TINT_R -> editingPreset?.toneCurve?.shadowsTintR = newValue.coerceIn(0f, 1f)
EditAttribute.SHADOWS_TINT_G -> editingPreset?.toneCurve?.shadowsTintG = newValue.coerceIn(0f, 1f)
EditAttribute.SHADOWS_TINT_B -> editingPreset?.toneCurve?.shadowsTintB = newValue.coerceIn(0f, 1f)
EditAttribute.GRAIN_AMOUNT -> editingPreset?.grain?.grainAmount = newValue.coerceIn(0f, 1f)
EditAttribute.GRAIN_SIZE -> editingPreset?.grain?.grainSize = newValue.coerceIn(0f, 1f)
}
// Luôn luôn gọi render cập nhật đồ họa thời gian thực lên Camera ngay lập tức
applyLiveFilterToCamera(editingPreset)
}
```
---
## 📅 4. Các Đầu Việc Cần Triển Khai (Checklist)
* [ ] **Chuẩn bị 14 cặp Icons tương ứng:** Tạo hoặc import các asset dạng vector đơn sắc trắng vào thư mục dự án trên Linux.
* [ ] **Cấu hình `clipToPadding="false"` cho RecyclerView:** Thiết lập thuộc tính này cho thanh icon để khi cuộn, các icon ở hai đầu rìa có khoảng không trượt mượt mà, không bị cắt cụt đột ngột.
* [ ] **Kiểm thử liên kết:** Đảm bảo khi mở màn hình Editor lên, mặc định app sẽ tự động giả lập click vào nút đầu tiên (Brightness), icon sáng cam và dải chữ phía trên hiển thị đúng chữ `"Brightness (Độ sáng)"` như ảnh `image.png`.
@@ -0,0 +1,78 @@
package com.photobooth.app
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
enum class EditAttribute {
BRIGHTNESS, CONTRAST, SATURATION, VIBRANCE, TEMPERATURE, TINT, // Nhóm 1: Cơ bản
CLARITY, DEHAZE, VIGNETTE_AMOUNT, // Nhóm 2: Nâng cao
FADED_BLACK_LEVEL, SHADOWS_TINT_R, SHADOWS_TINT_G, SHADOWS_TINT_B, // Nhóm 3: Tone Curve
GRAIN_AMOUNT, GRAIN_SIZE // Nhóm 4: Nhiễu hạt
}
data class AttributeNode(
val type: EditAttribute,
val displayName: String,
val iconResId: Int
)
class AttributeAdapter(
private val items: List<AttributeNode>,
private val onItemSelected: (AttributeNode) -> Unit
) : RecyclerView.Adapter<AttributeAdapter.ViewHolder>() {
var selectedPosition = 0
set(value) {
val old = field
field = value
notifyItemChanged(old)
notifyItemChanged(value)
}
fun updateSelection(selectedType: EditAttribute) {
val index = items.indexOfFirst { it.type == selectedType }
if (index != -1) {
selectedPosition = index
}
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imgIcon: ImageView = view.findViewById(R.id.imgAttributeIcon)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_attribute_icon, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.imgIcon.setImageResource(item.iconResId)
// If selected, color is orange, else white
if (position == selectedPosition) {
holder.imgIcon.setColorFilter(Color.parseColor("#FF9800"))
} else {
// Keep original vector coloring if it is colored droplet, else white tint
if (item.type == EditAttribute.SHADOWS_TINT_R ||
item.type == EditAttribute.SHADOWS_TINT_G ||
item.type == EditAttribute.SHADOWS_TINT_B) {
holder.imgIcon.clearColorFilter()
} else {
holder.imgIcon.setColorFilter(Color.parseColor("#FFFFFF"))
}
}
holder.itemView.setOnClickListener {
selectedPosition = holder.adapterPosition
onItemSelected(item)
}
}
override fun getItemCount(): Int = items.size
}
@@ -45,6 +45,11 @@ class MainActivity : AppCompatActivity() {
private var currentSelectedPreset: ColorPreset? = null private var currentSelectedPreset: ColorPreset? = null
private var editingPreset: ColorPreset? = null private var editingPreset: ColorPreset? = null
private var currentAttribute: EditAttribute = EditAttribute.BRIGHTNESS
private lateinit var attributeAdapter: AttributeAdapter
private var startX = 0f
private var startVal = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
@@ -194,6 +199,38 @@ class MainActivity : AppCompatActivity() {
}, { preset -> }, { preset ->
openAdvancedSliders(preset) openAdvancedSliders(preset)
}) })
// Setup Attributes List and Ruler View for the editor panel
val attributeItems = listOf(
AttributeNode(EditAttribute.BRIGHTNESS, "Brightness (Độ sáng)", R.drawable.ic_attr_brightness),
AttributeNode(EditAttribute.CONTRAST, "Contrast (Tương phản)", R.drawable.ic_attr_contrast),
AttributeNode(EditAttribute.SATURATION, "Saturation (Bão hòa màu)", R.drawable.ic_attr_saturation),
AttributeNode(EditAttribute.VIBRANCE, "Vibrance (Bão hòa thông minh)", R.drawable.ic_attr_vibrance),
AttributeNode(EditAttribute.TEMPERATURE, "Temperature (Nhiệt màu)", R.drawable.ic_attr_temperature),
AttributeNode(EditAttribute.TINT, "Tint (Sắc độ hồng/xanh)", R.drawable.ic_attr_tint),
AttributeNode(EditAttribute.CLARITY, "Clarity (Độ rõ nét)", R.drawable.ic_attr_clarity),
AttributeNode(EditAttribute.DEHAZE, "Dehaze (Độ trong suốt / Sương mù)", R.drawable.ic_attr_dehaze),
AttributeNode(EditAttribute.VIGNETTE_AMOUNT, "Vignette Amount (Bo tối góc)", R.drawable.ic_attr_vignette),
AttributeNode(EditAttribute.FADED_BLACK_LEVEL, "Faded Black Level (Độ mờ vùng đen)", R.drawable.ic_attr_fade),
AttributeNode(EditAttribute.SHADOWS_TINT_R, "Shadows Tint R (Sắc Đỏ vùng tối)", R.drawable.ic_attr_tint_r),
AttributeNode(EditAttribute.SHADOWS_TINT_G, "Shadows Tint G (Sắc Lục vùng tối)", R.drawable.ic_attr_tint_g),
AttributeNode(EditAttribute.SHADOWS_TINT_B, "Shadows Tint B (Sắc Lam vùng tối)", R.drawable.ic_attr_tint_b),
AttributeNode(EditAttribute.GRAIN_AMOUNT, "Grain Amount (Mật độ hạt)", R.drawable.ic_attr_grain_amount),
AttributeNode(EditAttribute.GRAIN_SIZE, "Grain Size (Kích thước hạt)", R.drawable.ic_attr_grain_size)
)
attributeAdapter = AttributeAdapter(attributeItems) { node ->
onAttributeSelected(node)
}
binding.rvAttributeIcons.layoutManager = LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, false
)
binding.rvAttributeIcons.adapter = attributeAdapter
binding.rulerView.onValueChangeListener = { valInt ->
binding.tvRulerValue.text = "$valInt"
setCurrentAttributeValue(valInt / 100f)
editingPreset?.let { applyPresetFilter(it) }
}
} }
private fun setFrameOverlay(fileName: String) { private fun setFrameOverlay(fileName: String) {
@@ -217,48 +254,142 @@ class MainActivity : AppCompatActivity() {
currentSelectedPreset = preset currentSelectedPreset = preset
if (preset.id == "DEFAULT") { if (preset.id == "DEFAULT") {
binding.imgFilterOverlay.visibility = android.view.View.GONE binding.imgFilterOverlay.visibility = android.view.View.GONE
binding.viewFinder.setLayerType(android.view.View.LAYER_TYPE_NONE, null)
} else { } else {
binding.imgFilterOverlay.visibility = android.view.View.VISIBLE binding.imgFilterOverlay.visibility = android.view.View.VISIBLE
val isBeingEdited = editingPreset != null val isBeingEdited = editingPreset != null
val filterColor = if (isBeingEdited) { val useCalculation = isBeingEdited || (preset.id != "instax_faded_warm_01" && preset.id != "instax_bw_cool" && preset.id != "instax_cool_summer")
// Sophisticated ARGB mapping to simulate film filters using adjustment parameters val filterColor = if (useCalculation) {
// Vibrance and Saturation increase opacity of the filter color // 1. ALPHA (Độ đậm/Mờ của lớp filter): Được quyết định bởi Saturation, Vibrance và hiệu ứng nâng cao
val alpha = (((preset.basic.vibrance + preset.basic.saturation) / 2f + 1.0f) * 40 + 20).toInt().coerceIn(10, 120) // Quy đổi dải từ -1.0 -> 1.0 sang mức độ trong suốt hợp lý (từ 15 đến 140 trong hệ 255)
val baseAlpha = 60f
val saturationImpact = (preset.basic.saturation + preset.basic.vibrance) * 25f
val contrastImpact = preset.basic.contrast * 15f
val alpha = (baseAlpha + saturationImpact + contrastImpact).toInt().coerceIn(15, 140)
// Temperature (+ is warm/red-yellow), Brightness, Clarity, and ShadowsTintR affect red // 2. BASE COLOR (Màu nền mặc định ban đầu trước khi tinh chỉnh)
val tempWarmth = if (preset.basic.temperature > 0) preset.basic.temperature * 50f else 0f // Bắt đầu từ một màu trung tính hoặc lấy màu gốc của Theme nếu có
val r = (((preset.advanced.clarity + preset.basic.brightness) / 2f + 1.0f) * 100 + 30 + tempWarmth + (preset.toneCurve.shadowsTintR * 30)).toInt().coerceIn(0, 255) var baseR = 140f
var baseG = 130f
var baseB = 120f
// Contrast, Tint (- is green), and ShadowsTintG affect green // 3. BRIGHTNESS (Độ sáng): Bắt buộc phải cộng/trừ ĐỀU vào cả 3 kênh R, G, B để dịch chuyển Exposure
val tintGreen = if (preset.basic.tint < 0) -preset.basic.tint * 50f else 0f val brightnessOffset = preset.basic.brightness * 80f // Nhân hệ số để biên độ thay đổi từ -100 đến +100 rõ rệt
val g = ((preset.basic.contrast + 1.0f) * 90 + 20 + tintGreen + (preset.toneCurve.shadowsTintG * 30)).toInt().coerceIn(0, 255) baseR += brightnessOffset
baseG += brightnessOffset
baseB += brightnessOffset
// Dehaze, cool Temperature (- is blue), and ShadowsTintB affect blue // 4. TEMPERATURE (Nhiệt màu): + là Ấm (Tăng Đỏ, Giảm Lam), - là Lạnh (Giảm Đỏ, Tăng Lam)
val tempCoolness = if (preset.basic.temperature < 0) -preset.basic.temperature * 60f else 0f val tempOffset = preset.basic.temperature * 50f
val b = (((preset.advanced.dehaze + 1.0f) * 80) + 20 + tempCoolness + (preset.toneCurve.shadowsTintB * 30)).toInt().coerceIn(0, 255) baseR += tempOffset
baseB -= tempOffset
// 5. TINT (Sắc độ): + là Ám hồng (Tăng Đỏ & Lam, Giảm Lục), - là Ám xanh (Giảm Đỏ & Lam, Tăng Lục)
val tintOffset = preset.basic.tint * 40f
baseG -= tintOffset
baseR += (tintOffset * 0.5f)
baseB += (tintOffset * 0.5f)
// 6. TONE CURVE & SHADOWS TINT (Ám màu vùng tối đặc trưng của Instax)
baseR += (preset.toneCurve.shadowsTintR * 35f)
baseG += (preset.toneCurve.shadowsTintG * 35f)
baseB += (preset.toneCurve.shadowsTintB * 35f)
// Nâng đáy màu đen (Faded Black) tạo độ sờn ảnh
val fadeOffset = preset.toneCurve.fadedBlackLevel * 30f
baseR += fadeOffset
baseG += fadeOffset
baseB += fadeOffset
// 7. CLARITY & DEHAZE (Điều tiết thêm độ tương phản cục bộ của màu)
val advancedOffset = (preset.advanced.clarity - preset.advanced.dehaze) * 20f
baseR += advancedOffset
baseG += advancedOffset
baseB += advancedOffset
// 8. ÉP BIÊN ĐỘ (COERCE) để đảm bảo giá trị RGB luôn nằm trong giới hạn vật lý 0 -> 255
val r = baseR.toInt().coerceIn(0, 255)
val g = baseG.toInt().coerceIn(0, 255)
val b = baseB.toInt().coerceIn(0, 255)
android.graphics.Color.argb(alpha, r, g, b) android.graphics.Color.argb(alpha, r, g, b)
} else { } else {
// Khối xử lý cho các mẫu mặc định cố định không sửa đổi
when (preset.id) { when (preset.id) {
"instax_faded_warm_01" -> android.graphics.Color.argb(55, 255, 152, 0) "instax_faded_warm_01" -> android.graphics.Color.argb(55, 255, 152, 0)
"instax_bw_cool" -> android.graphics.Color.argb(80, 128, 128, 128) "instax_bw_cool" -> android.graphics.Color.argb(80, 128, 128, 128)
"instax_cool_summer" -> android.graphics.Color.argb(45, 0, 188, 212) "instax_cool_summer" -> android.graphics.Color.argb(45, 0, 188, 212)
else -> { else -> android.graphics.Color.argb(0, 0, 0, 0)
val alpha = (((preset.basic.vibrance + preset.basic.saturation) / 2f + 1.0f) * 40 + 20).toInt().coerceIn(10, 120)
val tempWarmth = if (preset.basic.temperature > 0) preset.basic.temperature * 50f else 0f
val r = (((preset.advanced.clarity + preset.basic.brightness) / 2f + 1.0f) * 100 + 30 + tempWarmth + (preset.toneCurve.shadowsTintR * 30)).toInt().coerceIn(0, 255)
val tintGreen = if (preset.basic.tint < 0) -preset.basic.tint * 50f else 0f
val g = ((preset.basic.contrast + 1.0f) * 90 + 20 + tintGreen + (preset.toneCurve.shadowsTintG * 30)).toInt().coerceIn(0, 255)
val tempCoolness = if (preset.basic.temperature < 0) -preset.basic.temperature * 60f else 0f
val b = (((preset.advanced.dehaze + 1.0f) * 80) + 20 + tempCoolness + (preset.toneCurve.shadowsTintB * 30)).toInt().coerceIn(0, 255)
android.graphics.Color.argb(alpha, r, g, b)
}
} }
} }
binding.imgFilterOverlay.setBackgroundColor(filterColor) binding.imgFilterOverlay.setBackgroundColor(filterColor)
applyColorMatrixToViewFinder(preset)
} }
} }
private fun applyColorMatrixToViewFinder(preset: ColorPreset) {
val finalMatrix = android.graphics.ColorMatrix()
// 1. Saturation & Vibrance (Sắc độ bão hòa màu thực tế)
val sat = (preset.basic.saturation + preset.basic.vibrance * 0.5f) + 1f
val satMatrix = android.graphics.ColorMatrix().apply {
setSaturation(sat.coerceIn(0f, 3f))
}
finalMatrix.postConcat(satMatrix)
// 2. Contrast (Tương phản thực tế)
val scale = preset.basic.contrast + 1f
val translate = (-0.5f * scale + 0.5f) * 255f
val contrastMatrix = android.graphics.ColorMatrix(floatArrayOf(
scale, 0f, 0f, 0f, translate,
0f, scale, 0f, 0f, translate,
0f, 0f, scale, 0f, translate,
0f, 0f, 0f, 1f, 0f
))
finalMatrix.postConcat(contrastMatrix)
// 3. Brightness (Độ sáng Exposure thực tế)
val brightnessOffset = preset.basic.brightness * 60f
val brightnessMatrix = android.graphics.ColorMatrix(floatArrayOf(
1f, 0f, 0f, 0f, brightnessOffset,
0f, 1f, 0f, 0f, brightnessOffset,
0f, 0f, 1f, 0f, brightnessOffset,
0f, 0f, 0f, 1f, 0f
))
finalMatrix.postConcat(brightnessMatrix)
// 4. Temperature (Nhiệt màu thực tế: Vàng ấm / Xanh lạnh)
val temp = preset.basic.temperature
val rScale = 1f + temp * 0.12f
val gScale = 1f + temp * 0.04f
val bScale = 1f - temp * 0.12f
val tempMatrix = android.graphics.ColorMatrix(floatArrayOf(
rScale, 0f, 0f, 0f, 0f,
0f, gScale, 0f, 0f, 0f,
0f, 0f, bScale, 0f, 0f,
0f, 0f, 0f, 1f, 0f
))
finalMatrix.postConcat(tempMatrix)
// 5. Tint (Sắc độ: Hồng dâu / Xanh lá)
val tint = preset.basic.tint
val rScaleTint = 1f + tint * 0.06f
val gScaleTint = 1f - tint * 0.12f
val bScaleTint = 1f + tint * 0.06f
val tintMatrix = android.graphics.ColorMatrix(floatArrayOf(
rScaleTint, 0f, 0f, 0f, 0f,
0f, gScaleTint, 0f, 0f, 0f,
0f, 0f, bScaleTint, 0f, 0f,
0f, 0f, 0f, 1f, 0f
))
finalMatrix.postConcat(tintMatrix)
val paint = android.graphics.Paint().apply {
colorFilter = android.graphics.ColorMatrixColorFilter(finalMatrix)
}
binding.viewFinder.setLayerType(android.view.View.LAYER_TYPE_HARDWARE, paint)
}
private fun updateFrameModeIcon(frame: FrameItem) { private fun updateFrameModeIcon(frame: FrameItem) {
if (frame.id == "DEFAULT" || frame.imageFileName.isEmpty()) { if (frame.id == "DEFAULT" || frame.imageFileName.isEmpty()) {
binding.btnMainFrame.setImageResource(R.drawable.ic_default_frame_thumbnail) binding.btnMainFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
@@ -341,118 +472,31 @@ class MainActivity : AppCompatActivity() {
onCaptureClick() onCaptureClick()
} }
// Advanced Sliders listeners // Camera viewfinder container swipe gesture listener to adjust parameters
binding.sliderBrightness.addOnChangeListener { _, value, _ -> binding.cameraContainer.setOnTouchListener { _, event ->
editingPreset?.let { preset -> if (binding.layoutTimelineEditor.visibility != android.view.View.VISIBLE) {
preset.basic.brightness = value return@setOnTouchListener false
binding.tvLabelBrightness.text = "Brightness (Độ sáng): ${String.format("%.2f", value)}" }
applyPresetFilter(preset) when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
startX = event.x
startVal = binding.rulerView.value
}
android.view.MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - startX
val sensitivity = 5f // pixels per unit value change
val deltaVal = (deltaX / sensitivity).toInt()
binding.rulerView.value = startVal + deltaVal
} }
} }
binding.sliderContrast.addOnChangeListener { _, value, _ -> true
editingPreset?.let { preset ->
preset.basic.contrast = value
binding.tvLabelContrast.text = "Contrast (Tương phản): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderSaturation.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.basic.saturation = value
binding.tvLabelSaturation.text = "Saturation (Bão hòa màu): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderVibrance.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.basic.vibrance = value
binding.tvLabelVibrance.text = "Vibrance (Bão hòa thông minh): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderTemperature.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.basic.temperature = value
binding.tvLabelTemperature.text = "Temperature (Nhiệt màu): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderTint.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.basic.tint = value
binding.tvLabelTint.text = "Tint (Sắc độ hồng/xanh): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderClarity.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.advanced.clarity = value
binding.tvLabelClarity.text = "Clarity (Độ rõ nét vùng trung tính): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderDehaze.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.advanced.dehaze = value
binding.tvLabelDehaze.text = "Dehaze (Độ trong suốt / Sương mù): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderVignetteAmount.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.advanced.vignetteAmount = value
binding.tvLabelVignetteAmount.text = "Vignette Amount (Bo tối góc): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderFadedBlack.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.toneCurve.fadedBlackLevel = value
binding.tvLabelFadedBlack.text = "Faded Black Level (Độ mờ vùng đen): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderShadowsTintR.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.toneCurve.shadowsTintR = value
binding.tvLabelShadowsTintR.text = "Shadows Tint R (Sắc Đỏ vùng tối): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderShadowsTintG.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.toneCurve.shadowsTintG = value
binding.tvLabelShadowsTintG.text = "Shadows Tint G (Sắc Lục vùng tối): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderShadowsTintB.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.toneCurve.shadowsTintB = value
binding.tvLabelShadowsTintB.text = "Shadows Tint B (Sắc Lam vùng tối): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderGrainAmount.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.grain.grainAmount = value
binding.tvLabelGrainAmount.text = "Grain Amount (Mật độ hạt): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderGrainSize.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.grain.grainSize = value
binding.tvLabelGrainSize.text = "Grain Size (Kích thước hạt): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
} }
binding.btnCancelEdit.setOnClickListener { binding.btnEditorClose.setOnClickListener {
closeAdvancedSliders() closeAdvancedSliders()
} }
binding.btnSaveCustomPreset.setOnClickListener { binding.btnEditorCheck.setOnClickListener {
editingPreset?.let { preset -> editingPreset?.let { preset ->
showSavePresetDialog(preset) showSavePresetDialog(preset)
} }
@@ -548,72 +592,105 @@ class MainActivity : AppCompatActivity() {
grain = selectedPreset.grain.copy() grain = selectedPreset.grain.copy()
) )
binding.tvEditPresetName.text = "Chỉnh sửa: ${selectedPreset.name}" binding.layoutEditorTitleBar.visibility = android.view.View.VISIBLE
// Update Sliders visual values // Setup initial selected attribute: Brightness
binding.sliderBrightness.value = editingPreset!!.basic.brightness val initialNode = AttributeNode(EditAttribute.BRIGHTNESS, "Brightness (Độ sáng)", R.drawable.ic_attr_brightness)
binding.sliderContrast.value = editingPreset!!.basic.contrast onAttributeSelected(initialNode)
binding.sliderSaturation.value = editingPreset!!.basic.saturation
binding.sliderVibrance.value = editingPreset!!.basic.vibrance
binding.sliderTemperature.value = editingPreset!!.basic.temperature
binding.sliderTint.value = editingPreset!!.basic.tint
binding.sliderClarity.value = editingPreset!!.advanced.clarity
binding.sliderDehaze.value = editingPreset!!.advanced.dehaze
binding.sliderVignetteAmount.value = editingPreset!!.advanced.vignetteAmount
binding.sliderFadedBlack.value = editingPreset!!.toneCurve.fadedBlackLevel
binding.sliderShadowsTintR.value = editingPreset!!.toneCurve.shadowsTintR
binding.sliderShadowsTintG.value = editingPreset!!.toneCurve.shadowsTintG
binding.sliderShadowsTintB.value = editingPreset!!.toneCurve.shadowsTintB
binding.sliderGrainAmount.value = editingPreset!!.grain.grainAmount
binding.sliderGrainSize.value = editingPreset!!.grain.grainSize
// Update text labels binding.panelSelectionContainer.visibility = android.view.View.GONE
binding.tvLabelBrightness.text = "Brightness (Độ sáng): ${String.format("%.2f", editingPreset!!.basic.brightness)}" binding.bottomControlPanel.visibility = android.view.View.GONE
binding.tvLabelContrast.text = "Contrast (Tương phản): ${String.format("%.2f", editingPreset!!.basic.contrast)}"
binding.tvLabelSaturation.text = "Saturation (Bão hòa màu): ${String.format("%.2f", editingPreset!!.basic.saturation)}"
binding.tvLabelVibrance.text = "Vibrance (Bão hòa thông minh): ${String.format("%.2f", editingPreset!!.basic.vibrance)}"
binding.tvLabelTemperature.text = "Temperature (Nhiệt màu): ${String.format("%.2f", editingPreset!!.basic.temperature)}"
binding.tvLabelTint.text = "Tint (Sắc độ hồng/xanh): ${String.format("%.2f", editingPreset!!.basic.tint)}"
binding.tvLabelClarity.text = "Clarity (Độ rõ nét vùng trung tính): ${String.format("%.2f", editingPreset!!.advanced.clarity)}"
binding.tvLabelDehaze.text = "Dehaze (Độ trong suốt / Sương mù): ${String.format("%.2f", editingPreset!!.advanced.dehaze)}"
binding.tvLabelVignetteAmount.text = "Vignette Amount (Bo tối góc): ${String.format("%.2f", editingPreset!!.advanced.vignetteAmount)}"
binding.tvLabelFadedBlack.text = "Faded Black Level (Độ mờ vùng đen): ${String.format("%.2f", editingPreset!!.toneCurve.fadedBlackLevel)}"
binding.tvLabelShadowsTintR.text = "Shadows Tint R (Sắc Đỏ vùng tối): ${String.format("%.2f", editingPreset!!.toneCurve.shadowsTintR)}"
binding.tvLabelShadowsTintG.text = "Shadows Tint G (Sắc Lục vùng tối): ${String.format("%.2f", editingPreset!!.toneCurve.shadowsTintG)}"
binding.tvLabelShadowsTintB.text = "Shadows Tint B (Sắc Lam vùng tối): ${String.format("%.2f", editingPreset!!.toneCurve.shadowsTintB)}"
binding.tvLabelGrainAmount.text = "Grain Amount (Mật độ hạt): ${String.format("%.2f", editingPreset!!.grain.grainAmount)}"
binding.tvLabelGrainSize.text = "Grain Size (Kích thước hạt): ${String.format("%.2f", editingPreset!!.grain.grainSize)}"
// Scale animation from center binding.layoutTimelineEditor.apply {
binding.cardAdvancedSliders.apply {
visibility = android.view.View.VISIBLE visibility = android.view.View.VISIBLE
alpha = 0f alpha = 0f
scaleX = 0.8f
scaleY = 0.8f
animate() animate()
.alpha(1f) .alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setDuration(250) .setDuration(250)
.setInterpolator(android.view.animation.OvershootInterpolator(1.2f))
.start() .start()
} }
} }
private fun closeAdvancedSliders() { private fun closeAdvancedSliders() {
binding.cardAdvancedSliders.animate() binding.layoutTimelineEditor.animate()
.alpha(0f) .alpha(0f)
.scaleX(0.8f)
.scaleY(0.8f)
.setDuration(200) .setDuration(200)
.withEndAction { .withEndAction {
binding.cardAdvancedSliders.visibility = android.view.View.GONE binding.layoutTimelineEditor.visibility = android.view.View.GONE
binding.layoutEditorTitleBar.visibility = android.view.View.GONE
binding.panelSelectionContainer.visibility = android.view.View.VISIBLE
binding.bottomControlPanel.visibility = android.view.View.VISIBLE
editingPreset = null editingPreset = null
currentSelectedPreset?.let { applyPresetFilter(it) } currentSelectedPreset?.let { applyPresetFilter(it) }
} }
.start() .start()
} }
private fun onAttributeSelected(selectedNode: AttributeNode) {
currentAttribute = selectedNode.type
binding.tvCurrentAttributeName.text = selectedNode.displayName
// Adjust Ruler limits based on attribute range
val isNonNegative = selectedNode.type == EditAttribute.VIGNETTE_AMOUNT ||
selectedNode.type == EditAttribute.FADED_BLACK_LEVEL ||
selectedNode.type == EditAttribute.SHADOWS_TINT_R ||
selectedNode.type == EditAttribute.SHADOWS_TINT_G ||
selectedNode.type == EditAttribute.SHADOWS_TINT_B ||
selectedNode.type == EditAttribute.GRAIN_AMOUNT ||
selectedNode.type == EditAttribute.GRAIN_SIZE
binding.rulerView.minVal = if (isNonNegative) 0 else -100
binding.rulerView.maxVal = 100
val currentVal = (getCurrentAttributeValue() * 100f).toInt()
binding.rulerView.value = currentVal
binding.tvRulerValue.text = "$currentVal"
attributeAdapter.updateSelection(selectedNode.type)
}
private fun getCurrentAttributeValue(): Float {
val preset = editingPreset ?: return 0f
return when (currentAttribute) {
EditAttribute.BRIGHTNESS -> preset.basic.brightness
EditAttribute.CONTRAST -> preset.basic.contrast
EditAttribute.SATURATION -> preset.basic.saturation
EditAttribute.VIBRANCE -> preset.basic.vibrance
EditAttribute.TEMPERATURE -> preset.basic.temperature
EditAttribute.TINT -> preset.basic.tint
EditAttribute.CLARITY -> preset.advanced.clarity
EditAttribute.DEHAZE -> preset.advanced.dehaze
EditAttribute.VIGNETTE_AMOUNT -> preset.advanced.vignetteAmount
EditAttribute.FADED_BLACK_LEVEL -> preset.toneCurve.fadedBlackLevel
EditAttribute.SHADOWS_TINT_R -> preset.toneCurve.shadowsTintR
EditAttribute.SHADOWS_TINT_G -> preset.toneCurve.shadowsTintG
EditAttribute.SHADOWS_TINT_B -> preset.toneCurve.shadowsTintB
EditAttribute.GRAIN_AMOUNT -> preset.grain.grainAmount
EditAttribute.GRAIN_SIZE -> preset.grain.grainSize
}
}
private fun setCurrentAttributeValue(value: Float) {
val preset = editingPreset ?: return
when (currentAttribute) {
EditAttribute.BRIGHTNESS -> preset.basic.brightness = value
EditAttribute.CONTRAST -> preset.basic.contrast = value
EditAttribute.SATURATION -> preset.basic.saturation = value
EditAttribute.VIBRANCE -> preset.basic.vibrance = value
EditAttribute.TEMPERATURE -> preset.basic.temperature = value
EditAttribute.TINT -> preset.basic.tint = value
EditAttribute.CLARITY -> preset.advanced.clarity = value
EditAttribute.DEHAZE -> preset.advanced.dehaze = value
EditAttribute.VIGNETTE_AMOUNT -> preset.advanced.vignetteAmount = value.coerceIn(0f, 1f)
EditAttribute.FADED_BLACK_LEVEL -> preset.toneCurve.fadedBlackLevel = value.coerceIn(0f, 1f)
EditAttribute.SHADOWS_TINT_R -> preset.toneCurve.shadowsTintR = value.coerceIn(0f, 1f)
EditAttribute.SHADOWS_TINT_G -> preset.toneCurve.shadowsTintG = value.coerceIn(0f, 1f)
EditAttribute.SHADOWS_TINT_B -> preset.toneCurve.shadowsTintB = value.coerceIn(0f, 1f)
EditAttribute.GRAIN_AMOUNT -> preset.grain.grainAmount = value.coerceIn(0f, 1f)
EditAttribute.GRAIN_SIZE -> preset.grain.grainSize = value.coerceIn(0f, 1f)
}
}
private fun showSavePresetDialog(tempPreset: ColorPreset) { private fun showSavePresetDialog(tempPreset: ColorPreset) {
val input = android.widget.EditText(this).apply { val input = android.widget.EditText(this).apply {
hint = "Nhập tên Preset mới" hint = "Nhập tên Preset mới"
@@ -63,7 +63,7 @@ class PresetAdapter(
holder.cardPreset.strokeWidth = 0 holder.cardPreset.strokeWidth = 0
} }
// Setup custom 3-second long press detector with touch slop cancellation // Setup custom 1-second long press detector with touch slop cancellation
val handler = android.os.Handler(android.os.Looper.getMainLooper()) val handler = android.os.Handler(android.os.Looper.getMainLooper())
var isLongPressTriggered = false var isLongPressTriggered = false
var startX = 0f var startX = 0f
@@ -84,7 +84,7 @@ class PresetAdapter(
isLongPressTriggered = false isLongPressTriggered = false
startX = event.x startX = event.x
startY = event.y startY = event.y
handler.postDelayed(longPressRunnable, 3000) handler.postDelayed(longPressRunnable, 800)
} }
android.view.MotionEvent.ACTION_MOVE -> { android.view.MotionEvent.ACTION_MOVE -> {
if (Math.abs(event.x - startX) > touchSlop || Math.abs(event.y - startY) > touchSlop) { if (Math.abs(event.x - startX) > touchSlop || Math.abs(event.y - startY) > touchSlop) {
@@ -0,0 +1,114 @@
package com.photobooth.app
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
class RulerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
var value: Int = 0
set(newValue) {
val clamped = newValue.coerceIn(minVal, maxVal)
if (field != clamped) {
field = clamped
invalidate()
onValueChangeListener?.invoke(clamped)
}
}
var minVal: Int = -100
set(newValue) {
field = newValue
invalidate()
}
var maxVal: Int = 100
set(newValue) {
field = newValue
invalidate()
}
var onValueChangeListener: ((Int) -> Unit)? = null
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var lastX = 0f
private var isDragging = false
private val density = resources.displayMetrics.density
private fun dp(value: Float) = value * density
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val w = width.toFloat()
val h = height.toFloat()
val cx = w / 2f
// Draw ticks
// One tick mark every tickInterval value units.
// The spacing in pixels between tickInterval units is tickSpacingPx.
val tickInterval = 2
val tickSpacingPx = dp(8f)
// Draw ticks within visible area
val unitsPerPixel = tickInterval / tickSpacingPx
val minVisibleUnit = (value - cx * unitsPerPixel).toInt() - 5
val maxVisibleUnit = (value + cx * unitsPerPixel).toInt() + 5
for (u in minVisibleUnit..maxVisibleUnit) {
if (u < minVal || u > maxVal) continue
if (u % tickInterval != 0) continue
val x = cx + (u - value) * (tickSpacingPx / tickInterval)
if (x < 0 || x > w) continue
val isMajor = u % 10 == 0
val isMedium = u % 5 == 0 && !isMajor
val tickHeight = when {
isMajor -> dp(20f)
isMedium -> dp(14f)
else -> dp(8f)
}
paint.color = if (isMajor) Color.WHITE else Color.GRAY
paint.strokeWidth = if (isMajor) dp(1.5f) else dp(1.0f)
// Draw line from bottom upwards
canvas.drawLine(x, h, x, h - tickHeight, paint)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastX = event.x
isDragging = true
}
MotionEvent.ACTION_MOVE -> {
if (isDragging) {
val deltaX = event.x - lastX
lastX = event.x
// Move the value proportionally to translation
val tickSpacingPx = dp(8f)
val tickInterval = 2
val pixelsPerUnit = tickSpacingPx / tickInterval
val deltaVal = -(deltaX / pixelsPerUnit)
value = (value + deltaVal).toInt().coerceIn(minVal, maxVal)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isDragging = false
}
}
return true
}
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF9800"
android:pathData="M12,17l-6,-6h12z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM12,18c-3.31,0-6-2.69-6-6s2.69-6 6-6 6,2.69 6,6-2.69,6-6,6z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10-4.48 10-10S17.52,2 12,2zm0,18c-4.41,0-8-3.59-8-8s3.59-8 8-8 8,3.59 8,8-3.59,8-8,8zm0-13c-2.76,0-5,2.24-5,5s2.24,5 5,5 5-2.24 5-5-2.24-5-5-5zm0,8c-1.66,0-3-1.34-3-3s1.34-3 3-3 3,1.34 3,3-1.34,3-3,3z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,22c5.52,0 10-4.48 10-10S17.52,2 12,2 2,6.48 2,12s4.48,10 10,10zM12,4c4.42,0 8,3.58 8,8s-3.58,8-8,8V4z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M2,4h20v2H2zm0,5h20v2H2zm0,5h20v2H2zm0,5h20v2H2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2-0.9 2-2V5C21,3.9 20.1,3 19,3zm-2,10H7v-2h10v2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,5h2v2H3zm4,0h2v2H7zm4,0h2v2H11zm4,0h2v2H15zm4,0h2v2H19zM3,11h2v2H3zm4,0h2v2H7zm4,0h2v2H11zm4,0h2v2H15zm4,0h2v2H19zM3,17h2v2H3zm4,0h2v2H7zm4,0h2v2H11zm4,0h2v2H15zm4,0h2v2H19z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M6,12a2,2 0 1,0 4,0a2,2 0 1,0 -4,0M14,12a4,4 0 1,0 8,0a4,4 0 1,0 -8,0" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10-4.48 10-10S17.52,2 12,2zm0,18c-4.41,0-8-3.59-8-8s3.59-8 8-8 8,3.59 8,8-3.59,8-8,8z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M15,13V5c0-1.66-1.34-3-3-3S9,3.34 9,5v8c-1.21,0.91-2,2.37-2,4 0,2.76 2.24,5 5,5s5-2.24 5-5c0-1.63-0.79-3.09-2-4zM12,20c-1.66,0-3-1.34-3-3 0-1.09 0.58-2.05 1.5-2.58V5c0-0.28 0.22-0.5 0.5-0.5s0.5,0.22 0.5,0.5v9.42c0.92,0.53 1.5,1.49 1.5,2.58 0,1.66-1.34,3-3,3z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2.69C12,2.69 5,10 5,14c0,3.86 3.14,7 7,7s7-3.14 7-7c0-4-7-11.31-7-11.31zM12,19c-2.76,0-5-2.24-5-5 0-2.5 3.57-7.44 5-9.25 1.43,1.8 5,6.73 5,9.25 0,2.76-2.24,5-5,5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#40C4FF"
android:pathData="M12,2.69C12,2.69 5,10 5,14c0,3.86 3.14,7 7,7s7-3.14 7-7c0-4-7-11.31-7-11.31zM12,19c-2.76,0-5-2.24-5-5 0-2.5 3.57-7.44 5-9.25 1.43,1.8 5,6.73 5,9.25 0,2.76-2.24,5-5,5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#69F0AE"
android:pathData="M12,2.69C12,2.69 5,10 5,14c0,3.86 3.14,7 7,7s7-3.14 7-7c0-4-7-11.31-7-11.31zM12,19c-2.76,0-5-2.24-5-5 0-2.5 3.57-7.44 5-9.25 1.43,1.8 5,6.73 5,9.25 0,2.76-2.24,5-5,5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF5252"
android:pathData="M12,2.69C12,2.69 5,10 5,14c0,3.86 3.14,7 7,7s7-3.14 7-7c0-4-7-11.31-7-11.31zM12,19c-2.76,0-5-2.24-5-5 0-2.5 3.57-7.44 5-9.25 1.43,1.8 5,6.73 5,9.25 0,2.76-2.24,5-5,5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,7.5L14,12l4.5,2-4.5,2-2,4.5-2-4.5-4.5-2 4.5-2 2-4.5zM19,3l1,2.2L22.2,6l-2.2,1L19,9.2 18,7l-2.2-1 2.2-1L19,3z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2-0.9 2-2V5C23,3.9 22.1,3 21,3zM12,18c-3.31,0-6-2.69-6-6s2.69-6 6-6s6,2.69 6,6S15.31,18 12,18z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM12,18c-3.31,0-6-2.69-6-6s2.69-6 6-6 6,2.69 6,6-2.69,6-6,6z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41-1.41z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,22c5.52,0 10-4.48 10-10S17.52,2 12,2 2,6.48 2,12s4.48,10 10,10zM12,4c4.42,0 8,3.58 8,8s-3.58,8-8,8V4z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10-4.48 10-10S17.52,2 12,2zm0,18c-4.41,0-8-3.59-8-8s3.59-8 8-8 8,3.59 8,8-3.59,8-8,8z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M15,13V5c0-1.66-1.34-3-3-3S9,3.34 9,5v8c-1.21,0.91-2,2.37-2,4 0,2.76 2.24,5 5,5s5-2.24 5-5c0-1.63-0.79-3.09-2-4zM12,20c-1.66,0-3-1.34-3-3 0-1.09 0.58-2.05 1.5-2.58V5c0-0.28 0.22-0.5 0.5-0.5s0.5,0.22 0.5,0.5v9.42c0.92,0.53 1.5,1.49 1.5,2.58 0,1.66-1.34,3-3,3z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,7.5L14,12l4.5,2-4.5,2-2,4.5-2-4.5-4.5-2 4.5-2 2-4.5zM19,3l1,2.2L22.2,6l-2.2,1L19,9.2 18,7l-2.2-1 2.2-1L19,3z" />
</vector>
@@ -54,6 +54,29 @@
android:contentDescription="Switch Camera" /> android:contentDescription="Switch Camera" />
</LinearLayout> </LinearLayout>
<!-- DẢI ĐEN PHÍA TRÊN ẢNH CAMERA (Như mô tả trong image.png) -->
<RelativeLayout
android:id="@+id/layoutEditorTitleBar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#000000"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/topBar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<!-- Văn bản hiển thị động tên nút đang được chọn -->
<TextView
android:id="@+id/tvCurrentAttributeName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Brightness (Độ sáng)"
android:textColor="#FF9800"
android:textSize="14sp"
android:textStyle="bold" />
</RelativeLayout>
<!-- ================= 2. KHUNG FRAME CHÍNH (CAMERA + FILTER + FRAME OVERLAY) ================= --> <!-- ================= 2. KHUNG FRAME CHÍNH (CAMERA + FILTER + FRAME OVERLAY) ================= -->
<FrameLayout <FrameLayout
android:id="@+id/cameraContainer" android:id="@+id/cameraContainer"
@@ -102,179 +125,6 @@
android:visibility="gone" android:visibility="gone"
android:text="3" /> android:text="3" />
<!-- LỚP 4: BẢNG SLIDERS FLOATING (Nằm lọt vào giữa hình ảnh camera) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardAdvancedSliders"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="24dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
android:visibility="gone"
app:strokeWidth="0dp"
android:backgroundTint="#A6000000">
<!-- ScrollView cho phép cuộn lên xuống tự do để chỉnh tất cả các thông số -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Tiêu đề & Nút X (Đóng) -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp">
<TextView
android:id="@+id/tvEditPresetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chỉnh sửa: Instax Nắng Chiều"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:textSize="15sp" />
<ImageButton
android:id="@+id/btnCancelEdit"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentEnd="true"
android:background="@android:color/transparent"
android:src="@drawable/ic_close_white"
app:tint="#FFFFFF"
android:contentDescription="Cancel Edit" />
</RelativeLayout>
<!-- ================= NHÓM 1: CƠ BẢN (BASIC ADJUSTMENTS) ================= -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ĐIỀU CHỈNH CƠ BẢN"
android:textColor="#FF9800"
android:textStyle="bold"
android:textSize="11sp"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"/>
<!-- Brightness -->
<TextView android:id="@+id/tvLabelBrightness" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Brightness (Độ sáng): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderBrightness" 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"/>
<!-- Contrast -->
<TextView android:id="@+id/tvLabelContrast" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Contrast (Tương phản): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderContrast" 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"/>
<!-- Saturation -->
<TextView android:id="@+id/tvLabelSaturation" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Saturation (Bão hòa màu): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderSaturation" 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"/>
<!-- Vibrance -->
<TextView android:id="@+id/tvLabelVibrance" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Vibrance (Bão hòa thông minh): 0.00"/>
<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"/>
<!-- Temperature -->
<TextView android:id="@+id/tvLabelTemperature" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Temperature (Nhiệt màu): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderTemperature" 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"/>
<!-- Tint -->
<TextView android:id="@+id/tvLabelTint" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Tint (Sắc độ hồng/xanh): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderTint" 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"/>
<!-- ================= NHÓM 2: NÂNG CAO (ADVANCED EFFECTS) ================= -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="HIỆU ỨNG NÂNG CAO"
android:textColor="#FF9800"
android:textStyle="bold"
android:textSize="11sp"
android:layout_marginTop="12dp"
android:layout_marginBottom="4dp"/>
<!-- Clarity -->
<TextView android:id="@+id/tvLabelClarity" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Clarity (Độ rõ nét vùng trung tính): 0.00"/>
<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"/>
<!-- Dehaze -->
<TextView android:id="@+id/tvLabelDehaze" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Dehaze (Độ trong suốt / Sương mù): 0.00"/>
<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"/>
<!-- Vignette Amount -->
<TextView android:id="@+id/tvLabelVignetteAmount" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Vignette Amount (Bo tối góc): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderVignetteAmount" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.0" android:valueTo="1.0" android:value="0.0" app:thumbColor="#FF9800" app:trackColorActive="#FF9800" app:trackColorInactive="#333333"/>
<!-- ================= NHÓM 3: GIẢ LẬP ĐƯỜNG CONG MÀU (TONE CURVE EMULATION) ================= -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="GIẢ LẬP TONE CURVE &amp; MÀU VÙNG TỐI"
android:textColor="#FF9800"
android:textStyle="bold"
android:textSize="11sp"
android:layout_marginTop="12dp"
android:layout_marginBottom="4dp"/>
<!-- Faded Black Level -->
<TextView android:id="@+id/tvLabelFadedBlack" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Faded Black Level (Độ mờ vùng đen): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderFadedBlack" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.0" android:valueTo="1.0" android:value="0.0" app:thumbColor="#FF9800" app:trackColorActive="#FF9800" app:trackColorInactive="#333333"/>
<!-- Shadows Tint R -->
<TextView android:id="@+id/tvLabelShadowsTintR" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Shadows Tint R (Sắc Đỏ vùng tối): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderShadowsTintR" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.0" android:valueTo="1.0" android:value="0.0" app:thumbColor="#FF9800" app:trackColorActive="#FF9800" app:trackColorInactive="#333333"/>
<!-- Shadows Tint G -->
<TextView android:id="@+id/tvLabelShadowsTintG" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Shadows Tint G (Sắc Lục vùng tối): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderShadowsTintG" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.0" android:valueTo="1.0" android:value="0.0" app:thumbColor="#FF9800" app:trackColorActive="#FF9800" app:trackColorInactive="#333333"/>
<!-- Shadows Tint B -->
<TextView android:id="@+id/tvLabelShadowsTintB" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Shadows Tint B (Sắc Lam vùng tối): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderShadowsTintB" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.0" android:valueTo="1.0" android:value="0.0" app:thumbColor="#FF9800" app:trackColorActive="#FF9800" app:trackColorInactive="#333333"/>
<!-- ================= NHÓM 4: HẠT NHIỄU PHIM (FILM GRAIN) ================= -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="HẠT NHIỄU PHIM"
android:textColor="#FF9800"
android:textStyle="bold"
android:textSize="11sp"
android:layout_marginTop="12dp"
android:layout_marginBottom="4dp"/>
<!-- Grain Amount -->
<TextView android:id="@+id/tvLabelGrainAmount" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Grain Amount (Mật độ hạt): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderGrainAmount" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.0" android:valueTo="1.0" android:value="0.0" app:thumbColor="#FF9800" app:trackColorActive="#FF9800" app:trackColorInactive="#333333"/>
<!-- Grain Size -->
<TextView android:id="@+id/tvLabelGrainSize" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFFFFF" android:textSize="11sp" android:text="Grain Size (Kích thước hạt): 0.00"/>
<com.google.android.material.slider.Slider android:id="@+id/sliderGrainSize" android:layout_width="match_parent" android:layout_height="wrap_content" android:valueFrom="0.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="48dp"
android:layout_marginTop="16dp"
android:text="LƯU THÀNH PRESET MỚI"
android:textColor="#FFFFFF"
android:backgroundTint="#FF9800"
android:textStyle="bold"
android:insetTop="0dp"
android:insetBottom="0dp"
app:cornerRadius="4dp" />
</LinearLayout>
</ScrollView>
</com.google.android.material.card.MaterialCardView>
</FrameLayout> </FrameLayout>
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= --> <!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
@@ -415,4 +265,96 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<!-- KHU VỰC ĐIỀU KHIỂN CHỈNH SỬA ẢNH (Thay thế Control Panel khi nhấn giữ preset) -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layoutTimelineEditor"
android:layout_width="match_parent"
android:layout_height="180dp"
android:background="#000000"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent">
<!-- TẦNG 1: THANH TRƯỢT THƯỚC ĐO ĐỘC ĐÁO (RULER SLIDER) -->
<FrameLayout
android:id="@+id/rulerContainer"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent">
<com.photobooth.app.RulerView
android:id="@+id/rulerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Kim chỉ số 0 màu cam chính giữa cố định -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center">
<TextView
android:id="@+id/tvRulerValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#FF9800"
android:textSize="12sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"/>
<ImageView
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/ic_arrow_down_orange"
android:layout_gravity="center_horizontal"
android:contentDescription="Indicator" />
</LinearLayout>
</FrameLayout>
<!-- TẦNG 2: PHẦN HIỂN THỊ ICON CÁC THUỘC TÍNH (HORIZONTAL LIST) -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvAttributeIcons"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/rulerContainer"
app:layout_constraintBottom_toTopOf="@id/layoutEditorActionButtons" />
<!-- TẦNG 3: THANH THAO TÁC HOÀN THÀNH (HỦY / ĐỒNG Ý) -->
<RelativeLayout
android:id="@+id/layoutEditorActionButtons"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
app:layout_constraintBottom_toBottomOf="parent">
<!-- Nút X (Hủy bỏ các thông số vừa chỉnh) -->
<ImageButton
android:id="@+id/btnEditorClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@null"
android:src="@drawable/ic_close_white"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="Close" />
<!-- Nút Dấu Tích (Đồng ý để lưu preset) -->
<ImageButton
android:id="@+id/btnEditorCheck"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@null"
android:src="@drawable/ic_check_white"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="Save" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="64dp"
android:layout_height="50dp">
<ImageView
android:id="@+id/imgAttributeIcon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Attribute Icon" />
</FrameLayout>