Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e3c6f35c7 | |||
| b5d7311447 | |||
| bcbb853558 | |||
| 1a74887fe0 | |||
| 2308fe6fa9 | |||
| be997a0c72 | |||
| fad1c0a264 | |||
| 424ca4b419 | |||
| 4a33140c67 | |||
| edd4c2946d |
@@ -0,0 +1,221 @@
|
||||
Dưới đây là kế hoạch hoàn thiện toàn bộ danh sách các thanh kéo (Sliders) trong bảng Floating Card mờ cho tệp cấu hình JSON đầy đủ của bạn. Chúng ta sẽ đưa tất cả 14 thông số chia theo đúng 4 nhóm cấu trúc của tệp cấu hình vào trong giao diện cuộn dọc của `ScrollView`.
|
||||
|
||||
---
|
||||
|
||||
## 📐 1. Cấu Trúc Đầy Đủ Cho Layout XML (`activity_main.xml`)
|
||||
|
||||
Thay thế phần nội dung bên trong `LinearLayout` của bảng Floating Card (nằm dưới tiêu đề) để bổ sung đầy đủ tất cả các thanh kéo, phân chia theo từng nhóm trực quan giúp người dùng dễ thao tác.
|
||||
|
||||
```xml
|
||||
<!-- Đặt bên trong LinearLayout của bảng Floating Card mờ (Lớp 3 của cameraContainer) -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- ================= 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" 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" 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
|
||||
<!-- ================= 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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"/>
|
||||
<!-- Hiệu ứng bo tối góc đi từ 0.0 (Không bo) đến 1.0 (Bo đen tối đa) -->
|
||||
<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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
|
||||
<!-- ================= 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
|
||||
<!-- ================= 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
<!-- 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" app:thumbColor="#FF9800" app:trackColorActive="#FF9800"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 2. Logic Khởi Tạo & Đồng Bộ Giá Trị (Kotlin trong `MainActivity.kt`)
|
||||
|
||||
Khi người dùng mở bảng chỉnh sửa cho preset, hàm `openAdvancedSliders` sẽ thực hiện gán các giá trị ban đầu từ tệp JSON lên các thanh Slider phần cứng đồng thời thiết lập lắng nghe cử chỉ:
|
||||
|
||||
```kotlin
|
||||
fun openAdvancedSliders(selectedPreset: ColorPreset) {
|
||||
editingPreset = selectedPreset.deepCopy() // Nhân bản dữ liệu để tránh ghi đè bản gốc
|
||||
|
||||
binding.tvEditPresetName.text = "Chỉnh sửa: ${editingPreset!!.name}"
|
||||
|
||||
// 1. Nạp dữ liệu Nhóm Basic
|
||||
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
|
||||
|
||||
// 2. Nạp dữ liệu Nhóm Advanced
|
||||
binding.sliderClarity.value = editingPreset!!.advanced.clarity
|
||||
binding.sliderDehaze.value = editingPreset!!.advanced.dehaze
|
||||
binding.sliderVignetteAmount.value = editingPreset!!.advanced.vignetteAmount
|
||||
|
||||
// 3. Nạp dữ liệu Nhóm Tone Curve Emulation
|
||||
binding.sliderFadedBlack.value = editingPreset!!.toneCurve.fadedBlackLevel
|
||||
binding.sliderShadowsTintR.value = editingPreset!!.toneCurve.shadowsTintR
|
||||
binding.sliderShadowsTintG.value = editingPreset!!.toneCurve.shadowsTintG
|
||||
binding.sliderShadowsTintB.value = editingPreset!!.toneCurve.shadowsTintB
|
||||
|
||||
// 4. Nạp dữ liệu Nhóm Film Grain
|
||||
binding.sliderGrainAmount.value = editingPreset!!.grain.grainAmount
|
||||
binding.sliderGrainSize.value = editingPreset!!.grain.grainSize
|
||||
|
||||
// Gọi hàm thiết lập sự kiện lắng nghe chuyển động cho toàn bộ Sliders
|
||||
setupSliderListeners()
|
||||
|
||||
// Hiển thị Card mờ floating lọt lòng màn hình camera như ảnh image_2be399.jpg
|
||||
binding.cardAdvancedSliders.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 3. Cơ Chế Cập Nhật Giá Trị Thời Gian Thực (Real-time Pipeline)
|
||||
|
||||
Viết một hàm tập trung gán sự kiện thay đổi (`addOnChangeListener`) cho tất cả các thanh kéo. Khi con trỏ dịch chuyển, chữ số nhãn text sẽ đổi ngay lập tức và đẩy cấu trúc Object mới sang GPU xử lý trực tiếp trên kính ngắm.
|
||||
|
||||
```kotlin
|
||||
private fun setupSliderListeners() {
|
||||
|
||||
// Ví dụ gán cho Brightness
|
||||
binding.sliderBrightness.addOnChangeListener { _, value, _ ->
|
||||
binding.tvLabelBrightness.text = String.format("Brightness (Độ sáng): %.2f", value)
|
||||
editingPreset?.basic?.brightness = value
|
||||
applyLiveFilterToCamera(editingPreset) // Đẩy ma trận màu sang GPU render
|
||||
}
|
||||
|
||||
// Ví dụ gán cho Vibrance
|
||||
binding.sliderVibrance.addOnChangeListener { _, value, _ ->
|
||||
binding.tvLabelVibrance.text = String.format("Vibrance (Bão hòa thông minh): %.2f", value)
|
||||
editingPreset?.basic?.vibrance = value
|
||||
applyLiveFilterToCamera(editingPreset)
|
||||
}
|
||||
|
||||
// Ví dụ gán cho Clarity (Nhóm Advanced)
|
||||
binding.sliderClarity.addOnChangeListener { _, value, _ ->
|
||||
binding.tvLabelClarity.text = String.format("Clarity (Độ rõ nét vùng trung tính): %.2f", value)
|
||||
editingPreset?.advanced?.clarity = value
|
||||
applyLiveFilterToCamera(editingPreset)
|
||||
}
|
||||
|
||||
// Ví dụ gán cho Faded Black Level (Nhóm Tone Curve)
|
||||
binding.sliderFadedBlack.addOnChangeListener { _, value, _ ->
|
||||
binding.tvLabelFadedBlack.text = String.format("Faded Black Level (Độ mờ vùng đen): %.2f", value)
|
||||
editingPreset?.toneCurve?.fadedBlackLevel = value
|
||||
applyLiveFilterToCamera(editingPreset)
|
||||
}
|
||||
|
||||
// Ví dụ gán cho Grain Amount (Nhóm Film Grain)
|
||||
binding.sliderGrainAmount.addOnChangeListener { _, value, _ ->
|
||||
binding.tvLabelGrainAmount.text = String.format("Grain Amount (Mật độ hạt): %.2f", value)
|
||||
editingPreset?.grain?.grainAmount = value
|
||||
applyLiveFilterToCamera(editingPreset)
|
||||
}
|
||||
|
||||
// Thực hiện viết gán tương tự cho toàn bộ các thanh Sliders còn lại trong danh sách...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Bảng cấu trúc 14 thông số này sẽ tạo ra khả năng tùy biến bộ lọc ảnh mạnh mẽ không thua kém gì các bộ công cụ chỉnh ảnh phim chuyên nghiệp như VSCO hay Lightroom Mobile.
|
||||
@@ -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`.
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
|
||||
# 🎨 KẾ HOẠCH TÁI CẤU TRÚC UX/UI: TỐI GIẢN & TƯƠNG TÁC ĐỘNG
|
||||
|
||||
Mục tiêu: Chuyển đổi giao diện sang phong cách tối giản (không viền, không nền), sử dụng màu sắc để biểu thị trạng thái (Trắng ➔ Cam) và tối ưu hóa không gian hiển thị bằng cách gộp timeline của Frame và Presets.
|
||||
|
||||
---
|
||||
|
||||
## 📐 1. Cập Nhật Cấu Trúc Giao Diện (XML Layout)
|
||||
|
||||
Chúng ta sẽ điều chỉnh file `activity_main.xml` để loại bỏ các khối nền thừa và cấu trúc lại các cụm nút dựa theo `image_29a4a7.png`.
|
||||
|
||||
### 🔹 1.1. Top Bar (Các Icon Trạng Thái)
|
||||
|
||||
Thay thế các `Button` có text thành `ImageButton` chỉ chứa icon (Tia sét, Đồng hồ, Vòng xoay camera).
|
||||
|
||||
* **Style:** Không nền (`android:background="@null"`), không bo viền.
|
||||
* **Màu sắc:** Sử dụng `app:tint="@color/white"` làm mặc định.
|
||||
|
||||
```xml
|
||||
<LinearLayout
|
||||
android:id="@+id/topBar" ...>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnFlash"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_flash"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<!-- Tương tự cho btnTimer (ic_timer) và btnSwitchCamera (ic_flip_camera) -->
|
||||
</LinearLayout>
|
||||
|
||||
```
|
||||
|
||||
### 🔹 1.2. Nút Frame và Nút Presets (Toggle Buttons)
|
||||
|
||||
Đây là hai nút chính nằm trên cụm Zoom. Cần thiết kế dưới dạng `ImageView` (hoặc `CardView` có bo góc nhẹ nếu muốn chứa ảnh mượt hơn) để có thể thay đổi ảnh đại diện (thumbnail) khi người dùng chọn.
|
||||
|
||||
```xml
|
||||
<LinearLayout
|
||||
android:id="@+id/toggleModeContainer" ...>
|
||||
|
||||
<!-- Nút Frame -->
|
||||
<ImageView
|
||||
android:id="@+id/btnModeFrame"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:src="@drawable/ic_default_frame_thumbnail" />
|
||||
|
||||
<!-- Nút Presets -->
|
||||
<ImageView
|
||||
android:id="@+id/btnModePreset"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:src="@drawable/ic_default_preset_thumbnail" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Khung Timeline dùng chung (sẽ thay đổi Adapter tùy thuộc vào nút nào được bấm) -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvSharedTimeline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
```
|
||||
|
||||
### 🔹 1.3. Cụm Zoom (0.5x, 1x, 3x)
|
||||
|
||||
Thay thế các nút nền xám hiện tại thành `TextView` văn bản thuần túy.
|
||||
|
||||
* **Style:** Chữ đậm, không nền (`android:background="@null"`).
|
||||
* **Trạng thái màu:** Mặc định màu trắng (`#FFFFFF`), khi chọn đổi sang màu cam (`#FF9800`).
|
||||
|
||||
### 🔹 1.4. Bottom Panel (Thêm nút Play/Review)
|
||||
|
||||
Dựa theo vị trí khoanh đỏ ở góc dưới bên trái của `image_29a4a7.png`, thêm nút xem lại ảnh.
|
||||
|
||||
```xml
|
||||
<RelativeLayout
|
||||
android:id="@+id/controlPanel" ...>
|
||||
|
||||
<!-- Nút Play (Xem lại ảnh) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnReviewGallery"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="24dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_play_gallery" />
|
||||
|
||||
<!-- Nút Chụp (Ở giữa) -->
|
||||
<!-- ... -->
|
||||
</RelativeLayout>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 2. Kịch Bản Logic & Quản Lý Trạng Thái (Kotlin)
|
||||
|
||||
### 🔹 2.1. Logic Đổi Màu Icon Top Bar và Zoom (Trắng ➔ Cam)
|
||||
|
||||
Tạo các hàm helper để xử lý đổi màu nhanh gọn bằng `ColorStateList` hoặc set `tint` trực tiếp:
|
||||
|
||||
```kotlin
|
||||
// Ví dụ đổi màu icon Flash
|
||||
fun updateFlashIconState(isActive: Boolean) {
|
||||
val color = if (isActive) Color.parseColor("#FF9800") else Color.WHITE
|
||||
binding.btnFlash.imageTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
// Ví dụ đổi màu text Zoom
|
||||
fun updateZoomTextState(selectedZoom: TextView) {
|
||||
// Reset tất cả về trắng
|
||||
binding.tvZoom05.setTextColor(Color.WHITE)
|
||||
binding.tvZoom1.setTextColor(Color.WHITE)
|
||||
binding.tvZoom3.setTextColor(Color.WHITE)
|
||||
|
||||
// Set màu cam cho mức zoom được chọn
|
||||
selectedZoom.setTextColor(Color.parseColor("#FF9800"))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 🔹 2.2. Logic Toggle Giữa Frame và Presets
|
||||
|
||||
Để đáp ứng yêu cầu: Bấm Frame thì ẩn Presets (và ngược lại), chúng ta quản lý trạng thái hiển thị bằng `View.VISIBLE` và `View.GONE`.
|
||||
|
||||
1. **Khi nhấn nút Frame:** Mở Timeline Frame.
|
||||
Ẩn nút Presets (`btnModePreset.visibility = View.GONE`).
|
||||
Hiển thị `rvSharedTimeline`. Set Adapter của RecyclerView thành danh sách các Frames.
|
||||
*(Lưu ý: Bạn nên cấu hình để khi nhấn nút Frame lần nữa, Timeline sẽ đóng lại và nút Presets hiện ra lại để người dùng có thể chuyển đổi).*
|
||||
|
||||
|
||||
2. **Khi nhấn nút Presets:** Mở Timeline Presets.
|
||||
Ẩn nút Frame (`btnModeFrame.visibility = View.GONE`).
|
||||
Hiển thị `rvSharedTimeline`. Set Adapter của RecyclerView thành danh sách các Preset màu.
|
||||
|
||||
|
||||
3. **Khi chọn phần tử trong Timeline:** Cập nhật Thumbnail.
|
||||
Nếu đang ở chế độ Frame và người dùng chọn "Vintage Frame", lấy ảnh thumbnail của frame đó gán đè vào icon của nút Frame: `btnModeFrame.setImageBitmap(selectedFrameThumb)`. Tương tự cho Presets.
|
||||
|
||||
|
||||
### 🔹 2.3. Logic Nút "Play" (Xem lại ảnh)
|
||||
|
||||
Khi nhấn vào `btnReviewGallery`, sử dụng `Intent` để mở một Activity mới (`GalleryActivity`) hiển thị lưới (Grid) các ảnh đã chụp (đọc từ thư mục lưu trữ mà bạn đã cấu hình ở các bước trước). Hoặc đơn giản nhất là mở thẳng thư viện mặc định của điện thoại thông qua `ACTION_VIEW`.
|
||||
|
||||
---
|
||||
|
||||
## 📅 3. Các Đầu Việc Cần Làm (Checklist)
|
||||
|
||||
* [ ] **Chuẩn bị Assets (SVG/Vector):** Tìm/Tạo các icon dạng vector (XML) cho `ic_flash`, `ic_timer`, `ic_flip_camera`, `ic_play`. Sử dụng vector giúp ảnh không bị vỡ và dễ dàng đổi màu bằng thuộc tính `tint` trên Android.
|
||||
* [ ] **Cập nhật Layout:** Mở file `activity_main.xml` và xóa các thuộc tính background cũ, thay thế bằng các thuộc tính như kế hoạch trên.
|
||||
* [ ] **Viết Helper Functions:** Trong `MainActivity.kt`, tạo các hàm `toggleTimelineMode(isFrameMode: Boolean)` để quản lý việc ẩn/hiện và tráo đổi Adapter cho 2 nút Frame/Presets.
|
||||
* [ ] **Tạo Activity Review:** Tạo một màn hình đơn giản hoặc Fragment để hiển thị ảnh ngay khi bấm nút Play ở góc dưới cùng bên trái.
|
||||
@@ -0,0 +1,126 @@
|
||||
|
||||
# 🎨 THIẾT KẾ HỆ THỐNG PRESET MÀU CHUYÊN SÂU (GIẢ LẬP PHIM INSTAX)
|
||||
|
||||
Mục tiêu: Định nghĩa một cấu trúc dữ liệu chuẩn hóa (JSON Schema) chứa đầy đủ các thông số xử lý ảnh nâng cao để áp dụng trực tiếp lên kính ngắm Camera thời gian thực và cho phép người dùng tự thiết kế custom preset.
|
||||
|
||||
---
|
||||
|
||||
## 📁 1. Cấu Trúc Dữ Liệu Tổng Hợp (JSON Schema)
|
||||
|
||||
Mọi tệp cấu hình Preset (nằm trong thư mục hệ thống `assets/presets/` hoặc do người dùng tự lưu trong bộ nhớ máy) sẽ tuân theo định dạng chuẩn hóa dưới đây. Các giá trị số được quy ước trong khoảng từ `-1.0f` đến `1.0f` để tối ưu cho việc tính toán đồ họa phần cứng.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "instax_faded_warm_01",
|
||||
"name": "Instax Nắng Chiều",
|
||||
"theme_category": "Retro Vintage",
|
||||
"is_editable": true,
|
||||
"basic_adjustments": {
|
||||
"brightness": 0.05,
|
||||
"contrast": -0.15,
|
||||
"saturation": -0.20,
|
||||
"vibrance": 0.25,
|
||||
"temperature": 0.18,
|
||||
"tint": -0.02
|
||||
},
|
||||
"advanced_effects": {
|
||||
"clarity": -0.20,
|
||||
"dehaze": -0.15,
|
||||
"vignette_amount": 0.35
|
||||
},
|
||||
"tone_curve_emulation": {
|
||||
"faded_black_level": 0.12,
|
||||
"shadows_tint_r": 0.0,
|
||||
"shadows_tint_g": 0.04,
|
||||
"shadows_tint_b": 0.08
|
||||
},
|
||||
"film_grain": {
|
||||
"grain_amount": 0.28,
|
||||
"grain_size": 0.15
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 2. Giải Thích Ý Nghĩa Các Thông Số Trong Nhiếp Ảnh Chân Dung/Photobooth
|
||||
|
||||
Để giả lập chính xác chất ảnh Instax/Polaroid hoài cổ, ba thông số mới đóng vai trò quyết định cấu trúc của bức ảnh:
|
||||
|
||||
* **Vibrance (Bão hòa thông minh):** Khác với *Saturation* tăng màu đồng đều dễ làm cháy da người, *Vibrance* chỉ tăng cường các màu còn nhạt và chủ động né tông màu da. Công thức tạo màu phim luôn là: **[Giảm Saturation (-20%) + Tăng Vibrance (+25%)]** để màu bối cảnh trong trẻo mà da mặt nhân vật vẫn hồng hào tự nhiên.
|
||||
* **Clarity (Độ rõ nét vùng Midtone):** Đối với phong cách Vintage/Instax mờ ảo, thông số này thường được đặt giá trị **Âm (Negative Clarity từ -15% đến -30%)**. Việc giảm Clarity sẽ làm dịu các vùng chuyển góc gắt, tạo hiệu ứng mịn da tự nhiên và làm bức ảnh có cảm giác "mềm mại", mơ màng giống ống kính nhựa của máy phim cổ.
|
||||
* **Dehaze (Điều chỉnh độ trong suốt/Khói mù):** Khi đặt giá trị **Âm (Negative Dehaze từ -10% đến -25%)**, bộ lọc sẽ cố tình bơm thêm một lớp "sương mờ" nhẹ vào bức ảnh. Hiệu ứng này mô phỏng hiện tượng lọt sáng nhẹ (Light leak) hoặc sự suy giảm hóa chất của các tấm phim lấy liền theo thời gian.
|
||||
|
||||
---
|
||||
|
||||
## 💻 3. Lập Trình Lớp Đối Tượng Trên Android Native (Kotlin)
|
||||
|
||||
Tạo file `ColorPreset.kt` để quản lý các thông số này trong code logic ứng dụng:
|
||||
|
||||
```kotlin
|
||||
package com.photobooth.app.model
|
||||
|
||||
data class ColorPreset(
|
||||
val id: String,
|
||||
var name: String,
|
||||
val themeCategory: String,
|
||||
val isEditable: Boolean,
|
||||
val basic: BasicAdjustments,
|
||||
val advanced: AdvancedEffects,
|
||||
val toneCurve: ToneCurveEmulation,
|
||||
val grain: FilmGrain
|
||||
)
|
||||
|
||||
data class BasicAdjustments(
|
||||
var brightness: Float, // -1.0f đến 1.0f
|
||||
var contrast: Float, // -1.0f đến 1.0f
|
||||
var saturation: Float, // -1.0f đến 1.0f
|
||||
var vibrance: Float, // -1.0f đến 1.0f -> Bảo vệ màu da người
|
||||
var temperature: Float, // -1.0f đến 1.0f -> Ấm (vàng) / Lạnh (xanh)
|
||||
var tint: Float // -1.0f đến 1.0f -> Xanh lá / Hồng dâu
|
||||
)
|
||||
|
||||
data class AdvancedEffects(
|
||||
var clarity: Float, // -1.0f đến 1.0f -> Giảm để làm mịn thơ mộng
|
||||
var dehaze: Float, // -1.0f đến 1.0f -> Giảm để tạo lớp sương hoài cổ
|
||||
var vignetteAmount: Float // 0.0f đến 1.0f -> Bo tối 4 góc ảnh
|
||||
)
|
||||
|
||||
data class ToneCurveEmulation(
|
||||
var fadedBlackLevel: Float, // Nâng đáy màu đen thành xám sờn
|
||||
var shadowsTintR: Float,
|
||||
var shadowsTintG: Float,
|
||||
var shadowsTintB: Float // Ám màu xanh rêu/xanh biển vào vùng tối
|
||||
)
|
||||
|
||||
data class FilmGrain(
|
||||
var grainAmount: Float, // Độ đậm hạt nhiễu
|
||||
var grainSize: Float // Kích thước hạt nhiễu phim
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 4. Quy Trình Xử Lý Tương Tác Tùy Biến (Custom Workflow)
|
||||
|
||||
1. **Bước 1: Người dùng tinh chỉnh bộ thông số nâng cao:** Giao diện Slider.
|
||||
Trong chế độ chỉnh sửa, ứng dụng hiển thị một danh sách các thanh cuộn dọc hoặc ngang (SeekBars/Sliders) tương ứng với: *Vibrance, Clarity, Dehaze, Grain...* Người dùng kéo chỉnh thay đổi giá trị trực tiếp.
|
||||
|
||||
|
||||
2. **Bước 2: Áp dụng Real-time qua OpenGL ES / Vulkan:** Render Shader.
|
||||
Mỗi khi Slider thay đổi, giá trị float mới được đẩy thẳng vào một chương trình đồ họa phần cứng (Custom Fragment Shader). GPU sẽ xử lý tính toán màu sắc, độ mịn (Clarity), và độ mờ (Dehaze) trên luồng Surface của CameraX ở tốc độ 60 khung hình/giây.
|
||||
|
||||
|
||||
3. **Bước 3: Đóng gói và Ghi tệp JSON cá nhân:** Serialization.
|
||||
Khi người dùng ấn "Lưu làm Preset của tôi", đối tượng `ColorPreset` chứa các thông số tùy biến mới sẽ được serialize sang chuỗi JSON bằng thư viện GSON. File này được ghi vào thư mục `context.filesDir` (bộ nhớ riêng an toàn của app).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📅 5. Danh Sách Các Việc Cần Làm Tiếp Theo (Checklist)
|
||||
|
||||
* [ ] **Xây dựng Custom Slider View:** Thiết kế các thanh trượt có phong cách tối giản với con trỏ (thumb) màu cam bám sát theo triết lý UX mới.
|
||||
* [ ] **Tích hợp GPUImage cho Android:** Sử dụng thư viện `GPUImage` (hoặc viết OpenGL Shader thuần) để hiện thực hóa các bộ lọc toán học cho `VibranceFilter`, `ClarityFilter` (chuyển đổi dựa trên thuật toán Unsharp Mask) và `VignetteFilter`.
|
||||
* [ ] **Đồng bộ hóa sang iOS:** Đảm bảo cấu trúc JSON này khi đưa sang iOS sẽ được phân tích (Parse) bằng giao thức `Decodable` của Swift thành một struct tương đương nhằm gọi các filter chuẩn của Apple như `CIVibrance` và `CILinearToSRGBToneCurve`.
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
# 🛠️ KẾ HOẠCH FIX BUG: TÁI CẤU TRÚC PHÂN TẦNG TIMELINE & CONTROL PANEL
|
||||
|
||||
Mục tiêu: Loại bỏ dải nền xám chứa các nút zoom. Di chuyển các nút zoom tích hợp chung vào khu vực điều khiển camera phía dưới (cùng hàng với nút Shutter và nút Xem lại). Thay đổi biểu tượng nút Play thành biểu tượng Image (Thư viện).
|
||||
|
||||
---
|
||||
|
||||
## 📐 1. Phác Thảo Cấu Trúc Giao Diện Mới (Sau khi chỉnh sửa)
|
||||
|
||||
Giao diện từ cạnh dưới kính ngắm camera trở xuống sẽ được tối giản thành đúng 2 khối:
|
||||
|
||||
```
|
||||
[ Kính ngắm Camera chính (Tỉ lệ 3:4) ]
|
||||
--------------------------------------------------
|
||||
[ Khối 1: TIMELINE CONTAINER ]
|
||||
- Nút Neo cố định ở cạnh trái (Frame / Palette)
|
||||
- Thanh RecyclerView trượt ngang lướt dưới nút Neo
|
||||
--------------------------------------------------
|
||||
[ Khối 2: BOTTOM CONTROL PANEL ] (Không nền phụ, tối giản)
|
||||
- Hàng trên: Các cụm văn bản Zoom thuần (0.5x, 1x, 3x) - Text trắng/cam
|
||||
- Hàng dưới: [Icon Image/Gallery] ➔ [Nút Shutter Trắng/Cam] ➔ [Khoảng trống]
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 2. Chi Tiết Thay Đổi Trong Tệp Cấu Hình XML (`activity_main.xml`)
|
||||
|
||||
Bạn hãy thay thế toàn bộ khu vực phía dưới `cameraContainer` bằng đoạn mã tối ưu bằng `ConstraintLayout` dưới đây để giải quyết triệt để lỗi đè nền hiển thị trong ảnh `image_2afaba.png`.
|
||||
|
||||
```xml
|
||||
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
|
||||
<!-- Đặt ngay phía dưới Camera và không bị ai đè lên nữa -->
|
||||
<FrameLayout
|
||||
android:id="@+id/panelSelectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:background="#000000"
|
||||
app:layout_constraintTop_toBottomOf="@id/cameraContainer"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottomControlPanel">
|
||||
|
||||
<!-- Danh sách cuộn ngang -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvHorizontalTimeline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="75dp"
|
||||
android:paddingEnd="16dp" />
|
||||
|
||||
<!-- Nút chức năng chính (Góc trái làm điểm Neo đè lên RecyclerView) -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainAnchor"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="left|center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:src="@drawable/ic_palette_default"
|
||||
android:background="@null" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- ================= KHỐI 2: BOTTOM CONTROL PANEL (ĐIỀU KHIỂN CAMERA) ================= -->
|
||||
<!-- Khối này gom gọn cả nút Zoom, Shutter và nút Xem ảnh, xóa bỏ các dải nền thừa -->
|
||||
<ConstraintLayout
|
||||
android:id="@+id/bottomControlPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="#000000"
|
||||
app:layout_constraintTop_toBottomOf="@id/panelSelectionContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!-- CỤM ZOOM THUẦN (Xóa bỏ dải nền cũ trong image_2afaba.png) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutZoomGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvZoom05"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0.5x"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvZoom1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1x"
|
||||
android:textColor="#FF9800" <!-- Mặc định màu cam cho 1x -->
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvZoom3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="3x"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- NÚT XEM ẢNH (Thay thế nút mũi tên Play cũ thành biểu tượng Image/Gallery) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnGalleryReview"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_image_gallery" <!-- Đổi sang icon bức ảnh hình vuông/thư viện -->
|
||||
app:layout_constraintTop_toTopOf="@id/btnShutterCapture"
|
||||
app:layout_constraintBottom_toBottomOf="@id/btnShutterCapture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="32dp" />
|
||||
|
||||
<!-- NÚT SHUTTER CHỤP ẢNH (Giữ cố định vị trí vàng ở giữa dưới cùng) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnShutterCapture"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="@drawable/bg_shutter_orange_circle" <!-- Viền cam ruột trắng -->
|
||||
android:layout_marginBottom="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layoutZoomGroup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</ConstraintLayout>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 3. Các Đầu Việc Cần Cập Nhật Trong Mã Nguồn Kotlin
|
||||
|
||||
Sau khi cập nhật lại cây thư mục giao diện XML, bạn cần thực hiện ánh xạ lại ID trong `MainActivity.kt`:
|
||||
|
||||
1. **Thay đổi sự kiện Nút Xem Ảnh:** Đổi tên ánh xạ từ nút mũi tên Play cũ sang nút mới:
|
||||
```kotlin
|
||||
binding.btnGalleryReview.setOnClickListener {
|
||||
// Thực hiện logic mở thư viện xem lại ảnh đã chụp
|
||||
}
|
||||
|
||||
```
|
||||
2. **Kiểm tra độ cao vùng chạm:** Vì chúng ta loại bỏ phần nền chứa các nút zoom cũ, `RecyclerView` của Timeline giờ đây có toàn bộ diện tích `80dp` rõ ràng. Các item hiển thị hiển thị trọn vẹn, không sợ bị các nút zoom bấm đè đứt đoạn nữa.
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
|
||||
# 🎨 KẾ HOẠCH TÁI CẤU TRÚC UX: HOẠT HỌA DI CHUYỂN & DANH SÁCH CUỘN ĐỒNG HÀNG
|
||||
|
||||
Mục tiêu: Khi kích hoạt Frame hoặc Preset, nút đại diện sẽ dịch chuyển sang trái làm điểm neo. Danh sách các item sẽ trượt trên cùng một hàng ngang và chui xuống dưới nút neo khi lướt (Swipe). Nhấn giữ preset để mở shortcut chỉnh sửa.
|
||||
|
||||
---
|
||||
|
||||
## 📐 1. Cấu Trúc Lại XML Layout Tuyến Tính (`activity_main.xml`)
|
||||
|
||||
Để đạt được hiệu ứng cuộn "chui xuống dưới nút", nút Neo (Anchor) phải có thứ tự hiển thị nằm **trên** (Z-index cao hơn) và nằm đè lên `RecyclerView`. Ta sẽ sử dụng một `FrameLayout` hoặc `ConstraintLayout` để làm việc này tại khu vực viền xanh lá của file `image_2a8660.png`.
|
||||
|
||||
```xml
|
||||
<!-- KHU VỰC ĐIỀU KHIỂN DÒNG (Khung viền xanh trong image_2a8660.png) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/panelSelectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="70dp"
|
||||
android:background="#000000"
|
||||
app:layout_constraintTop_toBottomOf="@id/cameraContainer"
|
||||
app:layout_constraintBottom_toTopOf="@id/zoomControls">
|
||||
|
||||
<!-- LỚP ĐÁY: Danh sách cuộn ngang dùng chung cho cả Frame và Preset -->
|
||||
<!-- Thuộc tính paddingStart giúp phần tử đầu tiên không bị nút Neo che khuất lúc khởi tạo -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvHorizontalTimeline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="80dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- LỚP TRÊN: Chứa 2 nút chức năng chính (Gốc ở giữa, động chuyển sang trái) -->
|
||||
<ConstraintLayout
|
||||
android:id="@+id/buttonsWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Nút Frame -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainFrame"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_frame_default"
|
||||
android:background="@null"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnMainPreset"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
android:layout_marginEnd="16dp"/>
|
||||
|
||||
<!-- Nút Presets Màu -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainPreset"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_palette_default"
|
||||
android:background="@null"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/btnMainFrame"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 2. Kịch Bản Hoạt Họa & Điều Khiển Trạng Thái (Kotlin)
|
||||
|
||||
### 🔹 2.1. Logic Di Chuyển Nút Ra Cạnh Trái (Animation)
|
||||
|
||||
Khi người dùng nhấn vào nút Frame, ta sẽ chạy một Animation dịch chuyển nút này về sát rìa trái màn hình, đồng thời ẩn nút còn lại và hiển thị `RecyclerView`.
|
||||
|
||||
```kotlin
|
||||
private var isFrameModeActive = false
|
||||
|
||||
fun toggleFrameMode(activate: Boolean) {
|
||||
if (activate) {
|
||||
isFrameModeActive = true
|
||||
// 1. Ẩn nút Preset màu đi
|
||||
binding.btnMainPreset.animate().alpha(0f).setDuration(200).withEndAction {
|
||||
binding.btnMainPreset.visibility = View.GONE
|
||||
}
|
||||
|
||||
// 2. Dịch chuyển nút Frame sang trái (Dùng TransitionManager để mượt mà)
|
||||
TransitionManager.beginDelayedTransition(binding.buttonsWrapper)
|
||||
val params = binding.btnMainFrame.layoutParams as ConstraintLayout.LayoutParams
|
||||
params.endToStart = ConstraintLayout.LayoutParams.UNSET // Hủy liên kết xích giữa
|
||||
params.endToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
binding.btnMainFrame.layoutParams = params
|
||||
|
||||
// 3. Hiển thị RecyclerView cùng hàng
|
||||
binding.rvHorizontalTimeline.apply {
|
||||
adapter = frameAdapter // Đổi sang dữ liệu của Frame
|
||||
visibility = View.VISIBLE
|
||||
alpha = 0f
|
||||
animate().alpha(1f).setDuration(300).start()
|
||||
}
|
||||
} else {
|
||||
// Logic phục hồi về trạng thái cân bằng ở giữa ban đầu như trong ảnh image_2a8660.png
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
> **Giải thích cơ chế "Chui xuống dưới":** Do `rvHorizontalTimeline` được khai báo trước trong `FrameLayout`, nó nằm ở lớp dưới. Nút `btnMainFrame` nằm ở lớp trên (`ConstraintLayout`). Khi bạn vuốt cuộn ngang `RecyclerView`, các item bên trong sẽ tự nhiên trượt và chui xuống dưới khu vực nút Frame đang đứng cố định ở góc trái.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 3. Cơ Chế Nhấn Giữ Preset & Nút Chỉnh Sửa Nhỏ (Edit Shortcut)
|
||||
|
||||
Để giải quyết yêu cầu nhấn giữ mở nút Edit nhỏ mà không làm thay đổi preset gốc, chúng ta sẽ áp dụng cơ chế **Floating Context Button**.
|
||||
|
||||
### 🔹 3.1. Thiết kế Giao Diện Nút Edit Nhỏ trong Item của RecyclerView
|
||||
|
||||
Trong file layout thiết kế cho từng ô Preset nhỏ (`item_preset.xml`), ta thêm một nút icon cây bút nhỏ (`btnSmallEdit`) nằm đè ở góc trên bên phải của ô. Mặc định nút này sẽ ẩn (`android:visibility="gone"`).
|
||||
|
||||
### 🔹 3.2. Code bắt sự kiện Nhấn giữ (OnLongClickListener)
|
||||
|
||||
1. **Bước 1: Hiện nút Edit nhỏ:** Nhấn giữ Item.
|
||||
Trong Adapter của Preset, gán sự kiện `setOnLongClickListener` cho ô màu. Khi kích hoạt, hiển thị nút `btnSmallEdit` (`View.VISIBLE`) kèm hiệu ứng rung nhẹ (Haptic Feedback) để thông báo cho người dùng.
|
||||
|
||||
|
||||
2. **Bước 2: Mở bảng chỉnh sửa độc lập:** Chạm nút Edit.
|
||||
Khi người dùng bấm vào nút `btnSmallEdit` nhỏ vừa hiện ra, app sẽ nhân bản (Clone) đối tượng `ColorPreset` hiện tại thành một thực thể tạm thời và mở bảng điều khiển Sliders (Vibrance, Clarity, Dehaze...).
|
||||
|
||||
|
||||
3. **Bước 3: Lưu thành Preset mới:** Đặt tên & Ghi file.
|
||||
Sau khi tinh chỉnh xong và ấn Save, một Dialog hiện lên yêu cầu nhập tên mới (Ví dụ: "Instax của Lộc"). App lưu file JSON mới này vào bộ nhớ riêng (`context.filesDir`) và giữ nguyên vẹn file preset gốc ban đầu.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📅 4. Các Đầu Việc Cần Làm Ngay (Checklist)
|
||||
|
||||
* [ ] **Cập nhật lại Code Layout:** Thiết lập `FrameLayout` bao bọc bên ngoài đúng như cấu trúc để tạo lớp đè cho hiệu ứng chui dưới nút.
|
||||
* [ ] **Tạo File Hoạt Họa/Transition:** Viết logic hoàn chỉnh cho cả việc lật ngược trạng thái (khi người dùng bấm vào nút Neo lần nữa để đóng timeline, đưa 2 nút quay lại vị trí trung tâm cân bằng như ảnh `image_2a8660.png`).
|
||||
* [ ] **Cập nhật ViewHolder trong Adapter:** Thêm tính năng ẩn/hiện nút `btnSmallEdit` dựa trên trạng thái click của từng item đơn lẻ.
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# 🎨 KẾ HOẠCH PHÁT TRIỂN: BẢNG SLIDERS TÙY CHỈNH MÀU THỜI GIAN THỰC (REAL-TIME OVERLAY)
|
||||
|
||||
Mục tiêu: Khi bấm nút Edit nhỏ trên một Preset, một bảng chỉnh sửa mờ (Semi-transparent Overlay) sẽ trượt lên từ phía dưới, đè lên kính ngắm camera. Mọi thao tác kéo trượt của người dùng sẽ cập nhật trực tiếp vào bộ xử lý đồ họa phần cứng để thay đổi màu ảnh ngay lập tức (WYSIWYG).
|
||||
|
||||
---
|
||||
|
||||
## 📐 1. Cấu Trúc Thành Phần Giao Diện (XML Layout)
|
||||
|
||||
Để bảng điều khiển này có thể phủ (Overlay) lên hình ảnh camera mà không làm dịch chuyển hay thu nhỏ kính ngắm, chúng ta sẽ định nghĩa nó như một lớp trên cùng nằm trong `FrameLayout` chính của ứng dụng.
|
||||
|
||||
### 🔹 1.1. Bổ sung giao diện ẩn vào `activity_main.xml`
|
||||
|
||||
Bảng này mặc định sẽ ẩn đi (`android:visibility="gone"`) và được gom nhóm trong một thẻ `NestedScrollView` để người dùng có thể cuộn lên/xuống nếu có nhiều thông số.
|
||||
|
||||
```xml
|
||||
<!-- Chèn thêm vào phân tầng cao nhất của FrameLayout chứa camera hoặc Layout gốc -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutAdvancedSliders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="#D9000000" <!-- Nền đen mờ 85% để vừa nhìn thấy camera phía sau vừa rõ chữ -->
|
||||
android:padding="20dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/panelSelectionContainer">
|
||||
|
||||
<!-- Thanh tiêu đề & Nút thao tác nhanh -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
<TextView
|
||||
android:id="@+id/tvEditPresetName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Chỉnh sửa: Instax Warm"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp" />
|
||||
<ImageButton
|
||||
android:id="@+id/btnCancelEdit"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_close_white" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- KHU VỰC CÁC THANH KÉO (SLIDERS) -->
|
||||
<!-- 1. Thanh chỉnh VIBRANCE -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Vibrance (Bão hòa thông minh)"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="12sp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sliderVibrance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="-1.0"
|
||||
android:valueTo="1.0"
|
||||
android:value="0.0"
|
||||
app:thumbColor="#FF9800"
|
||||
app:trackColorActive="#FF9800"
|
||||
app:trackColorInactive="#333333" />
|
||||
|
||||
<!-- 2. Thanh chỉnh CLARITY -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Clarity (Độ rõ nét vùng trung tính)"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sliderClarity"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="-1.0"
|
||||
android:valueTo="1.0"
|
||||
android:value="0.0"
|
||||
app:thumbColor="#FF9800"
|
||||
app:trackColorActive="#FF9800"
|
||||
app:trackColorInactive="#333333" />
|
||||
|
||||
<!-- 3. Thanh chỉnh DEHAZE -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Dehaze (Độ trong suốt / Sương mù)"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sliderDehaze"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="-1.0"
|
||||
android:valueTo="1.0"
|
||||
android:value="0.0"
|
||||
app:thumbColor="#FF9800"
|
||||
app:trackColorActive="#FF9800"
|
||||
app:trackColorInactive="#333333" />
|
||||
|
||||
<!-- NÚT LƯU THÀNH PRESET MỚI -->
|
||||
<Button
|
||||
android:id="@+id/btnSaveCustomPreset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="LƯU THÀNH PRESET MỚI"
|
||||
android:textColor="#FFFFFF"
|
||||
android:backgroundTint="#FF9800"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 2. Kịch Bản Tương Tác & Đồng Bộ Thời Gian Thực (Kotlin)
|
||||
|
||||
### 🔹 2.1. Chuẩn bị biến lưu trữ tạm thời (Clone Object)
|
||||
|
||||
Khi người dùng bấm nút Edit nhỏ từ Item trong RecyclerView, chúng ta không chỉnh sửa trực tiếp lên file gốc mà nhân bản dữ liệu ra một biến tạm:
|
||||
|
||||
```kotlin
|
||||
private var editingPreset: ColorPreset? = null
|
||||
|
||||
fun openAdvancedSliders(selectedPreset: ColorPreset) {
|
||||
// Clone đối tượng thông qua cơ chế sao chép dữ liệu (Deep Copy hoặc JSON String conversion)
|
||||
editingPreset = selectedPreset.copy(
|
||||
basic = selectedPreset.basic.copy(),
|
||||
advanced = selectedPreset.advanced.copy()
|
||||
)
|
||||
|
||||
// Cập nhật giá trị hiện tại lên các thanh Slider trực quan
|
||||
binding.sliderVibrance.value = editingPreset!!.basic.vibrance
|
||||
binding.sliderClarity.value = editingPreset!!.advanced.clarity
|
||||
binding.sliderDehaze.value = editingPreset!!.advanced.dehaze
|
||||
|
||||
// Hiển thị bảng kèm hoạt họa trượt lên mượt mà (Slide up animation)
|
||||
binding.layoutAdvancedSliders.apply {
|
||||
visibility = View.VISIBLE
|
||||
translationY = this.height.toFloat()
|
||||
animate().translationY(0f).setDuration(300).start()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 🔹 2.2. Lắng nghe thay đổi của Sliders và cập nhật Camera (Real-time Pipeline)
|
||||
|
||||
1. **Bước 1: Người dùng kéo Slider:** Lắng nghe sự kiện.
|
||||
Gán trình lắng nghe `addOnChangeListener` cho từng Slider (ví dụ: `sliderVibrance`). Khi người dùng di chuyển ngón tay, giá trị float (`value`) thay đổi liên tục.
|
||||
|
||||
|
||||
2. **Bước 2: Cập nhật thông số vào Object tạm:** Cập nhật Biến số.
|
||||
Cập nhật ngay giá trị mới vào biến tạm thời:
|
||||
`editingPreset?.basic?.vibrance = value`.
|
||||
|
||||
|
||||
3. **Bước 3: Đẩy thông số vào Khung hình Camera:** Render GPU.
|
||||
Gọi hàm `applyLiveFilterToCamera(editingPreset)`. Hàm này sẽ chuyển đổi toàn bộ thông số của `editingPreset` thành một ma trận cấu hình hoặc nạp trực tiếp vào OpenGL Shader (hoặc GPUImage) đang liên kết với `PreviewView` của CameraX. Màn hình camera sẽ thay đổi sắc độ và cấu trúc ảnh ngay lập tức.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 💾 3. Quy Trình Đổi Tên Và Lưu File Độc Lập
|
||||
|
||||
Khi bấm nút **btnSaveCustomPreset**, ứng dụng sẽ thực hiện các bước sau để tránh đè lên preset mặc định:
|
||||
|
||||
1. **Hiển thị Dialog nhập tên:** Mở một `AlertDialog` tối giản với một ô `EditText` để người dùng đặt tên cho bộ lọc của họ (Ví dụ mặc định gợi ý sẵn: `[Tên cũ] - Custom`).
|
||||
2. **Sinh ID duy nhất:** Sử dụng `UUID.randomUUID().toString()` để tạo ID mới cho preset này. Đổi thuộc tính `themeCategory` thành `"User Custom"`.
|
||||
3. **Lưu file JSON vật lý:** Sử dụng thư viện GSON để chuyển đổi Object `editingPreset` vừa tạo thành chuỗi JSON và ghi vào thư mục lưu trữ nội bộ của ứng dụng trên điện thoại:
|
||||
```kotlin
|
||||
val file = File(context.filesDir, "preset_${newId}.json")
|
||||
file.writeText(gson.toJson(editingPreset))
|
||||
|
||||
```
|
||||
|
||||
|
||||
4. **Cập nhật lại thanh trượt ngoài màn hình chính:** Đóng bảng slider nâng cao, nạp lại danh sách preset từ thư mục chứa file để item mới vừa tạo xuất hiện ngay lập tức trên thanh Timeline ngang.
|
||||
|
||||
---
|
||||
|
||||
## 📅 4. Các Đầu Việc Cần Làm Tiếp Theo (Checklist)
|
||||
|
||||
* [ ] **Cài đặt thư viện đồ họa thời gian thực:** Cấu hình thư viện xử lý ảnh (như `GPUImage` cho Android) chạy song song với `PreviewView` của CameraX để nhận các lệnh cập nhật thông số liên tục từ Slider mà không bị giật lag khung hình.
|
||||
* [ ] **Thiết kế hoạt họa đóng/mở:** Viết các tệp Animation (`slide_up.xml`, `slide_down.xml`) để tạo hiệu ứng chuyển cảnh mượt mà cho bảng trượt Overlay.
|
||||
* [ ] **Kiểm tra giới hạn kéo:** Thiết lập tính năng hiển thị số thực tế (ví dụ: `+0.25`, `-0.10`) ngay cạnh tên Slider khi người dùng đang kéo để họ nhận biết được mức độ tăng giảm chính xác.
|
||||
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# 🎨 KẾ HOẠCH ĐIỀU CHỈNH: BẢNG SLIDERS FLOATING & HIỂN THỊ TRANSPARENT FRAME
|
||||
|
||||
Mục tiêu: Đưa bảng chỉnh sửa thành một khối mờ floating nằm gọn bên trong khung hình camera, cho phép cuộn dọc nội dung sliders. Đồng thời, cấu trúc lại hệ thống hiển thị để phần trong suốt (Transparent) của file Frame PNG lọt lòng đúng luồng camera.
|
||||
|
||||
---
|
||||
|
||||
## 📐 1. Cấu Trúc Lại Phân Tầng Giao Diện (`activity_main.xml`)
|
||||
|
||||
Để bảng điều khiển nằm lọt vào bên trong phần hình ảnh, chúng ta phải đưa nó vào làm con trực tiếp của `FrameLayout` quản lý camera (`cameraContainer`). Thứ tự khai báo trong XML sẽ quyết định tầng hiển thị (Z-index):
|
||||
|
||||
```
|
||||
[FrameLayout: cameraContainer] (Tỉ lệ 3:4)
|
||||
├── Lớp 1 (Đáy): PreviewView (Luồng camera gốc từ phần cứng)
|
||||
├── Lớp 2 (Giữa): ImageView (Khung ảnh PNG trong suốt)
|
||||
└── Lớp 3 (Trên cùng): CardView / ScrollView (Bảng Sliders mờ floating)
|
||||
|
||||
```
|
||||
|
||||
### 💻 Đoạn mã XML chỉnh sửa chi tiết:
|
||||
|
||||
```xml
|
||||
<!-- KHU VỰC HÌNH ẢNH CAMERA CHÍNH (Tỉ lệ 3:4) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/cameraContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="3:4"
|
||||
app:layout_constraintTop_toBottomOf="@id/topBar">
|
||||
|
||||
<!-- LỚP 1: Kính ngắm Camera -->
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/viewFinder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- LỚP 2: Khung Frame PNG (Phần ruột trong suốt sẽ tự động lộ Camera ở Lớp 1 ra) -->
|
||||
<ImageView
|
||||
android:id="@+id/imgFrameOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<!-- LỚP 3: BẢNG SLIDERS FLOATING (Nằm lọt vào giữa hình ảnh như image_2be399.jpg) -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardAdvancedSliders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="24dp" <!-- Tạo khoảng trống lọt lòng xung quanh giống ảnh mẫu -->
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="8dp"
|
||||
android:visibility="gone" <!-- Mặc định ẩn -->
|
||||
app:strokeWidth="0dp"
|
||||
android:backgroundTint="#A6000000"> <!-- Màu nền đen mờ tỉ lệ nén Alpha mịn -->
|
||||
|
||||
<!-- 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="@null"
|
||||
android:src="@drawable/ic_close_white"
|
||||
app:tint="#FFFFFF" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- ================= DANH SÁCH CÁC CỤM SLIDERS ĐỂ CUỘN ================= -->
|
||||
<!-- 1. VIBRANCE -->
|
||||
<TextView
|
||||
android:id="@+id/tvLabelVibrance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Vibrance (Bão hòa thông minh): 0.0"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="12sp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sliderVibrance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="-1.0"
|
||||
android:valueTo="1.0"
|
||||
android:value="0.0"
|
||||
app:thumbColor="#FF9800"
|
||||
app:trackColorActive="#FF9800" />
|
||||
|
||||
<!-- 2. CLARITY -->
|
||||
<TextView
|
||||
android:id="@+id/tvLabelClarity"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Clarity (Độ rõ nét vùng trung tính): 0.0"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sliderClarity"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="-1.0"
|
||||
android:valueTo="1.0"
|
||||
android:value="0.0"
|
||||
app:thumbColor="#FF9800"
|
||||
app:trackColorActive="#FF9800" />
|
||||
|
||||
<!-- 3. DEHAZE -->
|
||||
<TextView
|
||||
android:id="@+id/tvLabelDehaze"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Dehaze (Độ trong suốt / Sương mù): 0.0"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="8dp" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sliderDehaze"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="-1.0"
|
||||
android:valueTo="1.0"
|
||||
android:value="0.0"
|
||||
app:thumbColor="#FF9800"
|
||||
app:trackColorActive="#FF9800" />
|
||||
|
||||
<!-- Bổ sung thêm các Slider khác (Brightness, Saturation, Grain) vào đây để cuộn... -->
|
||||
|
||||
<!-- 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>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 2. Kịch Bản Xử Lý Kỹ Thuật (Kotlin)
|
||||
|
||||
### 🔹 2.1. Hiện thực hóa việc hiện thị Frame suốt (Transparent Layer)
|
||||
|
||||
Để luồng camera hiện lên ở phần ruột trong suốt của khung hình:
|
||||
|
||||
1. Các file asset khung ảnh (ví dụ: `instaxframe.png`) thiết kế trên phần mềm đồ họa phải được đục lỗ rỗng (Alpha channel = 0) ở khu vực hiển thị ảnh chụp.
|
||||
2. Trong Android, khi người dùng chọn mẫu từ Timeline, ta nạp trực tiếp bằng luồng luân chuyển nhẹ:
|
||||
```kotlin
|
||||
val bitmap = BitmapFactory.decodeStream(assets.open("frames/instaxframe.png"))
|
||||
binding.imgFrameOverlay.setImageBitmap(bitmap)
|
||||
|
||||
```
|
||||
|
||||
|
||||
Do cơ chế xếp chồng tầng của `FrameLayout`, luồng CameraX hoạt động ở `viewFinder` (Lớp 1) sẽ tự động xuyên thấu và hiển thị mượt mà qua các vùng trống của `imgFrameOverlay` (Lớp 2).
|
||||
|
||||
### 🔹 2.2. Logic cập nhật Text hiển thị chỉ số thời gian thực (Real-time Value Indicator)
|
||||
|
||||
Bám sát thiết kế hiển thị số ở nhãn text (ví dụ: `Vibrance (...): -0.23`), ta cập nhật chuỗi text liên tục khi con trỏ di chuyển:
|
||||
|
||||
```kotlin
|
||||
binding.sliderVibrance.addOnChangeListener { slider, value, fromUser ->
|
||||
// 1. Cập nhật chữ hiển thị giá trị số thực tế
|
||||
binding.tvLabelVibrance.text = String.format("Vibrance (Bão hòa thông minh): %.2f", value)
|
||||
|
||||
// 2. Cập nhật Object tạm thời
|
||||
editingPreset?.basic?.vibrance = value
|
||||
|
||||
// 3. Đẩy sang GPU render lập tức lên ViewFinder
|
||||
applyLiveFilterToCamera(editingPreset)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 3. Các Đầu Việc Cần Triển Khai (Checklist)
|
||||
|
||||
* [ ] **Kiểm tra độ trong suốt Asset:** Đảm bảo tất cả các file khung ảnh trong thư mục `assets/frames/` là định dạng **PNG-24** hỗ trợ Transparent hoàn toàn.
|
||||
* [ ] **Cập nhật hiệu ứng hiển thị Card:** Khi mở bảng sliders, áp dụng hiệu ứng phóng to nhẹ từ tâm (`ScaleAnimation`) thay vì trượt từ dưới lên để phù hợp hơn với kiểu giao diện hộp Floating Card cố định.
|
||||
* [ ] **Tối ưu hóa ScrollView:** Cài đặt thuộc tính ngăn chặn xung đột vuốt (`Touch Interception`) giữa thanh cuộn của `ScrollView` và các con trỏ trượt của `Slider Mechanism` của Material Component, giúp việc kéo thông số không bị trượt nhầm làm cuộn cả bảng.
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.photobooth.app
|
||||
|
||||
data class ColorPreset(
|
||||
val id: String,
|
||||
var name: String,
|
||||
val themeCategory: String,
|
||||
val isEditable: Boolean,
|
||||
val basic: BasicAdjustments,
|
||||
val advanced: AdvancedEffects,
|
||||
val toneCurve: ToneCurveEmulation,
|
||||
val grain: FilmGrain
|
||||
)
|
||||
|
||||
data class BasicAdjustments(
|
||||
var brightness: Float, // -1.0f đến 1.0f
|
||||
var contrast: Float, // -1.0f đến 1.0f
|
||||
var saturation: Float, // -1.0f đến 1.0f
|
||||
var vibrance: Float, // -1.0f đến 1.0f -> Bảo vệ màu da người
|
||||
var temperature: Float, // -1.0f đến 1.0f -> Ấm (vàng) / Lạnh (xanh)
|
||||
var tint: Float // -1.0f đến 1.0f -> Xanh lá / Hồng dâu
|
||||
)
|
||||
|
||||
data class AdvancedEffects(
|
||||
var clarity: Float, // -1.0f đến 1.0f -> Giảm để làm mịn thơ mộng
|
||||
var dehaze: Float, // -1.0f đến 1.0f -> Giảm để tạo lớp sương hoài cổ
|
||||
var vignetteAmount: Float // 0.0f đến 1.0f -> Bo tối 4 góc ảnh
|
||||
)
|
||||
|
||||
data class ToneCurveEmulation(
|
||||
var fadedBlackLevel: Float, // Nâng đáy màu đen thành xám sờn
|
||||
var shadowsTintR: Float,
|
||||
var shadowsTintG: Float,
|
||||
var shadowsTintB: Float // Ám màu xanh rêu/xanh biển vào vùng tối
|
||||
)
|
||||
|
||||
data class FilmGrain(
|
||||
var grainAmount: Float, // Độ đậm hạt nhiễu
|
||||
var grainSize: Float // Kích thước hạt nhiễu phim
|
||||
)
|
||||
@@ -14,6 +14,8 @@ import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.photobooth.app.databinding.ActivityMainBinding
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
@@ -31,11 +33,22 @@ class MainActivity : AppCompatActivity() {
|
||||
private var imageCapture: ImageCapture? = null // Capture usecase
|
||||
private var isCountingDown = false // Countdown state flag
|
||||
|
||||
// Frame UI selectors state
|
||||
private lateinit var frameStacks: List<FrameStack>
|
||||
private lateinit var stackAdapter: FrameStackAdapter
|
||||
// Frame & Preset UI selectors state
|
||||
private lateinit var allFrames: List<FrameItem>
|
||||
private lateinit var colorPresets: List<ColorPreset>
|
||||
private lateinit var frameItemAdapter: FrameItemAdapter
|
||||
private lateinit var presetAdapter: PresetAdapter
|
||||
|
||||
private var isFrameModeOpen = false
|
||||
private var isPresetModeOpen = false
|
||||
private var currentSelectedFramePath: String? = null
|
||||
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)
|
||||
@@ -45,7 +58,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// Setup UI control listeners
|
||||
setupCameraControls()
|
||||
|
||||
// Setup Frame selection lists
|
||||
// Setup Frame/Preset selection lists
|
||||
initFrameData()
|
||||
setupRecyclerViews()
|
||||
|
||||
@@ -60,43 +73,164 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initFrameData() {
|
||||
val retroFrames = listOf(
|
||||
FrameItem("DEFAULT", "", ""), // Default/No Frame
|
||||
allFrames = listOf(
|
||||
FrameItem("DEFAULT", "Mặc định", ""),
|
||||
FrameItem("instax_mini_single", "Instax Mini", "frames/instax_mini_single.png"),
|
||||
FrameItem("photobooth_4strip", "Photobooth 4-Strip", "frames/photobooth_4strip.png")
|
||||
)
|
||||
val vintageFrames = listOf(
|
||||
FrameItem("DEFAULT", "", ""), // Default/No Frame
|
||||
FrameItem("instax_mini_single_v", "Vintage Instax", "frames/instax_mini_single.png"),
|
||||
FrameItem("photobooth_4strip_v", "Vintage 4-Strip", "frames/photobooth_4strip.png")
|
||||
)
|
||||
frameStacks = listOf(
|
||||
FrameStack("retro", "Retro Theme", retroFrames),
|
||||
FrameStack("vintage", "Vintage Theme", vintageFrames)
|
||||
|
||||
// Initialize default Color Presets
|
||||
val defaultPresets = listOf(
|
||||
ColorPreset(
|
||||
id = "DEFAULT",
|
||||
name = "Mặc định",
|
||||
themeCategory = "None",
|
||||
isEditable = false,
|
||||
basic = BasicAdjustments(0f, 0f, 0f, 0f, 0f, 0f),
|
||||
advanced = AdvancedEffects(0f, 0f, 0f),
|
||||
toneCurve = ToneCurveEmulation(0f, 0f, 0f, 0f),
|
||||
grain = FilmGrain(0f, 0f)
|
||||
),
|
||||
ColorPreset(
|
||||
id = "instax_faded_warm_01",
|
||||
name = "Instax Nắng Chiều",
|
||||
themeCategory = "Retro Vintage",
|
||||
isEditable = true,
|
||||
basic = BasicAdjustments(0.05f, -0.15f, -0.20f, 0.25f, 0.18f, -0.02f),
|
||||
advanced = AdvancedEffects(-0.20f, -0.15f, 0.35f),
|
||||
toneCurve = ToneCurveEmulation(0.12f, 0.0f, 0.04f, 0.08f),
|
||||
grain = FilmGrain(0.28f, 0.15f)
|
||||
),
|
||||
ColorPreset(
|
||||
id = "instax_bw_cool",
|
||||
name = "Instax Trắng Đen",
|
||||
themeCategory = "Monochrome",
|
||||
isEditable = true,
|
||||
basic = BasicAdjustments(0.0f, 0.10f, -1.0f, 0.0f, -0.05f, 0.0f),
|
||||
advanced = AdvancedEffects(0.05f, 0.0f, 0.20f),
|
||||
toneCurve = ToneCurveEmulation(0.08f, 0.0f, 0.0f, 0.0f),
|
||||
grain = FilmGrain(0.35f, 0.18f)
|
||||
),
|
||||
ColorPreset(
|
||||
id = "instax_cool_summer",
|
||||
name = "Mùa Hè Dịu Mát",
|
||||
themeCategory = "Summer",
|
||||
isEditable = true,
|
||||
basic = BasicAdjustments(0.02f, -0.05f, -0.10f, 0.15f, -0.18f, 0.05f),
|
||||
advanced = AdvancedEffects(-0.10f, -0.05f, 0.15f),
|
||||
toneCurve = ToneCurveEmulation(0.05f, 0.02f, 0.05f, 0.10f),
|
||||
grain = FilmGrain(0.15f, 0.12f)
|
||||
)
|
||||
)
|
||||
|
||||
// Load custom user saved presets from filesDir
|
||||
colorPresets = defaultPresets + loadCustomPresets()
|
||||
}
|
||||
|
||||
private fun loadCustomPresets(): List<ColorPreset> {
|
||||
val customList = mutableListOf<ColorPreset>()
|
||||
val files = filesDir.listFiles { _, name -> name.endsWith(".json") }
|
||||
files?.forEach { file ->
|
||||
try {
|
||||
val content = file.readText()
|
||||
val id = extractJsonField(content, "id")
|
||||
val name = extractJsonField(content, "name")
|
||||
val category = extractJsonField(content, "theme_category")
|
||||
|
||||
val brightness = extractJsonDoubleField(content, "brightness").toFloat()
|
||||
val contrast = extractJsonDoubleField(content, "contrast").toFloat()
|
||||
val saturation = extractJsonDoubleField(content, "saturation").toFloat()
|
||||
val vibrance = extractJsonDoubleField(content, "vibrance").toFloat()
|
||||
val temperature = extractJsonDoubleField(content, "temperature").toFloat()
|
||||
val tint = extractJsonDoubleField(content, "tint").toFloat()
|
||||
|
||||
val clarity = extractJsonDoubleField(content, "clarity").toFloat()
|
||||
val dehaze = extractJsonDoubleField(content, "dehaze").toFloat()
|
||||
val vignetteAmount = extractJsonDoubleField(content, "vignette_amount").toFloat()
|
||||
|
||||
val fadedBlackLevel = extractJsonDoubleField(content, "faded_black_level").toFloat()
|
||||
val shadowsTintR = extractJsonDoubleField(content, "shadows_tint_r").toFloat()
|
||||
val shadowsTintG = extractJsonDoubleField(content, "shadows_tint_g").toFloat()
|
||||
val shadowsTintB = extractJsonDoubleField(content, "shadows_tint_b").toFloat()
|
||||
|
||||
val grainAmount = extractJsonDoubleField(content, "grain_amount").toFloat()
|
||||
val grainSize = extractJsonDoubleField(content, "grain_size").toFloat()
|
||||
|
||||
val preset = ColorPreset(
|
||||
id = id,
|
||||
name = name,
|
||||
themeCategory = category,
|
||||
isEditable = true,
|
||||
basic = BasicAdjustments(brightness, contrast, saturation, vibrance, temperature, tint),
|
||||
advanced = AdvancedEffects(clarity, dehaze, vignetteAmount),
|
||||
toneCurve = ToneCurveEmulation(fadedBlackLevel, shadowsTintR, shadowsTintG, shadowsTintB),
|
||||
grain = FilmGrain(grainAmount, grainSize)
|
||||
)
|
||||
customList.add(preset)
|
||||
} catch (e: Exception) {
|
||||
// Skip invalid JSON presets
|
||||
}
|
||||
}
|
||||
return customList
|
||||
}
|
||||
|
||||
private fun extractJsonField(json: String, field: String): String {
|
||||
val pattern = "\"$field\"\\s*:\\s*\"([^\"]*)\"".toRegex()
|
||||
return pattern.find(json)?.groupValues?.get(1) ?: ""
|
||||
}
|
||||
|
||||
private fun extractJsonDoubleField(json: String, field: String): Double {
|
||||
val pattern = "\"$field\"\\s*:\\s*(-?\\d+\\.?\\d*)".toRegex()
|
||||
return pattern.find(json)?.groupValues?.get(1)?.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
private fun setupRecyclerViews() {
|
||||
// Setup Master Stack RecyclerView
|
||||
binding.rvFrameStacks.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
stackAdapter = FrameStackAdapter(frameStacks) { stack, selectedPos ->
|
||||
if (selectedPos == -1) {
|
||||
// Collapse subframes
|
||||
binding.rvSubFrames.visibility = android.view.View.GONE
|
||||
} else {
|
||||
// Expand and load stack subframes
|
||||
binding.rvSubFrames.visibility = android.view.View.VISIBLE
|
||||
frameItemAdapter.updateItems(stack.frames)
|
||||
}
|
||||
}
|
||||
binding.rvFrameStacks.adapter = stackAdapter
|
||||
|
||||
// Setup Sub Frames RecyclerView
|
||||
binding.rvSubFrames.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
frameItemAdapter = FrameItemAdapter(emptyList()) { frame ->
|
||||
// Setup Shared Timeline RecyclerView
|
||||
binding.rvHorizontalTimeline.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
|
||||
frameItemAdapter = FrameItemAdapter(allFrames) { frame ->
|
||||
setFrameOverlay(frame.imageFileName)
|
||||
updateFrameModeIcon(frame)
|
||||
}
|
||||
|
||||
presetAdapter = PresetAdapter(colorPresets, { preset ->
|
||||
applyPresetFilter(preset)
|
||||
updatePresetModeIcon(preset)
|
||||
}, { 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) }
|
||||
}
|
||||
binding.rvSubFrames.adapter = frameItemAdapter
|
||||
}
|
||||
|
||||
private fun setFrameOverlay(fileName: String) {
|
||||
@@ -116,41 +250,549 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPresetFilter(preset: ColorPreset) {
|
||||
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 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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
try {
|
||||
val inputStream = assets.open(frame.imageFileName)
|
||||
val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream)
|
||||
binding.btnMainFrame.setImageBitmap(bitmap)
|
||||
} catch (e: Exception) {
|
||||
binding.btnMainFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePresetModeIcon(preset: ColorPreset) {
|
||||
if (preset.id == "DEFAULT") {
|
||||
binding.btnMainPreset.setImageResource(R.drawable.ic_default_preset_thumbnail)
|
||||
} else {
|
||||
val colorHex = when (preset.id) {
|
||||
"instax_faded_warm_01" -> "#FFF57C00"
|
||||
"instax_bw_cool" -> "#FF808080"
|
||||
"instax_cool_summer" -> "#FF00BCD4"
|
||||
else -> "#FF9E9E9E"
|
||||
}
|
||||
val circleDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
shape = android.graphics.drawable.GradientDrawable.OVAL
|
||||
setColor(android.graphics.Color.parseColor(colorHex))
|
||||
}
|
||||
binding.btnMainPreset.setImageDrawable(circleDrawable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCameraControls() {
|
||||
// Toggle Switch Camera Button
|
||||
// Mode Selectors
|
||||
binding.btnMainFrame.setOnClickListener {
|
||||
toggleFrameMode()
|
||||
}
|
||||
|
||||
binding.btnMainPreset.setOnClickListener {
|
||||
togglePresetMode()
|
||||
}
|
||||
|
||||
// Top Bar
|
||||
binding.btnSwitchCamera.setOnClickListener {
|
||||
toggleCamera()
|
||||
}
|
||||
|
||||
// Toggle Flash Button
|
||||
binding.btnFlash.setOnClickListener {
|
||||
toggleFlash()
|
||||
}
|
||||
|
||||
// Toggle Timer Button
|
||||
binding.btnTimer.setOnClickListener {
|
||||
toggleTimer()
|
||||
}
|
||||
|
||||
// Zoom 0.5x Button
|
||||
binding.btnZoom05.setOnClickListener {
|
||||
// Zoom Text selectors
|
||||
binding.tvZoom05.setOnClickListener {
|
||||
applyZoom(0.5f)
|
||||
}
|
||||
|
||||
// Zoom 1x Button
|
||||
binding.btnZoom10.setOnClickListener {
|
||||
binding.tvZoom1.setOnClickListener {
|
||||
applyZoom(1.0f)
|
||||
}
|
||||
|
||||
// Zoom 3x Button
|
||||
binding.btnZoom30.setOnClickListener {
|
||||
binding.tvZoom3.setOnClickListener {
|
||||
applyZoom(3.0f)
|
||||
}
|
||||
|
||||
// Play Gallery Button
|
||||
binding.btnGalleryReview.setOnClickListener {
|
||||
try {
|
||||
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
|
||||
type = "image/*"
|
||||
flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Không thể mở Thư viện ảnh", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// Shutter Button
|
||||
binding.btnCapture.setOnClickListener {
|
||||
binding.btnShutterCapture.setOnClickListener {
|
||||
onCaptureClick()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
binding.btnEditorClose.setOnClickListener {
|
||||
closeAdvancedSliders()
|
||||
}
|
||||
|
||||
binding.btnEditorCheck.setOnClickListener {
|
||||
editingPreset?.let { preset ->
|
||||
showSavePresetDialog(preset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleFrameMode() {
|
||||
if (isCountingDown) return
|
||||
val wrapper = binding.buttonsWrapper
|
||||
TransitionManager.beginDelayedTransition(wrapper)
|
||||
|
||||
if (!isFrameModeOpen) {
|
||||
isFrameModeOpen = true
|
||||
isPresetModeOpen = false
|
||||
|
||||
// Hide preset button
|
||||
binding.btnMainPreset.visibility = android.view.View.GONE
|
||||
|
||||
// Translate Frame button to left edge anchor
|
||||
val params = binding.btnMainFrame.layoutParams as ConstraintLayout.LayoutParams
|
||||
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
params.endToStart = ConstraintLayout.LayoutParams.UNSET
|
||||
params.endToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
binding.btnMainFrame.layoutParams = params
|
||||
|
||||
// Show horizontal timeline
|
||||
binding.rvHorizontalTimeline.visibility = android.view.View.VISIBLE
|
||||
binding.rvHorizontalTimeline.adapter = frameItemAdapter
|
||||
} else {
|
||||
isFrameModeOpen = false
|
||||
|
||||
// Reset Frame button layout params to center chain
|
||||
val params = binding.btnMainFrame.layoutParams as ConstraintLayout.LayoutParams
|
||||
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
params.endToStart = binding.btnMainPreset.id
|
||||
params.endToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
binding.btnMainFrame.layoutParams = params
|
||||
|
||||
// Show preset button
|
||||
binding.btnMainPreset.visibility = android.view.View.VISIBLE
|
||||
|
||||
// Hide timeline
|
||||
binding.rvHorizontalTimeline.visibility = android.view.View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun togglePresetMode() {
|
||||
if (isCountingDown) return
|
||||
val wrapper = binding.buttonsWrapper
|
||||
TransitionManager.beginDelayedTransition(wrapper)
|
||||
|
||||
if (!isPresetModeOpen) {
|
||||
isPresetModeOpen = true
|
||||
isFrameModeOpen = false
|
||||
|
||||
// Hide frame button
|
||||
binding.btnMainFrame.visibility = android.view.View.GONE
|
||||
|
||||
// Translate Preset button to left edge anchor (occupying Frame button start spot)
|
||||
val params = binding.btnMainPreset.layoutParams as ConstraintLayout.LayoutParams
|
||||
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
params.startToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
params.endToEnd = ConstraintLayout.LayoutParams.UNSET
|
||||
binding.btnMainPreset.layoutParams = params
|
||||
|
||||
// Show horizontal timeline
|
||||
binding.rvHorizontalTimeline.visibility = android.view.View.VISIBLE
|
||||
presetAdapter.resetSelection()
|
||||
binding.rvHorizontalTimeline.adapter = presetAdapter
|
||||
} else {
|
||||
isPresetModeOpen = false
|
||||
|
||||
// Reset Preset button layout params back to center chain
|
||||
val params = binding.btnMainPreset.layoutParams as ConstraintLayout.LayoutParams
|
||||
params.startToStart = ConstraintLayout.LayoutParams.UNSET
|
||||
params.startToEnd = binding.btnMainFrame.id
|
||||
params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
binding.btnMainPreset.layoutParams = params
|
||||
|
||||
// Show frame button
|
||||
binding.btnMainFrame.visibility = android.view.View.VISIBLE
|
||||
|
||||
// Hide timeline
|
||||
binding.rvHorizontalTimeline.visibility = android.view.View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAdvancedSliders(selectedPreset: ColorPreset) {
|
||||
editingPreset = selectedPreset.copy(
|
||||
basic = selectedPreset.basic.copy(),
|
||||
advanced = selectedPreset.advanced.copy(),
|
||||
toneCurve = selectedPreset.toneCurve.copy(),
|
||||
grain = selectedPreset.grain.copy()
|
||||
)
|
||||
|
||||
binding.layoutEditorTitleBar.visibility = android.view.View.VISIBLE
|
||||
|
||||
// Setup initial selected attribute: Brightness
|
||||
val initialNode = AttributeNode(EditAttribute.BRIGHTNESS, "Brightness (Độ sáng)", R.drawable.ic_attr_brightness)
|
||||
onAttributeSelected(initialNode)
|
||||
|
||||
binding.panelSelectionContainer.visibility = android.view.View.GONE
|
||||
binding.bottomControlPanel.visibility = android.view.View.GONE
|
||||
|
||||
binding.layoutTimelineEditor.apply {
|
||||
visibility = android.view.View.VISIBLE
|
||||
alpha = 0f
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.setDuration(250)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeAdvancedSliders() {
|
||||
binding.layoutTimelineEditor.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.withEndAction {
|
||||
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"
|
||||
}
|
||||
|
||||
val container = android.widget.FrameLayout(this).apply {
|
||||
setPadding(50, 20, 50, 20)
|
||||
addView(input)
|
||||
}
|
||||
|
||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("Lưu Preset cá nhân")
|
||||
.setView(container)
|
||||
.setPositiveButton("Lưu") { _, _ ->
|
||||
val name = input.text.toString().trim()
|
||||
if (name.isNotEmpty()) {
|
||||
saveCustomPreset(tempPreset, name)
|
||||
} else {
|
||||
Toast.makeText(this, "Tên không được để trống", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Hủy", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveCustomPreset(tempPreset: ColorPreset, newName: String) {
|
||||
val customId = "custom_" + System.currentTimeMillis()
|
||||
val customPreset = ColorPreset(
|
||||
id = customId,
|
||||
name = newName,
|
||||
themeCategory = "Custom",
|
||||
isEditable = true,
|
||||
basic = BasicAdjustments(
|
||||
tempPreset.basic.brightness,
|
||||
tempPreset.basic.contrast,
|
||||
tempPreset.basic.saturation,
|
||||
tempPreset.basic.vibrance,
|
||||
tempPreset.basic.temperature,
|
||||
tempPreset.basic.tint
|
||||
),
|
||||
advanced = AdvancedEffects(
|
||||
tempPreset.advanced.clarity,
|
||||
tempPreset.advanced.dehaze,
|
||||
tempPreset.advanced.vignetteAmount
|
||||
),
|
||||
toneCurve = ToneCurveEmulation(
|
||||
tempPreset.toneCurve.fadedBlackLevel,
|
||||
tempPreset.toneCurve.shadowsTintR,
|
||||
tempPreset.toneCurve.shadowsTintG,
|
||||
tempPreset.toneCurve.shadowsTintB
|
||||
),
|
||||
grain = FilmGrain(
|
||||
tempPreset.grain.grainAmount,
|
||||
tempPreset.grain.grainSize
|
||||
)
|
||||
)
|
||||
|
||||
// Serialize to JSON format manually (incorporating all 14 parameters)
|
||||
val jsonString = """
|
||||
{
|
||||
"id": "$customId",
|
||||
"name": "$newName",
|
||||
"theme_category": "Custom",
|
||||
"is_editable": true,
|
||||
"basic_adjustments": {
|
||||
"brightness": ${customPreset.basic.brightness},
|
||||
"contrast": ${customPreset.basic.contrast},
|
||||
"saturation": ${customPreset.basic.saturation},
|
||||
"vibrance": ${customPreset.basic.vibrance},
|
||||
"temperature": ${customPreset.basic.temperature},
|
||||
"tint": ${customPreset.basic.tint}
|
||||
},
|
||||
"advanced_effects": {
|
||||
"clarity": ${customPreset.advanced.clarity},
|
||||
"dehaze": ${customPreset.advanced.dehaze},
|
||||
"vignette_amount": ${customPreset.advanced.vignetteAmount}
|
||||
},
|
||||
"tone_curve_emulation": {
|
||||
"faded_black_level": ${customPreset.toneCurve.fadedBlackLevel},
|
||||
"shadows_tint_r": ${customPreset.toneCurve.shadowsTintR},
|
||||
"shadows_tint_g": ${customPreset.toneCurve.shadowsTintG},
|
||||
"shadows_tint_b": ${customPreset.toneCurve.shadowsTintB}
|
||||
},
|
||||
"film_grain": {
|
||||
"grain_amount": ${customPreset.grain.grainAmount},
|
||||
"grain_size": ${customPreset.grain.grainSize}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
try {
|
||||
val file = java.io.File(filesDir, "$customId.json")
|
||||
file.writeText(jsonString)
|
||||
|
||||
// Dynamic UI update
|
||||
colorPresets = colorPresets + customPreset
|
||||
presetAdapter.updatePresets(colorPresets)
|
||||
Toast.makeText(this, "Đã lưu preset: $newName", Toast.LENGTH_SHORT).show()
|
||||
closeAdvancedSliders()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Không thể lưu Preset file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
@@ -196,15 +838,15 @@ class MainActivity : AppCompatActivity() {
|
||||
} else {
|
||||
CameraSelector.DEFAULT_FRONT_CAMERA
|
||||
}
|
||||
binding.btnSwitchCamera.text = if (lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA) {
|
||||
getString(R.string.camera_front)
|
||||
} else {
|
||||
getString(R.string.camera_back)
|
||||
}
|
||||
// Collapse selections and reset overlay for clean transition
|
||||
stackAdapter.clearSelection()
|
||||
binding.rvSubFrames.visibility = android.view.View.GONE
|
||||
|
||||
// Reset Selector UI states
|
||||
if (isFrameModeOpen) toggleFrameMode()
|
||||
if (isPresetModeOpen) togglePresetMode()
|
||||
setFrameOverlay("")
|
||||
applyPresetFilter(colorPresets[0])
|
||||
updateFrameModeIcon(FrameItem("DEFAULT", "", ""))
|
||||
updatePresetModeIcon(colorPresets[0])
|
||||
|
||||
startCamera()
|
||||
}
|
||||
|
||||
@@ -214,16 +856,19 @@ class MainActivity : AppCompatActivity() {
|
||||
ImageCapture.FLASH_MODE_ON -> ImageCapture.FLASH_MODE_AUTO
|
||||
else -> ImageCapture.FLASH_MODE_OFF
|
||||
}
|
||||
|
||||
binding.btnFlash.text = when (flashMode) {
|
||||
ImageCapture.FLASH_MODE_ON -> getString(R.string.flash_on)
|
||||
ImageCapture.FLASH_MODE_AUTO -> getString(R.string.flash_auto)
|
||||
else -> getString(R.string.flash_off)
|
||||
}
|
||||
|
||||
updateFlashIconState(flashMode)
|
||||
imageCapture?.flashMode = flashMode
|
||||
}
|
||||
|
||||
private fun updateFlashIconState(flashMode: Int) {
|
||||
val color = if (flashMode == ImageCapture.FLASH_MODE_OFF) {
|
||||
ContextCompat.getColor(this, R.color.theme_white)
|
||||
} else {
|
||||
ContextCompat.getColor(this, R.color.theme_primary)
|
||||
}
|
||||
binding.btnFlash.imageTintList = android.content.res.ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
private fun toggleTimer() {
|
||||
if (isCountingDown) return
|
||||
timerSeconds = when (timerSeconds) {
|
||||
@@ -232,11 +877,16 @@ class MainActivity : AppCompatActivity() {
|
||||
5 -> 10
|
||||
else -> 0
|
||||
}
|
||||
updateTimerIconState(timerSeconds)
|
||||
}
|
||||
|
||||
binding.btnTimer.text = when (timerSeconds) {
|
||||
0 -> getString(R.string.timer_off)
|
||||
else -> getString(R.string.timer_seconds, timerSeconds)
|
||||
private fun updateTimerIconState(timerSecs: Int) {
|
||||
val color = if (timerSecs == 0) {
|
||||
ContextCompat.getColor(this, R.color.theme_white)
|
||||
} else {
|
||||
ContextCompat.getColor(this, R.color.theme_primary)
|
||||
}
|
||||
binding.btnTimer.imageTintList = android.content.res.ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
private fun applyZoom(ratio: Float) {
|
||||
@@ -259,15 +909,21 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun updateZoomButtonsUI(ratio: Float) {
|
||||
binding.btnZoom05.backgroundTintList = ContextCompat.getColorStateList(
|
||||
this, if (ratio == 0.5f) R.color.theme_primary else R.color.theme_item_card_bg
|
||||
)
|
||||
binding.btnZoom10.backgroundTintList = ContextCompat.getColorStateList(
|
||||
this, if (ratio == 1.0f) R.color.theme_primary else R.color.theme_item_card_bg
|
||||
)
|
||||
binding.btnZoom30.backgroundTintList = ContextCompat.getColorStateList(
|
||||
this, if (ratio == 3.0f) R.color.theme_primary else R.color.theme_item_card_bg
|
||||
)
|
||||
val selectedTv = when (ratio) {
|
||||
0.5f -> binding.tvZoom05
|
||||
1.0f -> binding.tvZoom1
|
||||
3.0f -> binding.tvZoom3
|
||||
else -> binding.tvZoom1
|
||||
}
|
||||
updateZoomTextState(selectedTv)
|
||||
}
|
||||
|
||||
private fun updateZoomTextState(selectedZoom: android.widget.TextView) {
|
||||
binding.tvZoom05.setTextColor(ContextCompat.getColor(this, R.color.theme_white))
|
||||
binding.tvZoom1.setTextColor(ContextCompat.getColor(this, R.color.theme_white))
|
||||
binding.tvZoom3.setTextColor(ContextCompat.getColor(this, R.color.theme_white))
|
||||
|
||||
selectedZoom.setTextColor(ContextCompat.getColor(this, R.color.theme_primary))
|
||||
}
|
||||
|
||||
private fun onCaptureClick() {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.photobooth.app
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
||||
class PresetAdapter(
|
||||
private var presets: List<ColorPreset>,
|
||||
private val onPresetSelected: (ColorPreset) -> Unit,
|
||||
private val onEditClicked: (ColorPreset) -> Unit
|
||||
) : RecyclerView.Adapter<PresetAdapter.ViewHolder>() {
|
||||
|
||||
private var selectedPosition = 0
|
||||
|
||||
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val cardPreset: MaterialCardView = view.findViewById(R.id.cardPreset)
|
||||
val viewPresetColor: View = view.findViewById(R.id.viewPresetColor)
|
||||
val txtPresetName: TextView = view.findViewById(R.id.txtPresetName)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_preset, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val preset = presets[position]
|
||||
holder.txtPresetName.text = preset.name
|
||||
|
||||
// Draw preview circle or block icon
|
||||
if (preset.id == "DEFAULT") {
|
||||
holder.viewPresetColor.setBackgroundResource(R.drawable.ic_block_white)
|
||||
holder.txtPresetName.text = holder.itemView.context.getString(R.string.default_frame)
|
||||
} else {
|
||||
val colorHex = when (preset.id) {
|
||||
"instax_faded_warm_01" -> "#FFF57C00"
|
||||
"instax_bw_cool" -> "#FF808080"
|
||||
"instax_cool_summer" -> "#FF00BCD4"
|
||||
else -> "#FF9E9E9E"
|
||||
}
|
||||
val circleDrawable = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.parseColor(colorHex))
|
||||
}
|
||||
holder.viewPresetColor.background = circleDrawable
|
||||
}
|
||||
|
||||
// Highlight selection
|
||||
val strokeWidthPx = (3 * holder.itemView.context.resources.displayMetrics.density).toInt()
|
||||
if (position == selectedPosition) {
|
||||
holder.cardPreset.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_primary)
|
||||
holder.cardPreset.strokeWidth = strokeWidthPx
|
||||
} else {
|
||||
holder.cardPreset.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_transparent)
|
||||
holder.cardPreset.strokeWidth = 0
|
||||
}
|
||||
|
||||
// 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
|
||||
var startY = 0f
|
||||
val touchSlop = 15f // pixels
|
||||
|
||||
val longPressRunnable = Runnable {
|
||||
if (preset.isEditable) {
|
||||
isLongPressTriggered = true
|
||||
holder.itemView.performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS)
|
||||
onEditClicked(preset)
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemView.setOnTouchListener { view, event ->
|
||||
when (event.action) {
|
||||
android.view.MotionEvent.ACTION_DOWN -> {
|
||||
isLongPressTriggered = false
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
handler.postDelayed(longPressRunnable, 800)
|
||||
}
|
||||
android.view.MotionEvent.ACTION_MOVE -> {
|
||||
if (Math.abs(event.x - startX) > touchSlop || Math.abs(event.y - startY) > touchSlop) {
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
}
|
||||
}
|
||||
android.view.MotionEvent.ACTION_UP -> {
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
if (!isLongPressTriggered) {
|
||||
val previousSelected = selectedPosition
|
||||
selectedPosition = holder.adapterPosition
|
||||
if (selectedPosition != RecyclerView.NO_POSITION) {
|
||||
notifyItemChanged(previousSelected)
|
||||
notifyItemChanged(selectedPosition)
|
||||
onPresetSelected(preset)
|
||||
view.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
android.view.MotionEvent.ACTION_CANCEL -> {
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = presets.size
|
||||
|
||||
fun updatePresets(newPresets: List<ColorPreset>) {
|
||||
this.presets = newPresets
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun resetSelection() {
|
||||
val previousSelected = selectedPosition
|
||||
selectedPosition = 0
|
||||
notifyItemChanged(previousSelected)
|
||||
notifyItemChanged(selectedPosition)
|
||||
}
|
||||
}
|
||||
@@ -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,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#40FF9800">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<stroke
|
||||
android:width="4dp"
|
||||
android:color="#FF9800" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -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="@color/theme_white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</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="@color/theme_white"
|
||||
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,3zm0,16H5V5h14v14zm-4,-4H9v-4h6v4z" />
|
||||
</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="@color/theme_white"
|
||||
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5 0,-0.61 -0.22,-1.19 -0.62,-1.64 -0.38,-0.43 -0.59,-0.99 -0.59,-1.56 0,-1.38 1.12,-2.5 2.5,-2.5H19c1.66,0 3,-1.34 3,-3 0,-4.97 -4.49,-9 -10,-9zm-5.5,9c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,8 6.5,8 8,8.67 8,9.5 7.33,11 6.5,11zm3,-4C8.67,7 8,6.33 8,5.5S8.67,4 9.5,4 11,4.67 11,5.5 10.33,7 9.5,7zm5,0c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,4 14.5,4 16,4.67 16,5.5 15.33,7 14.5,7zm3.5,4c-0.83,0 -1.5,-0.67 -1.5,-1.5S17.17,8 18,8s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.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="@color/theme_white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</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="@color/theme_white"
|
||||
android:pathData="M7,2v11h3v9l7-12h-4l4-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="@color/theme_white"
|
||||
android:pathData="M20,4h-3.17L15,2H9L7.17,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zm0,14H4V6h4.52l1.83,-2h3.3L15.48,6H20v12zm-8,-9c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2H16c0,-2.21 -1.79,-4 -4,-4zm0,6c2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2H8c0,2.21 1.79,4 4,4z" />
|
||||
</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="@color/theme_white"
|
||||
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,3zm0,16H5V5h14v14zm-5.04,-6.71l-2.75,3.54 -1.96,-2.36L6.5,17h11l-3.54,-4.71z" />
|
||||
</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="@color/theme_white"
|
||||
android:pathData="M8,5v14l11,-7z" />
|
||||
</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="@color/theme_white"
|
||||
android:pathData="M15,1L9,1v2h6L15,1zm-3,5c-4.42,0 -8,3.58 -8,8s3.58,8 8,8 8,-3.58 8,-8 -3.58,-8 -8,-8zm0,14c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6zm1,-9h-2v5h4v-2h-2L13,11z" />
|
||||
</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>
|
||||
@@ -16,54 +16,75 @@
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/btnFlash"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/theme_transparent"
|
||||
android:src="@drawable/ic_flash"
|
||||
app:tint="@color/theme_white"
|
||||
android:contentDescription="Flash Toggle" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:backgroundTint="@color/theme_item_card_bg"
|
||||
android:textColor="@color/theme_white"
|
||||
android:text="@string/flash_off"
|
||||
android:textSize="12sp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp" />
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
<ImageButton
|
||||
android:id="@+id/btnTimer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:backgroundTint="@color/theme_item_card_bg"
|
||||
android:textColor="@color/theme_white"
|
||||
android:text="@string/timer_off"
|
||||
android:textSize="12sp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp" />
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/theme_transparent"
|
||||
android:src="@drawable/ic_timer"
|
||||
app:tint="@color/theme_white"
|
||||
android:contentDescription="Timer Toggle" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSwitchCamera"
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:backgroundTint="@color/theme_item_card_bg"
|
||||
android:textColor="@color/theme_white"
|
||||
android:text="@string/camera_front"
|
||||
android:textSize="12sp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp" />
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSwitchCamera"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/theme_transparent"
|
||||
android:src="@drawable/ic_flip_camera"
|
||||
app:tint="@color/theme_white"
|
||||
android:contentDescription="Switch Camera" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ================= 2. KHUNG FRAME CHÍNH (CAMERA + OVERLAY) ================= -->
|
||||
<!-- 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="3:4"
|
||||
app:layout_constraintTop_toBottomOf="@id/topBar"
|
||||
app:layout_constraintBottom_toTopOf="@id/timelineContainer">
|
||||
app:layout_constraintBottom_toTopOf="@id/panelSelectionContainer">
|
||||
|
||||
<!-- Lớp đáy: Kính ngắm CameraX -->
|
||||
<androidx.camera.view.PreviewView
|
||||
@@ -71,6 +92,15 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- Lớp đè lọc màu (Filter Overlay): Hiển thị màu Preset cổ điển -->
|
||||
<ImageView
|
||||
android:id="@+id/imgFilterOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
android:contentDescription="Filter Overlay" />
|
||||
|
||||
<!-- Lớp đè (Overlay): Hiển thị Khung hình PNG do người dùng chọn -->
|
||||
<ImageView
|
||||
android:id="@+id/imgFrameOverlay"
|
||||
@@ -94,106 +124,237 @@
|
||||
android:shadowRadius="10"
|
||||
android:visibility="gone"
|
||||
android:text="3" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- ================= 3. TIMELINE DANH SÁCH STACK FRAMES ================= -->
|
||||
<LinearLayout
|
||||
android:id="@+id/timelineContainer"
|
||||
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
|
||||
<!-- Đặt ngay phía dưới Camera và không bị ai đè lên nữa -->
|
||||
<FrameLayout
|
||||
android:id="@+id/panelSelectionContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="96dp"
|
||||
android:background="#000000"
|
||||
app:layout_constraintTop_toBottomOf="@id/cameraContainer"
|
||||
app:layout_constraintBottom_toTopOf="@id/zoomControls">
|
||||
app:layout_constraintBottom_toTopOf="@id/bottomControlPanel">
|
||||
|
||||
<!-- TẦNG BỔ SUNG: RecyclerView hiển thị các mẫu khung con bên trong Stack -->
|
||||
<!-- Danh sách cuộn ngang -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvSubFrames"
|
||||
android:id="@+id/rvHorizontalTimeline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="6dp"
|
||||
android:background="@color/theme_sub_bar_bg"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="75dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- TẦNG GỐC: Danh sách các Stack chủ đề lớn -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvFrameStacks"
|
||||
<!-- LỚP TRÊN: Chứa 2 nút chức năng chính (Gốc ở giữa, động chuyển sang trái) -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/buttonsWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Nút Frame -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainFrame"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_default_frame_thumbnail"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnMainPreset"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="Frame Selector" />
|
||||
|
||||
<!-- Nút Presets Màu -->
|
||||
<ImageView
|
||||
android:id="@+id/btnMainPreset"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_default_preset_thumbnail"
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/btnMainFrame"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="Preset Color Selector" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<!-- ================= KHỐI 2: BOTTOM CONTROL PANEL (ĐIỀU KHIỂN CAMERA) ================= -->
|
||||
<!-- Khối này gom gọn cả nút Zoom, Shutter và nút Xem ảnh, xóa bỏ các dải nền thừa -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottomControlPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#000000"
|
||||
app:layout_constraintTop_toBottomOf="@id/panelSelectionContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<!-- CỤM ZOOM THUẦN (Xóa bỏ dải nền cũ) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutZoomGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp" />
|
||||
</LinearLayout>
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<!-- ================= 4. CỤM NÚT ĐIỀU KHIỂN ZOOM ================= -->
|
||||
<LinearLayout
|
||||
android:id="@+id/zoomControls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/theme_sub_bar_bg"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/timelineContainer"
|
||||
app:layout_constraintBottom_toTopOf="@id/controlPanel"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent">
|
||||
<TextView
|
||||
android:id="@+id/tvZoom05"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0.5x"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnZoom05"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:backgroundTint="@color/theme_item_card_bg"
|
||||
android:textColor="@color/theme_white"
|
||||
android:text="@string/zoom_05x"
|
||||
android:textSize="10sp"
|
||||
android:padding="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp" />
|
||||
<TextView
|
||||
android:id="@+id/tvZoom1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1x"
|
||||
android:textColor="@color/theme_primary"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnZoom10"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:backgroundTint="@color/theme_primary"
|
||||
android:textColor="@color/theme_white"
|
||||
android:text="@string/zoom_1x"
|
||||
android:textSize="10sp"
|
||||
android:padding="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnZoom30"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginHorizontal="2dp"
|
||||
android:backgroundTint="@color/theme_item_card_bg"
|
||||
android:textColor="@color/theme_white"
|
||||
android:text="@string/zoom_3x"
|
||||
android:textSize="10sp"
|
||||
android:padding="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ================= 5. CONTROL PANEL CHỤP ẢNH ================= -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/controlPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/theme_panel_dark"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/zoomControls">
|
||||
<TextView
|
||||
android:id="@+id/tvZoom3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="3x"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- NÚT XEM ẢNH (Biểu tượng Image/Gallery) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnCapture"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="@drawable/bg_capture_button"
|
||||
android:src="@drawable/ic_camera_white"
|
||||
android:contentDescription="@string/btn_capture_desc" />
|
||||
</RelativeLayout>
|
||||
android:id="@+id/btnGalleryReview"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_image_gallery"
|
||||
app:layout_constraintTop_toTopOf="@id/btnShutterCapture"
|
||||
app:layout_constraintBottom_toBottomOf="@id/btnShutterCapture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:contentDescription="Review Gallery" />
|
||||
|
||||
<!-- NÚT SHUTTER CHỤP ẢNH (Viền cam ruột trắng) -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnShutterCapture"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="@drawable/bg_shutter_orange_circle"
|
||||
android:layout_marginBottom="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layoutZoomGroup"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:contentDescription="Shutter Button" />
|
||||
|
||||
</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,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/theme_bg_dark"
|
||||
android:padding="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtDialogTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Chỉnh sửa Preset màu"
|
||||
android:textColor="@color/theme_white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="20dp"/>
|
||||
|
||||
<!-- Brightness Slider -->
|
||||
<TextView
|
||||
android:id="@+id/txtLabelBrightness"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/theme_white"
|
||||
android:textSize="12sp"
|
||||
android:text="Độ sáng (Brightness): 0.0" />
|
||||
<SeekBar
|
||||
android:id="@+id/seekBrightness"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="200"
|
||||
android:progress="100"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Contrast Slider -->
|
||||
<TextView
|
||||
android:id="@+id/txtLabelContrast"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/theme_white"
|
||||
android:textSize="12sp"
|
||||
android:text="Độ tương phản (Contrast): 0.0" />
|
||||
<SeekBar
|
||||
android:id="@+id/seekContrast"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="200"
|
||||
android:progress="100"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Saturation Slider -->
|
||||
<TextView
|
||||
android:id="@+id/txtLabelSaturation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/theme_white"
|
||||
android:textSize="12sp"
|
||||
android:text="Độ bão hòa (Saturation): 0.0" />
|
||||
<SeekBar
|
||||
android:id="@+id/seekSaturation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="200"
|
||||
android:progress="100"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Vibrance Slider -->
|
||||
<TextView
|
||||
android:id="@+id/txtLabelVibrance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/theme_white"
|
||||
android:textSize="12sp"
|
||||
android:text="Độ sống động (Vibrance): 0.0" />
|
||||
<SeekBar
|
||||
android:id="@+id/seekVibrance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="200"
|
||||
android:progress="100"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -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>
|
||||
@@ -2,8 +2,8 @@
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/cardSubFrame"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_width="68dp"
|
||||
android:layout_height="68dp"
|
||||
android:layout_margin="4dp"
|
||||
app:cardCornerRadius="6dp"
|
||||
app:cardElevation="1dp"
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgFrameThumb"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="Frame Thumb" />
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardPreset"
|
||||
android:layout_width="68dp"
|
||||
android:layout_height="68dp"
|
||||
android:layout_margin="4dp"
|
||||
app:cardCornerRadius="6dp"
|
||||
app:cardElevation="1dp"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardBackgroundColor="@color/theme_item_card_bg">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="4dp">
|
||||
|
||||
<!-- Vòng tròn màu xem trước bộ lọc -->
|
||||
<View
|
||||
android:id="@+id/viewPresetColor"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/ic_default_preset_thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtPresetName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/theme_white"
|
||||
android:textSize="10sp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:text="Preset" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</FrameLayout>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 411 KiB |
Reference in New Issue
Block a user