<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/preview_bg"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="13:16"
app:layout_constraintEnd_toEndOf="@id/vertical_centerline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="120dp"
app:layout_constraintBottom_toBottomOf="@id/preview_bg"
app:layout_constraintEnd_toEndOf="@id/preview_bg"
app:layout_constraintStart_toStartOf="@id/preview_bg"
app:layout_constraintTop_toTopOf="@id/preview_bg" />
<ImageView
android:id="@+id/iv_preview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="120dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/preview_bg"
app:layout_constraintEnd_toEndOf="@id/preview_bg"
app:layout_constraintStart_toStartOf="@id/preview_bg"
app:layout_constraintTop_toTopOf="@id/preview_bg" />
<TextView
android:id="@+id/tv_tag"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="bottom"
android:ellipsize="end"
android:gravity="center"
android:text="텍스트"
android:textColor="@color/black"
android:textSize="55sp"
app:layout_constraintBottom_toBottomOf="@id/preview_bg"
app:layout_constraintEnd_toEndOf="@id/preview_bg"
app:layout_constraintStart_toStartOf="@id/preview_bg"
app:layout_constraintTop_toBottomOf="@id/preview" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/btn_capture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CAPTURE"
app:layout_constraintBottom_toTopOf="@id/btn_cancel"
app:layout_constraintStart_toEndOf="@id/preview_bg"
app:layout_constraintTop_toTopOf="@id/preview_bg" />
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CANCEL"
app:layout_constraintBottom_toTopOf="@id/btn_save"
app:layout_constraintEnd_toEndOf="@id/btn_capture"
app:layout_constraintStart_toEndOf="@id/preview_bg"
app:layout_constraintTop_toBottomOf="@id/btn_capture" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SAVE"
app:layout_constraintBottom_toBottomOf="@id/preview_bg"
app:layout_constraintEnd_toEndOf="@id/btn_capture"
app:layout_constraintStart_toStartOf="@+id/btn_capture"
app:layout_constraintTop_toBottomOf="@id/btn_cancel" />
<View
android:id="@+id/divider"
android:layout_width="1dp"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="@id/vertical_centerline"
app:layout_constraintStart_toStartOf="@id/vertical_centerline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".70" />
</androidx.constraintlayout.widget.ConstraintLayout>
package com.example.camerax
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.example.camerax.base.BaseActivity
import com.example.camerax.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Locale
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var cameraSelector: CameraSelector
private lateinit var imageCapture: ImageCapture
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat
.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
binding.btnCapture.setOnClickListener {
takePicture()
}
binding.btnSave.setOnClickListener {
saveScreenshot()
}
binding.btnCancel.setOnClickListener {
cancelCapture()
}
}
private fun takePicture() {
imageCapture.takePicture(
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
CoroutineScope(Dispatchers.IO).launch {
runCatching {
Glide.with(this@MainActivity)
.asBitmap()
.load(bytes)
.submit()
.get()
}.onSuccess {
withContext(Dispatchers.Main) {
binding.ivPreview.isVisible = true
if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
it
} else {
it.reverse()
}.let(binding.ivPreview::setImageBitmap)
}
}.onFailure {
it.printStackTrace()
}
}
super.onCaptureSuccess(image)
image.close()
}
override fun onError(exception: ImageCaptureException) {}
})
}
private fun Bitmap.reverse(): Bitmap =
Bitmap.createBitmap(
this, 0, 0, width, height,
Matrix().apply { setScale(-1f, 1f) }, false
)
private fun saveScreenshot() {
val screenshot = getBitmapFromView()
val filename = SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss-SSS",
Locale.KOREA
).format(System.currentTimeMillis()) + ".png"
saveBitmapToGallery(screenshot, filename)
}
private fun getBitmapFromView(): Bitmap {
val bitmap = Bitmap.createBitmap(
binding.previewBg.width,
binding.previewBg.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
binding.previewBg.draw(canvas)
return bitmap
}
private fun saveBitmapToGallery(bitmap: Bitmap, filename: String) {
val outputStream: OutputStream
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val imageUri = contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return
outputStream = contentResolver.openOutputStream(imageUri) ?: return
} else {
val imagesDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
.toString()
File(imagesDir).mkdirs()
val imageFile = File(imagesDir, filename)
outputStream = FileOutputStream(imageFile)
}
runCatching {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}.onFailure {
it.printStackTrace()
}.also {
runCatching {
outputStream.close()
}.onFailure {
it.printStackTrace()
}
}
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(binding.preview.surfaceProvider)
imageCapture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ImageCapture.Builder()
.setTargetRotation(this@MainActivity.display?.rotation ?: 0)
.build()
} else {
ImageCapture.Builder()
.setTargetRotation(windowManager.defaultDisplay.rotation)
.build()
}
cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
runCatching {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
}.onFailure {
it.printStackTrace()
}
}, ContextCompat.getMainExecutor(this))
}
private fun cancelCapture() {
binding.ivPreview.setImageBitmap(null)
binding.ivPreview.isVisible = false
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast
.makeText(this, "Permissions not granted by the user.", LENGTH_SHORT)
.show()
finish()
}
}
}
private fun allPermissionsGranted(): Boolean {
return REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
}
companion object {
private const val REQUEST_CODE_PERMISSIONS = 9999
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}