화면 캡쳐 기능을 구현해보도록 하겠다.
가장 먼저 필요한 모듈을 implementation 해주겠다.
// Architectural Components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
// Coroutine Lifecycle Scopes
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
// Activity KTX for viewModels()
implementation "androidx.activity:activity-ktx:1.4.0"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"
// hilt
implementation "com.google.dagger:hilt-android:2.44"
implementation 'com.google.firebase:firebase-database-ktx:20.0.4'
implementation 'com.google.firebase:firebase-auth-ktx:21.0.3'
kapt "com.google.dagger:hilt-compiler:2.44"
Hilt를 사용하기 위해 BaseApp을 만들어주고
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class BaseApp : Application()
AndroidManifest.xml 파일에 다음을 추가해준다.
android:name=".BaseApp"
모듈을 생성해준다.
@Module
@InstallIn(SingletonComponent::class)
object ImageModule {
@Provides
fun provideContext(application: Application): Context {
return application
}
@Provides
fun provideImage(context : Context) : ImageRepository {
return ImageRepository(context)
}
}
레포지토리도 생성해준다.
class ImageRepository @Inject constructor(private val context : Context ){
fun saveImageToStorage(bitmap: Bitmap) : Flow<Status<String>> = flow{
emit(Status.Loading())
try {
val filename = "${System.currentTimeMillis()}.jpg"
var fos: OutputStream? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
context.contentResolver?.also { resolver ->
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
fos = imageUri?.let { resolver.openOutputStream(it) }
// 미디어 스캐닝 실행
MediaScannerConnection.scanFile(
context,
arrayOf(imageUri.toString()),
null
) { _, _ -> }
}
} else {
val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val image = File(imagesDir, filename)
fos = FileOutputStream(image)
// 미디어 스캐닝 실행
MediaScannerConnection.scanFile(
context,
arrayOf(image.absolutePath),
null
) { _, _ -> }
}
fos?.use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
emit(Status.Success())
}
fos?.close()
}
catch (e: Exception) {
emit(Status.Error(message = e.localizedMessage ?: ""))
}
}
}
안드로이드 SDK 29 버전 이후에는 정책이 바뀌었기 때문에 29 전 후로 다르게 처리를 해주어야 한다.
29 버전 이후에는 resolver.insert로 파일쓰기가 가능하지만, 이전버전에는 FIle로 가능하다.
또 사진을 저장한 후에는 미디어 스캐닝 과정이 진행이 되어야 갤러리에서 확인이 가능하기에 MediaSanner를 실행해준다.
각 과정은 Flow를 통해 흐름이 진행이되며, 함수 실행시 Status 상태가 Loading 상태이며,
성공시 Success를 emit하며 실패시 Error코드와 함께 Error를 emit한다.
상태는 다음과 같다.
sealed class Status<T>(val message: String? = null) {
class Success<T>() :Status<T>()
class Error<T>(message: String) : Status<T>(message)
class Loading<T>() : Status<T>()
}
viewModel을 작성하면
@HiltViewModel
class ImageViewModel
@Inject
constructor(
private var imageRepository: ImageRepository
) : ViewModel() {
private val _save = MutableStateFlow(ImageState())
val save : StateFlow<ImageState> = _save
fun saveImage(bitmap: Bitmap) {
imageRepository.saveImageToStorage(bitmap).onEach {
when(it) {
is Status.Loading -> {
_save.value = ImageState(isLoading = true)
}
is Status.Error -> {
_save.value = ImageState(error = it.message ?: "")
}
is Status.Success -> {
_save.value = ImageState(data = true)
}
}
}.launchIn(viewModelScope)
}
}
다음과 같은데, 상태의 변화에 따라 value값에 값을 저장해준다.
data class ImageState(
val error: String = "",
val isLoading: Boolean = false,
val data : Boolean? = null
)
MainActivity는 다음과 같다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val imageViewModel: ImageViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
lifecycle.coroutineScope.launchWhenCreated {
imageViewModel.save.collect{
if(it.isLoading){
Toast.makeText(this@MainActivity, "loading", Toast.LENGTH_SHORT).show()
}
else if (it.error.isNotBlank()) {
Toast.makeText(this@MainActivity, "Error : ${it.error}", Toast.LENGTH_SHORT).show()
}
it.data?.let {
Toast.makeText(this@MainActivity, "Save Image", Toast.LENGTH_SHORT).show()
}
}
}
binding.fab.setOnClickListener{
requestPermission {
val bitmap = getScreenShotFromView(binding.root)
if (bitmap != null) {
imageViewModel.saveImage(bitmap)
}
}
}
}
private fun requestPermission(logic : () -> Unit) {
TedPermission.create()
.setPermissionListener(object :PermissionListener{
override fun onPermissionGranted() {
logic()
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
Toast.makeText(this@MainActivity, "권한을 허가해주세요", Toast.LENGTH_SHORT).show()
}
}).setDeniedMessage("사진 저장 권한을 허용해주세요.").setPermissions(android.Manifest.permission.WRITE_EXTERNAL_STORAGE).check()
}
private fun getScreenShotFromView(v: View): Bitmap? {
var screenshot: Bitmap? = null
try {
screenshot =
Bitmap.createBitmap(v.measuredWidth, v.measuredHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(screenshot)
v.draw(canvas)
} catch (e: Exception) {
Log.e("GFG", "Failed to capture screenshot because:" + e.message)
}
return screenshot
}
}
사진 쓰기는 권한이 필요한 작업이기에 tedPermisstion을 이용하여 권한을 얻었다.
getScreenShotFromView의 경우에는 지정된 view를 bitmap 형태로 반환하는 함수이다.
'안드로이드' 카테고리의 다른 글
[Android] await()이 안될 때 (0) | 2023.09.01 |
---|---|
[Android] - zxing 라이브러리를 이용한 바코드 스캔 (0) | 2023.08.29 |
[Android] Hilt - Retrofit + ViewModel로 이해해보기 (0) | 2023.08.25 |
[Android] floating button Icon 색상 적용 안되는 문제 (0) | 2023.08.25 |
[Android] - Suggestion: add 'tools:replace="android:theme"' to <application> element at AndroidManifest.xml 에러 발생 (0) | 2023.07.13 |