Implement 7_UX_BUTTONS: Restructured selections horizontal timeline layers to overlap behind translated anchor buttons, added long-click haptic feedback edit shortcut and custom sliders dialog to save custom JSON presets

This commit is contained in:
2026-07-05 17:27:10 +07:00
parent edd4c2946d
commit 4a33140c67
8 changed files with 876 additions and 121 deletions
@@ -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
@@ -71,8 +73,8 @@ class MainActivity : AppCompatActivity() {
FrameItem("photobooth_4strip", "Photobooth 4-Strip", "frames/photobooth_4strip.png")
)
// Initialize Color Presets matching 6_PRESETS_COLOR.md
colorPresets = listOf(
// Initialize default Color Presets
val defaultPresets = listOf(
ColorPreset(
id = "DEFAULT",
name = "Mặc định",
@@ -114,21 +116,69 @@ class MainActivity : AppCompatActivity() {
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 preset = ColorPreset(
id = id,
name = name,
themeCategory = category,
isEditable = true,
basic = BasicAdjustments(brightness, contrast, saturation, vibrance, 0f, 0f),
advanced = AdvancedEffects(0f, 0f, 0f),
toneCurve = ToneCurveEmulation(0f, 0f, 0f, 0f),
grain = FilmGrain(0f, 0f)
)
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 Shared Timeline RecyclerView
binding.rvSharedTimeline.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
binding.rvHorizontalTimeline.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
frameItemAdapter = FrameItemAdapter(allFrames) { frame ->
setFrameOverlay(frame.imageFileName)
updateFrameModeIcon(frame)
}
presetAdapter = PresetAdapter(colorPresets) { preset ->
presetAdapter = PresetAdapter(colorPresets, { preset ->
applyPresetFilter(preset)
updatePresetModeIcon(preset)
}
}, { preset ->
showEditPresetDialog(preset)
})
}
private fun setFrameOverlay(fileName: String) {
@@ -158,7 +208,14 @@ class MainActivity : AppCompatActivity() {
"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
else -> {
// Map user configured RGB overlay
val alpha = ((preset.basic.vibrance + 1.0f) * 40 + 20).toInt().coerceIn(10, 100)
val r = ((preset.basic.brightness + 1.0f) * 127).toInt().coerceIn(0, 255)
val g = ((preset.basic.contrast + 1.0f) * 100 + 20).toInt().coerceIn(0, 255)
val b = ((preset.basic.saturation + 1.0f) * 80 + 10).toInt().coerceIn(0, 255)
android.graphics.Color.argb(alpha, r, g, b)
}
}
binding.imgFilterOverlay.setBackgroundColor(filterColor)
}
@@ -166,21 +223,21 @@ class MainActivity : AppCompatActivity() {
private fun updateFrameModeIcon(frame: FrameItem) {
if (frame.id == "DEFAULT" || frame.imageFileName.isEmpty()) {
binding.btnModeFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
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.btnModeFrame.setImageBitmap(bitmap)
binding.btnMainFrame.setImageBitmap(bitmap)
} catch (e: Exception) {
binding.btnModeFrame.setImageResource(R.drawable.ic_default_frame_thumbnail)
binding.btnMainFrame.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)
binding.btnMainPreset.setImageResource(R.drawable.ic_default_preset_thumbnail)
} else {
val colorHex = when (preset.id) {
"instax_faded_warm_01" -> "#FFF57C00"
@@ -192,17 +249,17 @@ class MainActivity : AppCompatActivity() {
shape = android.graphics.drawable.GradientDrawable.OVAL
setColor(android.graphics.Color.parseColor(colorHex))
}
binding.btnModePreset.setImageDrawable(circleDrawable)
binding.btnMainPreset.setImageDrawable(circleDrawable)
}
}
private fun setupCameraControls() {
// Mode Selectors
binding.btnModeFrame.setOnClickListener {
binding.btnMainFrame.setOnClickListener {
toggleFrameMode()
}
binding.btnModePreset.setOnClickListener {
binding.btnMainPreset.setOnClickListener {
togglePresetMode()
}
@@ -249,40 +306,249 @@ class MainActivity : AppCompatActivity() {
private fun toggleFrameMode() {
if (isCountingDown) return
val wrapper = binding.buttonsWrapper
TransitionManager.beginDelayedTransition(wrapper)
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
// Hide preset button
binding.btnMainPreset.visibility = android.view.View.GONE
binding.rvSharedTimeline.adapter = frameItemAdapter
// 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
// Collapse Frame UI, restore Preset button
binding.btnModePreset.visibility = android.view.View.VISIBLE
binding.rvSharedTimeline.visibility = android.view.View.GONE
// 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
// Show Shared timeline with Presets, hide Frame button
binding.btnModeFrame.visibility = android.view.View.GONE
binding.rvSharedTimeline.visibility = android.view.View.VISIBLE
// 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.rvSharedTimeline.adapter = presetAdapter
binding.rvHorizontalTimeline.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
// 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 showEditPresetDialog(preset: ColorPreset) {
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_preset, null)
val txtBrightness: android.widget.TextView = dialogView.findViewById(R.id.txtLabelBrightness)
val seekBrightness: android.widget.SeekBar = dialogView.findViewById(R.id.seekBrightness)
val txtContrast: android.widget.TextView = dialogView.findViewById(R.id.txtLabelContrast)
val seekContrast: android.widget.SeekBar = dialogView.findViewById(R.id.seekContrast)
val txtSaturation: android.widget.TextView = dialogView.findViewById(R.id.txtLabelSaturation)
val seekSaturation: android.widget.SeekBar = dialogView.findViewById(R.id.seekSaturation)
val txtVibrance: android.widget.TextView = dialogView.findViewById(R.id.txtLabelVibrance)
val seekVibrance: android.widget.SeekBar = dialogView.findViewById(R.id.seekVibrance)
// Set current values mapping (-1.0f..1.0f -> 0..200)
seekBrightness.progress = ((preset.basic.brightness + 1.0f) * 100).toInt()
seekContrast.progress = ((preset.basic.contrast + 1.0f) * 100).toInt()
seekSaturation.progress = ((preset.basic.saturation + 1.0f) * 100).toInt()
seekVibrance.progress = ((preset.basic.vibrance + 1.0f) * 100).toInt()
txtBrightness.text = "Độ sáng (Brightness): ${String.format("%.2f", preset.basic.brightness)}"
txtContrast.text = "Độ tương phản (Contrast): ${String.format("%.2f", preset.basic.contrast)}"
txtSaturation.text = "Độ bão hòa (Saturation): ${String.format("%.2f", preset.basic.saturation)}"
txtVibrance.text = "Độ sống động (Vibrance): ${String.format("%.2f", preset.basic.vibrance)}"
// Clone current preset to modify values temporarily
val tempPreset = ColorPreset(
id = preset.id,
name = preset.name,
themeCategory = preset.themeCategory,
isEditable = preset.isEditable,
basic = BasicAdjustments(preset.basic.brightness, preset.basic.contrast, preset.basic.saturation, preset.basic.vibrance, preset.basic.temperature, preset.basic.tint),
advanced = AdvancedEffects(preset.advanced.clarity, preset.advanced.dehaze, preset.advanced.vignetteAmount),
toneCurve = ToneCurveEmulation(preset.toneCurve.fadedBlackLevel, preset.toneCurve.shadowsTintR, preset.toneCurve.shadowsTintG, preset.toneCurve.shadowsTintB),
grain = FilmGrain(preset.grain.grainAmount, preset.grain.grainSize)
)
val seekBarListener = object : android.widget.SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
val valFloat = (progress - 100) / 100f
when (seekBar?.id) {
R.id.seekBrightness -> {
tempPreset.basic.brightness = valFloat
txtBrightness.text = "Độ sáng (Brightness): ${String.format("%.2f", valFloat)}"
}
R.id.seekContrast -> {
tempPreset.basic.contrast = valFloat
txtContrast.text = "Độ tương phản (Contrast): ${String.format("%.2f", valFloat)}"
}
R.id.seekSaturation -> {
tempPreset.basic.saturation = valFloat
txtSaturation.text = "Độ bão hòa (Saturation): ${String.format("%.2f", valFloat)}"
}
R.id.seekVibrance -> {
tempPreset.basic.vibrance = valFloat
txtVibrance.text = "Độ sống động (Vibrance): ${String.format("%.2f", valFloat)}"
}
}
// Live preview camera filter overlay while sliding seekbar!
applyPresetFilter(tempPreset)
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {}
}
seekBrightness.setOnSeekBarChangeListener(seekBarListener)
seekContrast.setOnSeekBarChangeListener(seekBarListener)
seekSaturation.setOnSeekBarChangeListener(seekBarListener)
seekVibrance.setOnSeekBarChangeListener(seekBarListener)
androidx.appcompat.app.AlertDialog.Builder(this)
.setView(dialogView)
.setPositiveButton("Lưu") { _, _ ->
showSavePresetDialog(tempPreset)
}
.setNegativeButton("Hủy") { dialog, _ ->
applyPresetFilter(preset) // Restore
dialog.dismiss()
}
.show()
}
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, 0f, 0f),
advanced = AdvancedEffects(0f, 0f, 0f),
toneCurve = ToneCurveEmulation(0f, 0f, 0f, 0f),
grain = FilmGrain(0f, 0f)
)
// Serialize to JSON format manually
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": 0.0,
"tint": 0.0
},
"advanced_effects": {
"clarity": 0.0,
"dehaze": 0.0,
"vignette_amount": 0.0
},
"tone_curve_emulation": {
"faded_black_level": 0.0,
"shadows_tint_r": 0.0,
"shadows_tint_g": 0.0,
"shadows_tint_b": 0.0
},
"film_grain": {
"grain_amount": 0.0,
"grain_size": 0.0
}
}
""".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()
} catch (e: Exception) {
Toast.makeText(this, "Không thể lưu Preset file", Toast.LENGTH_SHORT).show()
}
}
@@ -331,12 +597,8 @@ class MainActivity : AppCompatActivity() {
}
// 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
if (isFrameModeOpen) toggleFrameMode()
if (isPresetModeOpen) togglePresetMode()
setFrameOverlay("")
applyPresetFilter(colorPresets[0])
updateFrameModeIcon(FrameItem("DEFAULT", "", ""))
@@ -5,6 +5,7 @@ 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
@@ -12,15 +13,18 @@ import com.google.android.material.card.MaterialCardView
class PresetAdapter(
private var presets: List<ColorPreset>,
private val onPresetSelected: (ColorPreset) -> Unit
private val onPresetSelected: (ColorPreset) -> Unit,
private val onEditClicked: (ColorPreset) -> Unit
) : RecyclerView.Adapter<PresetAdapter.ViewHolder>() {
private var selectedPosition = 0 // Mặc định là Mặc định (0)
private var selectedPosition = 0
private var editingPosition = -1
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)
val btnSmallEdit: ImageButton = view.findViewById(R.id.btnSmallEdit)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -39,9 +43,9 @@ class PresetAdapter(
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
"instax_faded_warm_01" -> "#FFF57C00"
"instax_bw_cool" -> "#FF808080"
"instax_cool_summer" -> "#FF00BCD4"
else -> "#FF9E9E9E"
}
val circleDrawable = GradientDrawable().apply {
@@ -51,8 +55,15 @@ class PresetAdapter(
holder.viewPresetColor.background = circleDrawable
}
val strokeWidthPx = (3 * holder.itemView.context.resources.displayMetrics.density).toInt()
// Show edit button on long press
if (position == editingPosition && preset.isEditable) {
holder.btnSmallEdit.visibility = View.VISIBLE
} else {
holder.btnSmallEdit.visibility = View.GONE
}
// 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
@@ -68,13 +79,36 @@ class PresetAdapter(
notifyItemChanged(selectedPosition)
onPresetSelected(preset)
}
holder.itemView.setOnLongClickListener {
if (preset.isEditable) {
val prevEditing = editingPosition
editingPosition = if (editingPosition == position) -1 else position
notifyItemChanged(prevEditing)
if (editingPosition != -1) {
notifyItemChanged(editingPosition)
}
holder.itemView.performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS)
}
true
}
holder.btnSmallEdit.setOnClickListener {
onEditClicked(preset)
}
}
override fun getItemCount(): Int = presets.size
fun updatePresets(newPresets: List<ColorPreset>) {
this.presets = newPresets
notifyDataSetChanged()
}
fun resetSelection() {
val previousSelected = selectedPosition
selectedPosition = 0
editingPosition = -1
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="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>
@@ -61,7 +61,7 @@
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
@@ -103,62 +103,64 @@
android:text="3" />
</FrameLayout>
<!-- ================= 3. TIMELINE DANH SÁCH FRAMES / PRESETS DÙNG CHUNG ================= -->
<LinearLayout
android:id="@+id/timelineContainer"
<!-- ================= 3. KHU VỰC ĐIỀU KHIỂN DÒNG (CHỒNG LỚP CUỘN ĐÈ) ================= -->
<FrameLayout
android:id="@+id/panelSelectionContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_height="70dp"
android:background="#000000"
app:layout_constraintTop_toBottomOf="@id/cameraContainer"
app:layout_constraintBottom_toTopOf="@id/toggleModeContainer">
app:layout_constraintBottom_toTopOf="@id/zoomControls">
<!-- LỚP ĐÁY: Danh sách cuộn ngang dùng chung cho cả Frame và Preset -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSharedTimeline"
android:id="@+id/rvHorizontalTimeline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:padding="6dp"
android:background="@color/theme_sub_bar_bg"
android:clipToPadding="false"
android:paddingStart="80dp"
android:paddingEnd="16dp"
android:visibility="gone" />
</LinearLayout>
<!-- ================= 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">
<!-- 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">
<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" />
<!-- 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" />
<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>
<!-- 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>
<!-- ================= 5. CỤM NÚT ĐIỀU KHIỂN ZOOM (TEXTVIEW) ================= -->
<!-- ================= 4. CỤM NÚT ĐIỀU KHIỂN ZOOM (TEXTVIEW) ================= -->
<LinearLayout
android:id="@+id/zoomControls"
android:layout_width="wrap_content"
@@ -166,7 +168,7 @@
android:orientation="horizontal"
android:background="@color/theme_transparent"
android:padding="4dp"
app:layout_constraintTop_toBottomOf="@id/toggleModeContainer"
app:layout_constraintTop_toBottomOf="@id/panelSelectionContainer"
app:layout_constraintBottom_toTopOf="@id/controlPanel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
@@ -205,7 +207,7 @@
android:padding="6dp" />
</LinearLayout>
<!-- ================= 6. CONTROL PANEL CHỤP ẢNH (PLAY + SHUTTER) ================= -->
<!-- ================= 5. CONTROL PANEL CHỤP ẢNH (PLAY + SHUTTER) ================= -->
<RelativeLayout
android:id="@+id/controlPanel"
android:layout_width="match_parent"
@@ -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>
@@ -1,39 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="4dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardPreset"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_margin="6dp"
app:cardCornerRadius="6dp"
app:cardElevation="1dp"
app:strokeWidth="0dp"
app:cardBackgroundColor="@color/theme_item_card_bg">
<!-- 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"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
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>
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>
<!-- Nút edit nhỏ ở góc trên bên phải, hiển thị đè đục lên card -->
<ImageButton
android:id="@+id/btnSmallEdit"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top|end"
android:background="@android:color/transparent"
android:src="@drawable/ic_edit_small"
android:visibility="gone"
app:tint="@color/theme_primary"
android:contentDescription="Edit Preset" />
</FrameLayout>