CameraX View와 함께 스크린샷

<?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 {/* 비트맵 변환 성공시 UI 업데이트 */
                            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 {
            // 비트맵을 PNG 형식으로 압축하여 출력 스트림에 쓰기 시도합니다.
            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)
    }
}
profile
클린코드와 UX를 생각하는 비즈니스 드리븐 소프트웨어 엔지니어입니다.

0개의 댓글