fix(이미지 선택): 이미지 선택 및 크롭 로직 수정
This commit is contained in:
@@ -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, // 회전
|
||||
|
||||
Reference in New Issue
Block a user