Implement 4_ANDROID_UI: Two-tier Frame Selector UI (RecyclerViews, Adapters, Real-time Overlay) with standardized resources (strings, colors, assets configurations)

This commit is contained in:
2026-07-05 16:18:50 +07:00
parent 9f9ec87d54
commit 53926a3f95
14 changed files with 721 additions and 82 deletions
+5
View File
@@ -36,6 +36,11 @@ android {
buildFeatures {
viewBinding = true // Bật ViewBinding để thao tác với giao diện dễ dàng
}
sourceSets {
getByName("main") {
assets.srcDirs("../../assets")
}
}
}
dependencies {
@@ -0,0 +1,76 @@
package com.photobooth.app
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
class FrameItemAdapter(
private var items: List<FrameItem>,
private val onFrameSelected: (FrameItem) -> Unit
) : RecyclerView.Adapter<FrameItemAdapter.ViewHolder>() {
private var selectedPosition = 0 // Mặc định ở vị trí 0 (Mặc định - Không dùng khung)
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val cardSubFrame: MaterialCardView = view.findViewById(R.id.cardSubFrame)
val imgFrameThumb: ImageView = view.findViewById(R.id.imgFrameThumb)
val txtFrameName: TextView = view.findViewById(R.id.txtFrameName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_frame_sub, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.txtFrameName.text = item.name
// Set thumbnail
if (item.id == "DEFAULT") {
holder.imgFrameThumb.setImageResource(R.drawable.ic_block_white)
holder.txtFrameName.text = holder.itemView.context.getString(R.string.default_frame)
} else {
try {
val inputStream = holder.itemView.context.assets.open("frames/${item.imageFileName}")
val bitmap = BitmapFactory.decodeStream(inputStream)
holder.imgFrameThumb.setImageBitmap(bitmap)
} catch (e: Exception) {
holder.imgFrameThumb.setImageDrawable(null)
}
}
val strokeWidthPx = (3 * holder.itemView.context.resources.displayMetrics.density).toInt()
if (position == selectedPosition) {
holder.cardSubFrame.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_primary)
holder.cardSubFrame.strokeWidth = strokeWidthPx
} else {
holder.cardSubFrame.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_transparent)
holder.cardSubFrame.strokeWidth = 0
}
holder.itemView.setOnClickListener {
val previousSelected = selectedPosition
selectedPosition = position
notifyItemChanged(previousSelected)
notifyItemChanged(selectedPosition)
onFrameSelected(item)
}
}
override fun getItemCount(): Int = items.size
fun updateItems(newItems: List<FrameItem>) {
items = newItems
selectedPosition = 0 // Reset to default when switching stack
notifyDataSetChanged()
}
}
@@ -0,0 +1,13 @@
package com.photobooth.app
data class FrameItem(
val id: String,
val name: String,
val imageFileName: String // Tên file PNG trong suốt nằm trong thư mục assets
)
data class FrameStack(
val id: String,
val themeName: String, // Ví dụ: "Retro", "Vintage", "Summer"
val frames: List<FrameItem> // Danh sách các frame thuộc chủ đề này
)
@@ -0,0 +1,65 @@
package com.photobooth.app
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 FrameStackAdapter(
private val stacks: List<FrameStack>,
private val onStackSelected: (FrameStack, Int) -> Unit
) : RecyclerView.Adapter<FrameStackAdapter.ViewHolder>() {
private var selectedPosition = -1
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val cardStack: MaterialCardView = view.findViewById(R.id.cardStack)
val txtStackName: TextView = view.findViewById(R.id.txtStackName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_frame_stack, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val stack = stacks[position]
holder.txtStackName.text = stack.themeName
val strokeWidthPx = (3 * holder.itemView.context.resources.displayMetrics.density).toInt()
if (position == selectedPosition) {
holder.cardStack.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_red_stroke)
holder.cardStack.strokeWidth = strokeWidthPx
} else {
holder.cardStack.strokeColor = ContextCompat.getColor(holder.itemView.context, R.color.theme_transparent)
holder.cardStack.strokeWidth = 0
}
holder.itemView.setOnClickListener {
val previousSelected = selectedPosition
selectedPosition = if (selectedPosition == position) {
-1 // Toggle collapse if clicked again
} else {
position
}
notifyItemChanged(previousSelected)
notifyItemChanged(selectedPosition)
onStackSelected(stack, selectedPosition)
}
}
override fun getItemCount(): Int = stacks.size
fun clearSelection() {
val previousSelected = selectedPosition
selectedPosition = -1
if (previousSelected != -1) {
notifyItemChanged(previousSelected)
}
}
}
@@ -13,6 +13,7 @@ import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.photobooth.app.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -30,6 +31,12 @@ 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
private lateinit var frameItemAdapter: FrameItemAdapter
private var currentSelectedFramePath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -38,6 +45,10 @@ class MainActivity : AppCompatActivity() {
// Setup UI control listeners
setupCameraControls()
// Setup Frame selection lists
initFrameData()
setupRecyclerViews()
// Request camera permission
if (allPermissionsGranted()) {
startCamera()
@@ -48,6 +59,63 @@ class MainActivity : AppCompatActivity() {
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun initFrameData() {
val retroFrames = listOf(
FrameItem("DEFAULT", "", ""), // Default/No Frame
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)
)
}
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 ->
setFrameOverlay(frame.imageFileName)
}
binding.rvSubFrames.adapter = frameItemAdapter
}
private fun setFrameOverlay(fileName: String) {
if (fileName.isEmpty()) {
binding.imgFrameOverlay.setImageDrawable(null)
currentSelectedFramePath = null
} else {
try {
val inputStream = assets.open(fileName)
val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream)
binding.imgFrameOverlay.setImageBitmap(bitmap)
currentSelectedFramePath = fileName
} catch (e: Exception) {
val errorMsg = getString(R.string.toast_frame_load_error, fileName)
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
}
}
}
private fun setupCameraControls() {
// Toggle Switch Camera Button
binding.btnSwitchCamera.setOnClickListener {
@@ -115,7 +183,8 @@ class MainActivity : AppCompatActivity() {
// Reset zoom to 1.0x on start/switch
applyZoom(1.0f)
} catch (xc: Exception) {
Toast.makeText(this, "Không thể khởi động Camera", Toast.LENGTH_SHORT).show()
val errorMsg = getString(R.string.toast_camera_start_error)
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
}
}, ContextCompat.getMainExecutor(this))
}
@@ -127,7 +196,15 @@ class MainActivity : AppCompatActivity() {
} else {
CameraSelector.DEFAULT_FRONT_CAMERA
}
binding.btnSwitchCamera.text = if (lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA) "🔄 Front" else "🔄 Back"
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
setFrameOverlay("")
startCamera()
}
@@ -139,9 +216,9 @@ class MainActivity : AppCompatActivity() {
}
binding.btnFlash.text = when (flashMode) {
ImageCapture.FLASH_MODE_ON -> "⚡ On"
ImageCapture.FLASH_MODE_AUTO -> "⚡ Auto"
else -> "⚡ Off"
ImageCapture.FLASH_MODE_ON -> getString(R.string.flash_on)
ImageCapture.FLASH_MODE_AUTO -> getString(R.string.flash_auto)
else -> getString(R.string.flash_off)
}
imageCapture?.flashMode = flashMode
@@ -157,8 +234,8 @@ class MainActivity : AppCompatActivity() {
}
binding.btnTimer.text = when (timerSeconds) {
0 -> "⏱️ Off"
else -> "⏱️ ${timerSeconds}s"
0 -> getString(R.string.timer_off)
else -> getString(R.string.timer_seconds, timerSeconds)
}
}
@@ -171,7 +248,8 @@ class MainActivity : AppCompatActivity() {
cameraControl?.setZoomRatio(ratio)
updateZoomButtonsUI(ratio)
} else {
Toast.makeText(this, "Thiết bị không hỗ trợ mức zoom ${ratio}x", Toast.LENGTH_SHORT).show()
val errorMsg = getString(R.string.toast_zoom_unsupported, ratio)
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
}
} ?: run {
// Fallback for emulator where zoomState might be null on start
@@ -181,14 +259,14 @@ class MainActivity : AppCompatActivity() {
}
private fun updateZoomButtonsUI(ratio: Float) {
binding.btnZoom05.backgroundTintList = android.content.res.ColorStateList.valueOf(
android.graphics.Color.parseColor(if (ratio == 0.5f) "#FF9800" else "#2D2D2D")
binding.btnZoom05.backgroundTintList = ContextCompat.getColorStateList(
this, if (ratio == 0.5f) R.color.theme_primary else R.color.theme_item_card_bg
)
binding.btnZoom10.backgroundTintList = android.content.res.ColorStateList.valueOf(
android.graphics.Color.parseColor(if (ratio == 1.0f) "#FF9800" else "#2D2D2D")
binding.btnZoom10.backgroundTintList = ContextCompat.getColorStateList(
this, if (ratio == 1.0f) R.color.theme_primary else R.color.theme_item_card_bg
)
binding.btnZoom30.backgroundTintList = android.content.res.ColorStateList.valueOf(
android.graphics.Color.parseColor(if (ratio == 3.0f) "#FF9800" else "#2D2D2D")
binding.btnZoom30.backgroundTintList = ContextCompat.getColorStateList(
this, if (ratio == 3.0f) R.color.theme_primary else R.color.theme_item_card_bg
)
}
@@ -214,7 +292,7 @@ class MainActivity : AppCompatActivity() {
val toneG = android.media.ToneGenerator(android.media.AudioManager.STREAM_ALARM, 50)
toneG.startTone(android.media.ToneGenerator.TONE_PROP_BEEP, 100)
} catch (e: Exception) {
// Fallback if ToneGenerator fails
// Fallback
}
}
@@ -234,7 +312,8 @@ class MainActivity : AppCompatActivity() {
} catch (e: Exception) {
// Fallback
}
Toast.makeText(this, "📸 Tách! Chụp ảnh thành công!", Toast.LENGTH_SHORT).show()
val successMsg = getString(R.string.toast_capture_success)
Toast.makeText(this, successMsg, Toast.LENGTH_SHORT).show()
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
@@ -251,7 +330,8 @@ class MainActivity : AppCompatActivity() {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this, "Quyền Camera bị từ chối.", Toast.LENGTH_SHORT).show()
val errorMsg = getString(R.string.toast_camera_permission_denied)
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
}
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#40FFFFFF">
<item>
<shape android:shape="oval">
<solid android:color="@color/theme_white" />
<stroke android:width="4dp" android:color="@color/theme_primary" />
<size android:width="64dp" android:height="64dp" />
</shape>
</item>
</ripple>
@@ -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.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm0,18c-4.41,0 -8,-3.59 -8,-8 0,-1.85 0.63,-3.55 1.69,-4.9L16.9,18.31C15.55,19.37 13.85,20 12,20zm6.31,-3.1L6.9,5.69C8.25,4.63 9.95,4 12,4c4.41,0 8,3.59 8,8 0,1.85 -0.63,3.55 -1.69,4.9z" />
</vector>
@@ -0,0 +1,12 @@
<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,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
<path
android:fillColor="@color/theme_white"
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
</vector>
@@ -3,99 +3,136 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#121212">
android:background="@color/theme_bg_dark">
<!-- Màn hình xem trước Camera -->
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/controlPanel" />
<!-- Thanh công cụ trên đầu (Translucent Black Top Bar) -->
<!-- ================= 1. TOP BAR CONTROL ================= -->
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#80000000"
android:background="@color/theme_bar_bg"
android:orientation="horizontal"
android:gravity="center"
android:paddingHorizontal="16dp"
app:layout_constraintTop_toTopOf="parent">
<!-- Nút Flash -->
<Button
android:id="@+id/btnFlash"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="4dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="⚡ Off"
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" />
<!-- Nút Hẹn giờ -->
<Button
android:id="@+id/btnTimer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="4dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="⏱️ Off"
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" />
<!-- Nút Đổi Camera -->
<Button
android:id="@+id/btnSwitchCamera"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="4dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="🔄 Front"
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" />
</LinearLayout>
<!-- Số đếm ngược khi hẹn giờ chụp -->
<TextView
android:id="@+id/txtTimerCountdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:textSize="100sp"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="10"
android:visibility="gone"
android:text="3"
app:layout_constraintTop_toTopOf="@id/viewFinder"
app:layout_constraintBottom_toBottomOf="@id/viewFinder"
app:layout_constraintLeft_toLeftOf="@id/viewFinder"
app:layout_constraintRight_toRightOf="@id/viewFinder" />
<!-- ================= 2. KHUNG FRAME CHÍNH (CAMERA + OVERLAY) ================= -->
<FrameLayout
android:id="@+id/cameraContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toBottomOf="@id/topBar"
app:layout_constraintBottom_toTopOf="@id/timelineContainer">
<!-- Thanh chọn Zoom ngay trên bảng điều khiển -->
<!-- Lớp đáy: Kính ngắm CameraX -->
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Lớp đè (Overlay): Hiển thị Khung hình PNG do người dùng chọn -->
<ImageView
android:id="@+id/imgFrameOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:contentDescription="Frame Overlay" />
<!-- Số đếm ngược khi hẹn giờ chụp -->
<TextView
android:id="@+id/txtTimerCountdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/theme_white"
android:textSize="100sp"
android:textStyle="bold"
android:shadowColor="@color/theme_panel_dark"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="10"
android:visibility="gone"
android:text="3" />
</FrameLayout>
<!-- ================= 3. TIMELINE DANH SÁCH STACK FRAMES ================= -->
<LinearLayout
android:id="@+id/zoomBar"
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">
<!-- 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: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 ================= -->
<LinearLayout
android:id="@+id/zoomControls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:background="#80000000"
android:orientation="horizontal"
android:background="@color/theme_sub_bar_bg"
android:padding="4dp"
app:layout_constraintTop_toBottomOf="@id/timelineContainer"
app:layout_constraintBottom_toTopOf="@id/controlPanel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
@@ -105,9 +142,9 @@
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="0.5x"
android:backgroundTint="@color/theme_item_card_bg"
android:textColor="@color/theme_white"
android:text="@string/zoom_05x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
@@ -118,9 +155,9 @@
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="#FF9800"
android:textColor="#FFFFFF"
android:text="1x"
android:backgroundTint="@color/theme_primary"
android:textColor="@color/theme_white"
android:text="@string/zoom_1x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
@@ -131,32 +168,32 @@
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="3x"
android:backgroundTint="@color/theme_item_card_bg"
android:textColor="@color/theme_white"
android:text="@string/zoom_3x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
</LinearLayout>
<!-- Bảng điều khiển phía dưới -->
<!-- ================= 5. CONTROL PANEL CHỤP ẢNH ================= -->
<RelativeLayout
android:id="@+id/controlPanel"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="#000000"
app:layout_constraintBottom_toBottomOf="parent">
android:layout_height="0dp"
android:background="@color/theme_panel_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/zoomControls">
<!-- Nút chụp hình -->
<ImageButton
android:id="@+id/btnCapture"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_centerInParent="true"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_camera"
android:contentDescription="Capture Button" />
android:background="@drawable/bg_capture_button"
android:src="@drawable/ic_camera_white"
android:contentDescription="@string/btn_capture_desc" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,23 @@
<?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/cardStack"
android:layout_width="120dp"
android:layout_height="50dp"
android:layout_margin="6dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
app:strokeWidth="0dp"
app:cardBackgroundColor="@color/theme_item_card_bg">
<TextView
android:id="@+id/txtStackName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@color/theme_white"
android:textSize="14sp"
android:textStyle="bold"
android:padding="8dp"
android:text="Stack 1" />
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,38 @@
<?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/cardSubFrame"
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">
<ImageView
android:id="@+id/imgFrameThumb"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="fitCenter"
android:contentDescription="Frame Thumb" />
<TextView
android:id="@+id/txtFrameName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/theme_white"
android:textSize="10sp"
android:singleLine="true"
android:ellipsize="end"
android:text="Frame" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="theme_bg_dark">#121212</color>
<color name="theme_panel_dark">#000000</color>
<color name="theme_bar_bg">#80000000</color>
<color name="theme_sub_bar_bg">#1E1E1E</color>
<color name="theme_item_card_bg">#2D2D2D</color>
<color name="theme_primary">#FF9800</color>
<color name="theme_white">#FFFFFF</color>
<color name="theme_red_stroke">#D32F2F</color>
<color name="theme_transparent">#00000000</color>
</resources>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Retro Photobooth</string>
<string name="btn_capture_desc">Capture Button</string>
<!-- Camera controls translation -->
<string name="flash_off">⚡ Off</string>
<string name="flash_on">⚡ On</string>
<string name="flash_auto">⚡ Auto</string>
<string name="timer_off">⏱️ Off</string>
<string name="timer_seconds">⏱️ %1$ds</string>
<string name="camera_front">🔄 Front</string>
<string name="camera_back">🔄 Back</string>
<string name="default_frame">Mặc định</string>
<string name="zoom_05x">0.5x</string>
<string name="zoom_1x">1x</string>
<string name="zoom_3x">3x</string>
<!-- User feedback toasts -->
<string name="toast_camera_start_error">Không thể khởi động Camera</string>
<string name="toast_camera_permission_denied">Quyền Camera bị từ chối.</string>
<string name="toast_zoom_unsupported">Thiết bị không hỗ trợ mức zoom %1$.1fx</string>
<string name="toast_capture_success">📸 Tách! Chụp ảnh thành công!</string>
<string name="toast_frame_load_error">Không thể tải khung hình: %1$s</string>
</resources>