diff --git a/5_UI_ANDROID.md b/5_UI_ANDROID.md
new file mode 100644
index 0000000..bbcf05a
--- /dev/null
+++ b/5_UI_ANDROID.md
@@ -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
+
+
+
+
+
+
+
+```
+
+### 🔹 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
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 🔹 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
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 💻 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.
\ No newline at end of file
diff --git a/6_PRESETS_COLOR.md b/6_PRESETS_COLOR.md
new file mode 100644
index 0000000..c4b4455
--- /dev/null
+++ b/6_PRESETS_COLOR.md
@@ -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`.
\ No newline at end of file
diff --git a/android-project/app/src/main/java/com/photobooth/app/ColorPreset.kt b/android-project/app/src/main/java/com/photobooth/app/ColorPreset.kt
new file mode 100644
index 0000000..904b150
--- /dev/null
+++ b/android-project/app/src/main/java/com/photobooth/app/ColorPreset.kt
@@ -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
+)
diff --git a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt
index 8a78cb6..65c90a4 100644
--- a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt
+++ b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt
@@ -31,11 +31,16 @@ 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
- private lateinit var stackAdapter: FrameStackAdapter
+ // Frame & Preset UI selectors state
+ private lateinit var allFrames: List
+ private lateinit var colorPresets: List
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -45,7 +50,7 @@ class MainActivity : AppCompatActivity() {
// Setup UI control listeners
setupCameraControls()
- // Setup Frame selection lists
+ // Setup Frame/Preset selection lists
initFrameData()
setupRecyclerViews()
@@ -60,43 +65,70 @@ 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 Color Presets matching 6_PRESETS_COLOR.md
+ colorPresets = 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)
+ )
)
}
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.rvSharedTimeline.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+
+ frameItemAdapter = FrameItemAdapter(allFrames) { frame ->
setFrameOverlay(frame.imageFileName)
+ updateFrameModeIcon(frame)
+ }
+
+ presetAdapter = PresetAdapter(colorPresets) { preset ->
+ applyPresetFilter(preset)
+ updatePresetModeIcon(preset)
}
- binding.rvSubFrames.adapter = frameItemAdapter
}
private fun setFrameOverlay(fileName: String) {
@@ -116,43 +148,144 @@ class MainActivity : AppCompatActivity() {
}
}
+ private fun applyPresetFilter(preset: ColorPreset) {
+ currentSelectedPreset = preset
+ if (preset.id == "DEFAULT") {
+ binding.imgFilterOverlay.visibility = android.view.View.GONE
+ } else {
+ binding.imgFilterOverlay.visibility = android.view.View.VISIBLE
+ val filterColor = 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.TRANSPARENT
+ }
+ binding.imgFilterOverlay.setBackgroundColor(filterColor)
+ }
+ }
+
+ private fun updateFrameModeIcon(frame: FrameItem) {
+ if (frame.id == "DEFAULT" || frame.imageFileName.isEmpty()) {
+ binding.btnModeFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
+ } else {
+ try {
+ val inputStream = assets.open(frame.imageFileName)
+ val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream)
+ binding.btnModeFrame.setImageBitmap(bitmap)
+ } catch (e: Exception) {
+ binding.btnModeFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
+ }
+ }
+ }
+
+ private fun updatePresetModeIcon(preset: ColorPreset) {
+ if (preset.id == "DEFAULT") {
+ binding.btnModePreset.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.btnModePreset.setImageDrawable(circleDrawable)
+ }
+ }
+
private fun setupCameraControls() {
- // Toggle Switch Camera Button
+ // Mode Selectors
+ binding.btnModeFrame.setOnClickListener {
+ toggleFrameMode()
+ }
+
+ binding.btnModePreset.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.btnReviewGallery.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 {
onCaptureClick()
}
}
+ private fun toggleFrameMode() {
+ if (isCountingDown) return
+ if (!isFrameModeOpen) {
+ isFrameModeOpen = true
+ isPresetModeOpen = false
+
+ // Show Shared timeline under frame mode, hide preset button
+ binding.btnModePreset.visibility = android.view.View.GONE
+ binding.rvSharedTimeline.visibility = android.view.View.VISIBLE
+
+ binding.rvSharedTimeline.adapter = frameItemAdapter
+ } else {
+ isFrameModeOpen = false
+ // Collapse Frame UI, restore Preset button
+ binding.btnModePreset.visibility = android.view.View.VISIBLE
+ binding.rvSharedTimeline.visibility = android.view.View.GONE
+ }
+ }
+
+ private fun togglePresetMode() {
+ if (isCountingDown) return
+ if (!isPresetModeOpen) {
+ isPresetModeOpen = true
+ isFrameModeOpen = false
+
+ // Show Shared timeline with Presets, hide Frame button
+ binding.btnModeFrame.visibility = android.view.View.GONE
+ binding.rvSharedTimeline.visibility = android.view.View.VISIBLE
+
+ presetAdapter.resetSelection()
+ binding.rvSharedTimeline.adapter = presetAdapter
+ } else {
+ isPresetModeOpen = false
+ // Collapse Preset UI, restore Frame button
+ binding.btnModeFrame.visibility = android.view.View.VISIBLE
+ binding.rvSharedTimeline.visibility = android.view.View.GONE
+ }
+ }
+
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
@@ -196,15 +329,19 @@ 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
+ isFrameModeOpen = false
+ isPresetModeOpen = false
+ binding.btnModeFrame.visibility = android.view.View.VISIBLE
+ binding.btnModePreset.visibility = android.view.View.VISIBLE
+ binding.rvSharedTimeline.visibility = android.view.View.GONE
+
setFrameOverlay("")
+ applyPresetFilter(colorPresets[0])
+ updateFrameModeIcon(FrameItem("DEFAULT", "", ""))
+ updatePresetModeIcon(colorPresets[0])
+
startCamera()
}
@@ -214,16 +351,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 +372,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 +404,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() {
diff --git a/android-project/app/src/main/java/com/photobooth/app/PresetAdapter.kt b/android-project/app/src/main/java/com/photobooth/app/PresetAdapter.kt
new file mode 100644
index 0000000..a957d87
--- /dev/null
+++ b/android-project/app/src/main/java/com/photobooth/app/PresetAdapter.kt
@@ -0,0 +1,81 @@
+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.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.card.MaterialCardView
+
+class PresetAdapter(
+ private var presets: List,
+ private val onPresetSelected: (ColorPreset) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var selectedPosition = 0 // Mặc định là Mặc định (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" // Orange Warm
+ "instax_bw_cool" -> "#FF808080" // Gray B&W
+ "instax_cool_summer" -> "#FF00BCD4" // Cyan Cool
+ else -> "#FF9E9E9E"
+ }
+ val circleDrawable = GradientDrawable().apply {
+ shape = GradientDrawable.OVAL
+ setColor(Color.parseColor(colorHex))
+ }
+ holder.viewPresetColor.background = circleDrawable
+ }
+
+ 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
+ }
+
+ holder.itemView.setOnClickListener {
+ val previousSelected = selectedPosition
+ selectedPosition = position
+ notifyItemChanged(previousSelected)
+ notifyItemChanged(selectedPosition)
+ onPresetSelected(preset)
+ }
+ }
+
+ override fun getItemCount(): Int = presets.size
+
+ fun resetSelection() {
+ val previousSelected = selectedPosition
+ selectedPosition = 0
+ notifyItemChanged(previousSelected)
+ notifyItemChanged(selectedPosition)
+ }
+}
diff --git a/android-project/app/src/main/res/drawable/ic_default_frame_thumbnail.xml b/android-project/app/src/main/res/drawable/ic_default_frame_thumbnail.xml
new file mode 100644
index 0000000..fb2a05c
--- /dev/null
+++ b/android-project/app/src/main/res/drawable/ic_default_frame_thumbnail.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android-project/app/src/main/res/drawable/ic_default_preset_thumbnail.xml b/android-project/app/src/main/res/drawable/ic_default_preset_thumbnail.xml
new file mode 100644
index 0000000..742b372
--- /dev/null
+++ b/android-project/app/src/main/res/drawable/ic_default_preset_thumbnail.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android-project/app/src/main/res/drawable/ic_flash.xml b/android-project/app/src/main/res/drawable/ic_flash.xml
new file mode 100644
index 0000000..8e3ee7a
--- /dev/null
+++ b/android-project/app/src/main/res/drawable/ic_flash.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android-project/app/src/main/res/drawable/ic_flip_camera.xml b/android-project/app/src/main/res/drawable/ic_flip_camera.xml
new file mode 100644
index 0000000..8d1ebea
--- /dev/null
+++ b/android-project/app/src/main/res/drawable/ic_flip_camera.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android-project/app/src/main/res/drawable/ic_play_gallery.xml b/android-project/app/src/main/res/drawable/ic_play_gallery.xml
new file mode 100644
index 0000000..2aa8700
--- /dev/null
+++ b/android-project/app/src/main/res/drawable/ic_play_gallery.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android-project/app/src/main/res/drawable/ic_timer.xml b/android-project/app/src/main/res/drawable/ic_timer.xml
new file mode 100644
index 0000000..c92bc38
--- /dev/null
+++ b/android-project/app/src/main/res/drawable/ic_timer.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android-project/app/src/main/res/layout/activity_main.xml b/android-project/app/src/main/res/layout/activity_main.xml
index ed07f47..0e496f4 100644
--- a/android-project/app/src/main/res/layout/activity_main.xml
+++ b/android-project/app/src/main/res/layout/activity_main.xml
@@ -16,47 +16,45 @@
android:paddingHorizontal="16dp"
app:layout_constraintTop_toTopOf="parent">
-
+
+
+ android:layout_weight="1" />
-
+ 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" />
-
+ android:layout_weight="1" />
+
+
-
+
+
+
+
-
+
+ app:layout_constraintBottom_toTopOf="@id/toggleModeContainer">
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:padding="6dp" />
-
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:padding="6dp" />
-
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:padding="6dp" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+