fix(이미지 선택): 이미지 선택 및 크롭 로직 수정

This commit is contained in:
2025-09-18 00:17:20 +09:00
parent 02155065f7
commit d22907c7d5
16 changed files with 397 additions and 571 deletions

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
@@ -13,6 +14,7 @@ import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCropActivity
import kr.co.vividnext.sodalive.BuildConfig
import java.io.File
import java.io.FileOutputStream
/**
* 단일 이미지 선택(13+ Photo Picker / 12- GetContent) → uCrop(기본 9:20) → [File, Uri] 반환
@@ -20,8 +22,10 @@ import java.io.File
* - 결과 파일은 cacheDir에 임시 생성 → 업로드 후 cleanup() 호출로 삭제
*/
class ImagePickerCropper(
private val caller: ActivityResultCaller,
caller: ActivityResultCaller,
private val context: Context,
private val excludeGif: Boolean = false,
private val isEnabledFreeStyleCrop: Boolean = false,
private val config: Config = Config(),
private val onSuccess: (file: File, uri: Uri) -> Unit,
private val onError: (Throwable) -> Unit = { it.printStackTrace() }
@@ -41,15 +45,21 @@ class ImagePickerCropper(
// 13+ : 시스템 Photo Picker
private val pickPhoto: ActivityResultLauncher<PickVisualMediaRequest> =
caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri == null) onError(CancellationException("User cancelled picking."))
else startCrop(uri)
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
}
// 12- : SAF GetContent
private val pickContent: ActivityResultLauncher<String> =
caller.registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) onError(CancellationException("User cancelled picking."))
else startCrop(uri)
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
}
private val openDocument =
caller.registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) onError(CancellationException("이미지 선택을 취소했습니다."))
else handlePickedUri(uri)
}
// uCrop 결과 수신
@@ -62,38 +72,98 @@ class ImagePickerCropper(
if (out != null && file != null && file.exists()) {
onSuccess(file, out)
} else {
onError(IllegalStateException("Crop finished but no output file/uri"))
onError(IllegalStateException("이미지 크롭을 실패했습니다.\n다시 시도해 주세요"))
}
}
UCrop.RESULT_ERROR -> onError(
UCrop.getError(result.data!!) ?: RuntimeException("Crop error")
UCrop.getError(result.data!!)
?: RuntimeException("이미지 크롭을 실패했습니다.\n다시 시도해 주세요")
)
else -> onError(CancellationException("User cancelled cropping."))
else -> onError(CancellationException("이미지 크롭을 취소했습니다."))
}
}
/** 외부에서 호출: 선택 → 크롭 시작 */
fun launch() {
if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) {
pickPhoto.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
if (excludeGif) {
openDocument.launch(arrayOf("image/png", "image/jpg", "image/jpeg"))
} else {
pickContent.launch("image/*")
if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) {
pickPhoto.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
} else {
pickContent.launch("image/*")
}
}
}
/** 마지막 크롭 결과 파일 (있으면) */
fun getCroppedFile(): File? = lastCroppedFile?.takeIf { it.exists() }
/** 임시 파일 삭제 */
fun cleanup() {
lastCroppedFile?.let { if (it.exists()) it.delete() }
lastCroppedFile = null
}
private fun handlePickedUri(source: Uri) {
if (isGifUri(source)) {
// 1) 캐시에 gif 그대로 복사
val gifFile = copyUriToCacheAsGif(source)
lastCroppedFile = gifFile
val fileUri = FileProvider.getUriForFile(
context, "${BuildConfig.APPLICATION_ID}.fileprovider", gifFile
)
// 2) 바로 반환 (크롭 생략)
onSuccess(gifFile, fileUri)
} else {
// 기존 그대로: uCrop 9:20 실행
startCrop(source)
}
}
private fun isGifUri(uri: Uri): Boolean {
// 1순위: MIME type
val mime = context.contentResolver.getType(uri)
if (mime?.equals("image/gif", ignoreCase = true) == true) return true
// 2순위: 파일명 확장자
val name = getDisplayName(uri)?.lowercase()
return name?.endsWith(".gif") == true
}
private fun getDisplayName(uri: Uri): String? {
return context.contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null
)
?.use { c ->
val idx = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (idx >= 0 && c.moveToFirst()) c.getString(idx) else null
}
}
private fun copyUriToCacheAsGif(uri: Uri): File {
val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir
else context.cacheDir
// 원본 이름 유지 시도, 실패하면 타임스탬프
val name = getDisplayName(uri)?.takeIf { it.endsWith(".gif", true) }
?: "picked_${System.currentTimeMillis()}.gif"
val outFile = File(base, name)
context.contentResolver.openInputStream(uri).use { input ->
requireNotNull(input) { "Cannot open input stream for $uri" }
FileOutputStream(outFile).use { output ->
input.copyTo(output)
}
}
return outFile
}
private fun startCrop(source: Uri) {
val destFile = createTempCropFile()
lastCroppedFile = destFile
@@ -110,8 +180,8 @@ class ImagePickerCropper(
setCompressionQuality(config.compressQuality)
// 제스처/컨트롤 (필요시 조절)
setFreeStyleCropEnabled(false)
setHideBottomControls(false)
setFreeStyleCropEnabled(isEnabledFreeStyleCrop)
setHideBottomControls(true)
setAllowedGestures(
UCropActivity.SCALE, // Aspect 줄이진 못하지만 확대/축소
UCropActivity.ROTATE, // 회전