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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user