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:
+233
@@ -0,0 +1,233 @@
|
||||
|
||||
|
||||
# 📱 KẾ HOẠCH PHÁT TRIỂN: GIAO DIỆN CHỌN FRAME THEO STACK (REAL-TIME OVERLAY)
|
||||
|
||||
Mục tiêu: Xây dựng bố cục giao diện gồm khu vực Top Bar điều khiển, Khung camera chính tích hợp Frame Overlay động, danh sách các Stack chủ đề dạng thanh trượt ngang (Timeline), cụm nút Zoom và Control Panel chụp ảnh phía dưới.
|
||||
|
||||
---
|
||||
|
||||
## 📁 1. Chuẩn Bị Cấu Trúc Dữ Liệu Model (Kotlin)
|
||||
|
||||
Để quản lý các Frame được chia theo từng Stack chủ đề như trong file `image_28b4c8.png`, chúng ta định nghĩa một cấu trúc dữ liệu rõ ràng:
|
||||
|
||||
```kotlin
|
||||
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
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 2. Cấu Trúc XML Bố Cục Giao Diện (`activity_main.xml`)
|
||||
|
||||
Bố cục được chia tầng bằng `ConstraintLayout` kết hợp với `ImageView` đóng vai trò làm lớp Overlay đè lên `PreviewView` của CameraX.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res/auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#121212">
|
||||
|
||||
<!-- ================= 1. TOP BAR CONTROL ================= -->
|
||||
<LinearLayout
|
||||
android:id="@+id/topBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
<!-- Các nút AUTO FLASH, TIMER OFF, FRONT CAMERA như trong image_28b4c8.png -->
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ================= 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">
|
||||
|
||||
<!-- 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" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- ================= 3. TIMELINE DANH SÁCH STACK FRAMES ================= -->
|
||||
<!-- Thanh trượt ngang hiển thị các Stack 1 Retro, Stack 2 Vintage... -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvFrameStacks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/cameraContainer" />
|
||||
|
||||
<!-- ================= 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:orientation="horizontal"
|
||||
android:background="#1A1A1A"
|
||||
android:padding="4dp"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/rvFrameStacks">
|
||||
<!-- Các nút 0.5x, 1x, 3x xếp ngang -->
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ================= 5. CONTROL PANEL CHỤP ẢNH ================= -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/controlPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/zoomControls">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCapture"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="@drawable/bg_capture_button"
|
||||
android:src="@drawable/ic_camera_white" />
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 3. Kịch Bản Logic Điều Khiển Giao Diện (Kotlin)
|
||||
|
||||
Quy trình tương tác khi người dùng thao tác trên giao diện timeline:
|
||||
|
||||
1. **Nạp danh sách Stack từ Assets:** Đọc dữ liệu.
|
||||
Ứng dụng quét thư mục `assets/frames/`, khởi tạo danh sách `List<FrameStack>` gồm các chủ đề kèm ảnh đại diện cho từng Stack rồi đẩy dữ liệu vào `rvFrameStacks`.
|
||||
|
||||
|
||||
2. **Người dùng chọn một Stack:** Sự kiện chạm.
|
||||
Khi chạm vào một Stack (ví dụ: "Stack 2 Vintage" trong file `image_28b4c8.png`), giao diện có thể bung ra một hàng ngang phụ (Sub-timeline) hiển thị các mẫu khung con bên trong hoặc tự động áp dụng khung mặc định đầu tiên của Stack đó.
|
||||
|
||||
|
||||
3. **Hiển thị Overlay thời gian thực:** Cập nhật UI.
|
||||
Đọc file ảnh PNG tương ứng từ assets bằng `BitmapFactory.decodeStream()`. Gán bitmap này vào `imgFrameOverlay`. Nhờ lớp `FrameLayout`, khung ảnh ngay lập tức hiển thị đè mịn màng lên luồng preview của camera.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📅 4. Các Đầu Việc Cần Làm Tiếp Theo (Checklist)
|
||||
|
||||
* [ ] **Tạo Adapter cho RecyclerView:** Viết `FrameStackAdapter` để hiển thị các ô vuông "Stack 1 Retro", "Stack 2 Vintage" với viền đỏ bo góc như thiết kế trong ảnh `image_28b4c8.png`.
|
||||
* [ ] **Xử lý hiệu ứng Selection:** Khi một Stack được bấm chọn, vẽ một viền màu nổi bật (hoặc đổi màu background của ô) để người dùng biết họ đang ở chế độ nào.
|
||||
* [ ] **Đồng bộ ảnh chụp cuối cùng:** Đảm bảo khi bấm chụp (`btnCapture`), thuật toán gộp ảnh sẽ lấy chính xác ID của frame đang hiển thị trên `imgFrameOverlay` để vẽ đè lên ảnh độ phân giải cao.
|
||||
|
||||
## 🛠️ 5. Cấu Trúc Lại XML Giao Diện (`activity_main.xml`)
|
||||
|
||||
Chúng ta sẽ chèn thêm một `RecyclerView` thứ hai nằm ngay trên danh sách các Stack để làm thanh chọn khung con.
|
||||
|
||||
```xml
|
||||
<!-- Thay thế phần 3 (TIMELINE DANH SÁCH STACK FRAMES) ở kế hoạch cũ bằng cụm này -->
|
||||
<LinearLayout
|
||||
android:id="@+id/timelineContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toBottomOf="@id/cameraContainer">
|
||||
|
||||
<!-- TẦNG BỔ SUNG: RecyclerView hiển thị các mẫu khung con bên trong Stack -->
|
||||
<!-- Mặc định khi mới mở app, thanh này ẩn (android:visibility="gone") -->
|
||||
<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="#1E1E1E" />
|
||||
|
||||
<!-- TẦNG GỐC (Giống ảnh image_28b4c8.png): 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>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 6. Cấu Trúc Logic Cập Nhật & Tạo Nút Mặc Định (Kotlin)
|
||||
|
||||
### 🔹 6.1. Cập nhật Model Dữ Liệu
|
||||
|
||||
Để tạo ra nút "Mặc định", chúng ta sẽ quy ước một đối tượng `FrameItem` đặc biệt với ID là `"NONE"` hoặc `"DEFAULT"`.
|
||||
|
||||
```kotlin
|
||||
// Hàm khởi tạo dữ liệu mẫu cho một Stack luôn đi kèm nút Mặc định ở đầu
|
||||
fun getFramesForStack(stackId: String): List<FrameItem> {
|
||||
val subFrames = mutableListOf<FrameItem>()
|
||||
|
||||
// Luôn thêm phần tử Mặc định (Không dùng khung) vào vị trí đầu tiên [0]
|
||||
subFrames.add(FrameItem(id = "DEFAULT", name = "Mặc định", imageFileName = ""))
|
||||
|
||||
// Tải các khung thực tế thuộc chủ đề từ Assets
|
||||
when(stackId) {
|
||||
"retro" -> {
|
||||
subFrames.add(FrameItem("r1", "Retro Polaroid", "retro_1.png"))
|
||||
subFrames.add(FrameItem("r2", "Retro Film 35mm", "retro_2.png"))
|
||||
}
|
||||
"vintage" -> {
|
||||
subFrames.add(FrameItem("v1", "Vintage Wood", "vintage_1.png"))
|
||||
}
|
||||
}
|
||||
return subFrames
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 🔹 6.2. Kịch Bản Tương Tác Hai Tầng (User Interaction Flow)
|
||||
|
||||
1. **Bước 1: Người dùng chạm vào Stack (Tầng 1):** Chọn chủ đề chính.
|
||||
Khi bấm vào "Stack 2 Vintage" (từ layout `image_28b4c8.png`), app kích hoạt hiển thị `rvSubFrames` (`visibility = View.VISIBLE`). Đồng thời nạp danh sách khung con tương ứng vào thanh này.
|
||||
|
||||
|
||||
2. **Bước 2: Người dùng chọn Khung con (Tầng 2):** Chọn mẫu cụ thể.
|
||||
Người dùng vuốt và chọn một mẫu khung con cụ thể. Nếu chọn trúng các mẫu như `v1`, `r1`, ảnh PNG tương ứng từ assets sẽ được render đè lên màn hình camera thông qua lớp `imgFrameOverlay`.
|
||||
|
||||
|
||||
3. **Bước 3: Quay về trạng thái Mặc định:** Hủy bỏ bộ lọc.
|
||||
Nếu người dùng không ưng ý, họ chạm vào phần tử đầu tiên (ô "Mặc định"). Logic code lập tức gọi lệnh `imgFrameOverlay.setImageDrawable(null)`, xóa bỏ hoàn toàn khung overlay hiện tại để camera quay về trạng thái gốc sạch sẽ.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📅 7. Các Đầu Việc Cần Triển Khai (Checklist)
|
||||
|
||||
* [ ] **Thiết kế UI cho ô "Mặc định":** Tạo một file ảnh icon hoặc vẽ một hình tròn có gạch chéo đỏ nhẹ (biểu tượng đóng/bỏ chọn) để làm ảnh đại diện (thumbnail) cho ô Mặc định trong danh sách con.
|
||||
* [ ] **Quản lý biến trạng thái toàn cục:** Tạo biến `private var currentSelectedFramePath: String? = null`. Nếu biến này bằng `null` (khi chọn mặc định), hàm chụp ảnh cuối cùng sẽ hiểu là chỉ lưu ảnh camera gốc mà không thực hiện gộp layer.
|
||||
* [ ] **Hiệu ứng thu gọn (Ẩn thanh con):** Cài đặt logic khi người dùng bấm lại vào chính Stack chủ đề đang chọn lần thứ hai, thanh `rvSubFrames` sẽ tự động ẩn đi (`View.GONE`) để giao diện gọn gàng hơn.
|
||||
@@ -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