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