fix: chỉnh sửa button cho phần chỉnh presets
This commit is contained in:
@@ -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` và `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.
|
||||
@@ -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
@@ -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** và **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.
|
||||
|
||||
@@ -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 hiển 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 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
@@ -194,6 +199,38 @@ class MainActivity : AppCompatActivity() {
|
||||
}, { 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) {
|
||||
@@ -217,48 +254,142 @@ class MainActivity : AppCompatActivity() {
|
||||
currentSelectedPreset = preset
|
||||
if (preset.id == "DEFAULT") {
|
||||
binding.imgFilterOverlay.visibility = android.view.View.GONE
|
||||
binding.viewFinder.setLayerType(android.view.View.LAYER_TYPE_NONE, null)
|
||||
} else {
|
||||
binding.imgFilterOverlay.visibility = android.view.View.VISIBLE
|
||||
val isBeingEdited = editingPreset != null
|
||||
val filterColor = if (isBeingEdited) {
|
||||
// Sophisticated ARGB mapping to simulate film filters using adjustment parameters
|
||||
// Vibrance and Saturation increase opacity of the filter color
|
||||
val alpha = (((preset.basic.vibrance + preset.basic.saturation) / 2f + 1.0f) * 40 + 20).toInt().coerceIn(10, 120)
|
||||
val useCalculation = isBeingEdited || (preset.id != "instax_faded_warm_01" && preset.id != "instax_bw_cool" && preset.id != "instax_cool_summer")
|
||||
val filterColor = if (useCalculation) {
|
||||
// 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)
|
||||
|
||||
// Temperature (+ is warm/red-yellow), Brightness, Clarity, and ShadowsTintR affect red
|
||||
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)
|
||||
// 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
|
||||
|
||||
// Contrast, Tint (- is green), and ShadowsTintG affect green
|
||||
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)
|
||||
// 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
|
||||
|
||||
// Dehaze, cool Temperature (- is blue), and ShadowsTintB affect blue
|
||||
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)
|
||||
// 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 -> {
|
||||
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)
|
||||
}
|
||||
else -> android.graphics.Color.argb(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (frame.id == "DEFAULT" || frame.imageFileName.isEmpty()) {
|
||||
binding.btnMainFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
|
||||
@@ -341,118 +472,31 @@ class MainActivity : AppCompatActivity() {
|
||||
onCaptureClick()
|
||||
}
|
||||
|
||||
// Advanced Sliders listeners
|
||||
binding.sliderBrightness.addOnChangeListener { _, value, _ ->
|
||||
editingPreset?.let { preset ->
|
||||
preset.basic.brightness = value
|
||||
binding.tvLabelBrightness.text = "Brightness (Độ sáng): ${String.format("%.2f", value)}"
|
||||
applyPresetFilter(preset)
|
||||
// Camera viewfinder container swipe gesture listener to adjust parameters
|
||||
binding.cameraContainer.setOnTouchListener { _, event ->
|
||||
if (binding.layoutTimelineEditor.visibility != android.view.View.VISIBLE) {
|
||||
return@setOnTouchListener false
|
||||
}
|
||||
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, _ ->
|
||||
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)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
binding.btnCancelEdit.setOnClickListener {
|
||||
binding.btnEditorClose.setOnClickListener {
|
||||
closeAdvancedSliders()
|
||||
}
|
||||
|
||||
binding.btnSaveCustomPreset.setOnClickListener {
|
||||
binding.btnEditorCheck.setOnClickListener {
|
||||
editingPreset?.let { preset ->
|
||||
showSavePresetDialog(preset)
|
||||
}
|
||||
@@ -548,72 +592,105 @@ class MainActivity : AppCompatActivity() {
|
||||
grain = selectedPreset.grain.copy()
|
||||
)
|
||||
|
||||
binding.tvEditPresetName.text = "Chỉnh sửa: ${selectedPreset.name}"
|
||||
binding.layoutEditorTitleBar.visibility = android.view.View.VISIBLE
|
||||
|
||||
// Update Sliders visual values
|
||||
binding.sliderBrightness.value = editingPreset!!.basic.brightness
|
||||
binding.sliderContrast.value = editingPreset!!.basic.contrast
|
||||
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
|
||||
// Setup initial selected attribute: Brightness
|
||||
val initialNode = AttributeNode(EditAttribute.BRIGHTNESS, "Brightness (Độ sáng)", R.drawable.ic_attr_brightness)
|
||||
onAttributeSelected(initialNode)
|
||||
|
||||
// Update text labels
|
||||
binding.tvLabelBrightness.text = "Brightness (Độ sáng): ${String.format("%.2f", editingPreset!!.basic.brightness)}"
|
||||
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)}"
|
||||
binding.panelSelectionContainer.visibility = android.view.View.GONE
|
||||
binding.bottomControlPanel.visibility = android.view.View.GONE
|
||||
|
||||
// Scale animation from center
|
||||
binding.cardAdvancedSliders.apply {
|
||||
binding.layoutTimelineEditor.apply {
|
||||
visibility = android.view.View.VISIBLE
|
||||
alpha = 0f
|
||||
scaleX = 0.8f
|
||||
scaleY = 0.8f
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(250)
|
||||
.setInterpolator(android.view.animation.OvershootInterpolator(1.2f))
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeAdvancedSliders() {
|
||||
binding.cardAdvancedSliders.animate()
|
||||
binding.layoutTimelineEditor.animate()
|
||||
.alpha(0f)
|
||||
.scaleX(0.8f)
|
||||
.scaleY(0.8f)
|
||||
.setDuration(200)
|
||||
.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
|
||||
currentSelectedPreset?.let { applyPresetFilter(it) }
|
||||
}
|
||||
.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) {
|
||||
val input = android.widget.EditText(this).apply {
|
||||
hint = "Nhập tên Preset mới"
|
||||
|
||||
@@ -63,7 +63,7 @@ class PresetAdapter(
|
||||
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())
|
||||
var isLongPressTriggered = false
|
||||
var startX = 0f
|
||||
@@ -84,7 +84,7 @@ class PresetAdapter(
|
||||
isLongPressTriggered = false
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
handler.postDelayed(longPressRunnable, 3000)
|
||||
handler.postDelayed(longPressRunnable, 800)
|
||||
}
|
||||
android.view.MotionEvent.ACTION_MOVE -> {
|
||||
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" />
|
||||
</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) ================= -->
|
||||
<FrameLayout
|
||||
android:id="@+id/cameraContainer"
|
||||
@@ -102,179 +125,6 @@
|
||||
android:visibility="gone"
|
||||
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 & 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>
|
||||
|
||||
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
|
||||
@@ -415,4 +265,96 @@
|
||||
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user