Feat: Implement floating advanced sliders inside camera view finder with scale animation (9_FEAT_UI.md)

This commit is contained in:
2026-07-05 18:10:42 +07:00
parent 2308fe6fa9
commit 1a74887fe0
3 changed files with 360 additions and 124 deletions
+214
View File
@@ -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" <!-- To khong trng lt lòng xung quanh ging nh mu -->
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.
@@ -311,21 +311,21 @@ class MainActivity : AppCompatActivity() {
binding.sliderVibrance.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.basic.vibrance = value
binding.lblVibrance.text = "Vibrance (Bão hòa thông minh): ${String.format("%.2f", value)}"
binding.tvLabelVibrance.text = "Vibrance (Bão hòa thông minh): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderClarity.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.advanced.clarity = value
binding.lblClarity.text = "Clarity (Độ rõ nét vùng trung tính): ${String.format("%.2f", value)}"
binding.tvLabelClarity.text = "Clarity (Độ rõ nét vùng trung tính): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
binding.sliderDehaze.addOnChangeListener { _, value, _ ->
editingPreset?.let { preset ->
preset.advanced.dehaze = value
binding.lblDehaze.text = "Dehaze (Độ trong suốt / Sương mù): ${String.format("%.2f", value)}"
binding.tvLabelDehaze.text = "Dehaze (Độ trong suốt / Sương mù): ${String.format("%.2f", value)}"
applyPresetFilter(preset)
}
}
@@ -438,30 +438,34 @@ class MainActivity : AppCompatActivity() {
binding.sliderDehaze.value = editingPreset!!.advanced.dehaze
// Update text labels
binding.lblVibrance.text = "Vibrance (Bão hòa thông minh): ${String.format("%.2f", editingPreset!!.basic.vibrance)}"
binding.lblClarity.text = "Clarity (Độ rõ nét vùng trung tính): ${String.format("%.2f", editingPreset!!.advanced.clarity)}"
binding.lblDehaze.text = "Dehaze (Độ trong suốt / Sương mù): ${String.format("%.2f", editingPreset!!.advanced.dehaze)}"
binding.tvLabelVibrance.text = "Vibrance (Bão hòa thông minh): ${String.format("%.2f", editingPreset!!.basic.vibrance)}"
binding.tvLabelClarity.text = "Clarity (Độ rõ nét vùng trung tính): ${String.format("%.2f", editingPreset!!.advanced.clarity)}"
binding.tvLabelDehaze.text = "Dehaze (Độ trong suốt / Sương mù): ${String.format("%.2f", editingPreset!!.advanced.dehaze)}"
// Slide up overlay panel animation
binding.layoutAdvancedSliders.apply {
// Scale animation from center
binding.cardAdvancedSliders.apply {
visibility = android.view.View.VISIBLE
alpha = 0f
translationY = 200f
scaleX = 0.8f
scaleY = 0.8f
animate()
.alpha(1f)
.translationY(0f)
.setDuration(300)
.scaleX(1f)
.scaleY(1f)
.setDuration(250)
.setInterpolator(android.view.animation.OvershootInterpolator(1.2f))
.start()
}
}
private fun closeAdvancedSliders() {
binding.layoutAdvancedSliders.animate()
binding.cardAdvancedSliders.animate()
.alpha(0f)
.translationY(200f)
.setDuration(250)
.scaleX(0.8f)
.scaleY(0.8f)
.setDuration(200)
.withEndAction {
binding.layoutAdvancedSliders.visibility = android.view.View.GONE
binding.cardAdvancedSliders.visibility = android.view.View.GONE
currentSelectedPreset?.let { applyPresetFilter(it) }
}
.start()
@@ -101,117 +101,135 @@
android:shadowRadius="10"
android:visibility="gone"
android:text="3" />
<!-- LỚP 4: BẢNG SLIDERS FLOATING (Nằm lọt vào giữa hình ảnh camera) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardAdvancedSliders"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="24dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
android:visibility="gone"
app:strokeWidth="0dp"
android:backgroundTint="#A6000000">
<!-- ScrollView cho phép cuộn lên xuống tự do để chỉnh tất cả các thông số -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Tiêu đề & Nút X (Đóng) -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp">
<TextView
android:id="@+id/tvEditPresetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chỉnh sửa: Instax Nắng Chiều"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:textSize="15sp" />
<ImageButton
android:id="@+id/btnCancelEdit"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentEnd="true"
android:background="@android:color/transparent"
android:src="@drawable/ic_close_white"
app:tint="#FFFFFF"
android:contentDescription="Cancel Edit" />
</RelativeLayout>
<!-- ================= 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"
app:trackColorInactive="#333333" />
<!-- 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"
app:trackColorInactive="#333333" />
<!-- 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"
app:trackColorInactive="#333333" />
<!-- NÚT LƯU THÀNH PRESET MỚI -->
<Button
android:id="@+id/btnSaveCustomPreset"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:text="LƯU THÀNH PRESET MỚI"
android:textColor="#FFFFFF"
android:backgroundTint="#FF9800"
android:textStyle="bold"
android:insetTop="0dp"
android:insetBottom="0dp"
app:cornerRadius="4dp" />
</LinearLayout>
</ScrollView>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<!-- ================= BẢNG SLIDERS TÙY CHỈNH MÀU THỜI GIAN THỰC ================= -->
<LinearLayout
android:id="@+id/layoutAdvancedSliders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#D9000000"
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="@android:color/transparent"
android:src="@drawable/ic_close_white"
app:tint="@color/theme_white"
android:contentDescription="Cancel Edit" />
</RelativeLayout>
<!-- KHU VỰC CÁC THANH KÉO (SLIDERS) -->
<!-- 1. Thanh chỉnh VIBRANCE -->
<TextView
android:id="@+id/lblVibrance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Vibrance (Bão hòa thông minh): 0.0"
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:id="@+id/lblClarity"
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="#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:id="@+id/lblDehaze"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Dehaze (Độ trong suốt / Sương mù): 0.0"
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>
<!-- ================= KHỐI 1: TIMELINE FRAMES & PRESETS ================= -->
<!-- Đặt ngay phía dưới Camera và không bị ai đè lên nữa -->
<FrameLayout