Implement 3_ANDROID_CAMERA: Advanced Camera features (Zoom controls, flash modes, front/back switch, and countdown timer)

This commit is contained in:
2026-07-05 12:48:21 +07:00
parent 70f8ef4da4
commit 9f9ec87d54
4 changed files with 437 additions and 5 deletions
@@ -5,7 +5,10 @@ import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
@@ -18,12 +21,24 @@ class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var cameraExecutor: ExecutorService
// Camera settings state
private var lensFacing = CameraSelector.DEFAULT_FRONT_CAMERA // Default front camera
private var flashMode = ImageCapture.FLASH_MODE_OFF // Default flash off
private var timerSeconds = 0 // Default no timer (0s)
private var cameraControl: CameraControl? = null // Controls zoom/flash
private var cameraInfo: CameraInfo? = null // Query capabilities
private var imageCapture: ImageCapture? = null // Capture usecase
private var isCountingDown = false // Countdown state flag
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Xin quyền Camera
// Setup UI control listeners
setupCameraControls()
// Request camera permission
if (allPermissionsGranted()) {
startCamera()
} else {
@@ -33,33 +48,214 @@ class MainActivity : AppCompatActivity() {
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun setupCameraControls() {
// Toggle Switch Camera Button
binding.btnSwitchCamera.setOnClickListener {
toggleCamera()
}
// Toggle Flash Button
binding.btnFlash.setOnClickListener {
toggleFlash()
}
// Toggle Timer Button
binding.btnTimer.setOnClickListener {
toggleTimer()
}
// Zoom 0.5x Button
binding.btnZoom05.setOnClickListener {
applyZoom(0.5f)
}
// Zoom 1x Button
binding.btnZoom10.setOnClickListener {
applyZoom(1.0f)
}
// Zoom 3x Button
binding.btnZoom30.setOnClickListener {
applyZoom(3.0f)
}
// Shutter Button
binding.btnCapture.setOnClickListener {
onCaptureClick()
}
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Cấu hình Kính ngắm (Preview)
// Preview configuration
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
// Chọn Camera Trước (Mặc định cho Photobooth)
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
// ImageCapture configuration
imageCapture = ImageCapture.Builder()
.setFlashMode(flashMode)
.build()
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview)
// Bind usecases to Lifecycle
val camera = cameraProvider.bindToLifecycle(
this, lensFacing, preview, imageCapture
)
cameraControl = camera.cameraControl
cameraInfo = camera.cameraInfo
// 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()
}
}, ContextCompat.getMainExecutor(this))
}
private fun toggleCamera() {
if (isCountingDown) return
lensFacing = if (lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA) {
CameraSelector.DEFAULT_BACK_CAMERA
} else {
CameraSelector.DEFAULT_FRONT_CAMERA
}
binding.btnSwitchCamera.text = if (lensFacing == CameraSelector.DEFAULT_FRONT_CAMERA) "🔄 Front" else "🔄 Back"
startCamera()
}
private fun toggleFlash() {
flashMode = when (flashMode) {
ImageCapture.FLASH_MODE_OFF -> ImageCapture.FLASH_MODE_ON
ImageCapture.FLASH_MODE_ON -> ImageCapture.FLASH_MODE_AUTO
else -> ImageCapture.FLASH_MODE_OFF
}
binding.btnFlash.text = when (flashMode) {
ImageCapture.FLASH_MODE_ON -> "⚡ On"
ImageCapture.FLASH_MODE_AUTO -> "⚡ Auto"
else -> "⚡ Off"
}
imageCapture?.flashMode = flashMode
}
private fun toggleTimer() {
if (isCountingDown) return
timerSeconds = when (timerSeconds) {
0 -> 3
3 -> 5
5 -> 10
else -> 0
}
binding.btnTimer.text = when (timerSeconds) {
0 -> "⏱️ Off"
else -> "⏱️ ${timerSeconds}s"
}
}
private fun applyZoom(ratio: Float) {
cameraInfo?.zoomState?.value?.let { currentState ->
val maxZoom = currentState.maxZoomRatio
val minZoom = currentState.minZoomRatio
if (ratio in minZoom..maxZoom) {
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()
}
} ?: run {
// Fallback for emulator where zoomState might be null on start
cameraControl?.setZoomRatio(ratio)
updateZoomButtonsUI(ratio)
}
}
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.btnZoom10.backgroundTintList = android.content.res.ColorStateList.valueOf(
android.graphics.Color.parseColor(if (ratio == 1.0f) "#FF9800" else "#2D2D2D")
)
binding.btnZoom30.backgroundTintList = android.content.res.ColorStateList.valueOf(
android.graphics.Color.parseColor(if (ratio == 3.0f) "#FF9800" else "#2D2D2D")
)
}
private fun onCaptureClick() {
if (isCountingDown) return
if (timerSeconds > 0) {
startCountDown()
} else {
takePicture()
}
}
private fun startCountDown() {
isCountingDown = true
binding.txtTimerCountdown.visibility = android.view.View.VISIBLE
val timer = object : android.os.CountDownTimer(timerSeconds * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
val secondsRemaining = (millisUntilFinished / 1000L) + 1
binding.txtTimerCountdown.text = secondsRemaining.toString()
try {
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
}
}
override fun onFinish() {
binding.txtTimerCountdown.visibility = android.view.View.GONE
isCountingDown = false
takePicture()
}
}
timer.start()
}
private fun takePicture() {
try {
val toneG = android.media.ToneGenerator(android.media.AudioManager.STREAM_ALARM, 100)
toneG.startTone(android.media.ToneGenerator.TONE_PROP_ACK, 250)
} catch (e: Exception) {
// Fallback
}
Toast.makeText(this, "📸 Tách! Chụp ảnh thành công!", Toast.LENGTH_SHORT).show()
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this, "Quyền Camera bị từ chối.", Toast.LENGTH_SHORT).show()
}
}
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
@@ -14,6 +14,132 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/controlPanel" />
<!-- Thanh công cụ trên đầu (Translucent Black Top Bar) -->
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#80000000"
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: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: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: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" />
<!-- Thanh chọn Zoom ngay trên bảng điều khiển -->
<LinearLayout
android:id="@+id/zoomBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:background="#80000000"
android:orientation="horizontal"
android:padding="4dp"
app:layout_constraintBottom_toTopOf="@id/controlPanel"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<Button
android:id="@+id/btnZoom05"
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="0.5x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
<Button
android:id="@+id/btnZoom10"
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="#FF9800"
android:textColor="#FFFFFF"
android:text="1x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
<Button
android:id="@+id/btnZoom30"
android:layout_width="50dp"
android:layout_height="36dp"
android:layout_marginHorizontal="2dp"
android:backgroundTint="#2D2D2D"
android:textColor="#FFFFFF"
android:text="3x"
android:textSize="10sp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp" />
</LinearLayout>
<!-- Bảng điều khiển phía dưới -->
<RelativeLayout
android:id="@+id/controlPanel"