Implement 5_UI_ANDROID & 6_PRESETS_COLOR: Restructure to minimal UX/UI, merged frame/preset timeline, integrated ColorPreset models and real-time color filter overlay

This commit is contained in:
2026-07-05 17:10:03 +07:00
parent df1192dac4
commit edd4c2946d
13 changed files with 837 additions and 147 deletions
@@ -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
)
@@ -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<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
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() {
@@ -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<ColorPreset>,
private val onPresetSelected: (ColorPreset) -> Unit
) : RecyclerView.Adapter<PresetAdapter.ViewHolder>() {
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)
}
}
@@ -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="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="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="@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>
@@ -16,47 +16,45 @@
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) ================= -->
<!-- ================= 2. KHUNG FRAME CHÍNH (CAMERA + FILTER + FRAME OVERLAY) ================= -->
<FrameLayout
android:id="@+id/cameraContainer"
android:layout_width="match_parent"
@@ -71,6 +69,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"
@@ -96,88 +103,109 @@
android:text="3" />
</FrameLayout>
<!-- ================= 3. TIMELINE DANH SÁCH STACK FRAMES ================= -->
<!-- ================= 3. TIMELINE DANH SÁCH FRAMES / PRESETS DÙNG CHUNG ================= -->
<LinearLayout
android:id="@+id/timelineContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/cameraContainer"
app:layout_constraintBottom_toTopOf="@id/zoomControls">
app:layout_constraintBottom_toTopOf="@id/toggleModeContainer">
<!-- TẦNG BỔ SUNG: RecyclerView hiển thị các mẫu khung con bên trong Stack -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSubFrames"
android:id="@+id/rvSharedTimeline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="6dp"
android:background="@color/theme_sub_bar_bg"
android:visibility="gone" />
<!-- TẦNG GỐC: Danh sách các Stack chủ đề lớn -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFrameStacks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp" />
</LinearLayout>
<!-- ================= 4. CỤM NÚT ĐIỀU KHIỂN ZOOM ================= -->
<!-- ================= 4. TOGGLE CHẾ ĐỘ FRAME / PRESETS ================= -->
<LinearLayout
android:id="@+id/toggleModeContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginVertical="8dp"
app:layout_constraintTop_toBottomOf="@id/timelineContainer"
app:layout_constraintBottom_toTopOf="@id/zoomControls"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageView
android:id="@+id/btnModeFrame"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginHorizontal="12dp"
android:padding="4dp"
android:src="@drawable/ic_default_frame_thumbnail"
android:background="@color/theme_transparent"
android:scaleType="fitCenter"
android:contentDescription="Frame Selector" />
<ImageView
android:id="@+id/btnModePreset"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginHorizontal="12dp"
android:padding="4dp"
android:src="@drawable/ic_default_preset_thumbnail"
android:background="@color/theme_transparent"
android:scaleType="fitCenter"
android:contentDescription="Preset Color Selector" />
</LinearLayout>
<!-- ================= 5. CỤM NÚT ĐIỀU KHIỂN ZOOM (TEXTVIEW) ================= -->
<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:background="@color/theme_transparent"
android:padding="4dp"
app:layout_constraintTop_toBottomOf="@id/timelineContainer"
app:layout_constraintTop_toBottomOf="@id/toggleModeContainer"
app:layout_constraintBottom_toTopOf="@id/controlPanel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
android:id="@+id/btnZoom05"
<TextView
android:id="@+id/tvZoom05"
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="@color/theme_item_card_bg"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/theme_white"
android:text="@string/zoom_05x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
android:textSize="14sp"
android:textStyle="bold"
android:padding="6dp" />
<Button
android:id="@+id/btnZoom10"
<TextView
android:id="@+id/tvZoom1"
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="@color/theme_primary"
android:textColor="@color/theme_white"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/theme_primary"
android:text="@string/zoom_1x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
android:textSize="14sp"
android:textStyle="bold"
android:padding="6dp" />
<Button
android:id="@+id/btnZoom30"
<TextView
android:id="@+id/tvZoom3"
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="@color/theme_item_card_bg"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/theme_white"
android:text="@string/zoom_3x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
android:textSize="14sp"
android:textStyle="bold"
android:padding="6dp" />
</LinearLayout>
<!-- ================= 5. CONTROL PANEL CHỤP ẢNH ================= -->
<!-- ================= 6. CONTROL PANEL CHỤP ẢNH (PLAY + SHUTTER) ================= -->
<RelativeLayout
android:id="@+id/controlPanel"
android:layout_width="match_parent"
@@ -186,6 +214,20 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/zoomControls">
<!-- 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="@color/theme_transparent"
android:src="@drawable/ic_play_gallery"
app:tint="@color/theme_white"
android:contentDescription="Review Gallery" />
<!-- Nút Chụp (Ở giữa) -->
<ImageButton
android:id="@+id/btnCapture"
android:layout_width="64dp"
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-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/cardPreset"
android:layout_width="80dp"
android:layout_height="80dp"
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>