fix: 라이브 생성 이미지 선택
- 이미지 선택 및 Crop 방법 변경
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import com.yalantis.ucrop.UCrop
|
||||
import com.yalantis.ucrop.UCropActivity
|
||||
import kr.co.vividnext.sodalive.BuildConfig
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 단일 이미지 선택(13+ Photo Picker / 12- GetContent) → uCrop(기본 9:20) → [File, Uri] 반환
|
||||
* - 갤러리만 사용(카메라 X) → 런타임 권한 불필요
|
||||
* - 결과 파일은 cacheDir에 임시 생성 → 업로드 후 cleanup() 호출로 삭제
|
||||
*/
|
||||
class ImagePickerCropper(
|
||||
private val caller: ActivityResultCaller,
|
||||
private val context: Context,
|
||||
private val config: Config = Config(),
|
||||
private val onSuccess: (file: File, uri: Uri) -> Unit,
|
||||
private val onError: (Throwable) -> Unit = { it.printStackTrace() }
|
||||
) {
|
||||
data class Config(
|
||||
val aspectX: Float = 9f, // 고정 비율: 가로
|
||||
val aspectY: Float = 20f, // 고정 비율: 세로
|
||||
val maxWidth: Int? = null, // 예: 1080
|
||||
val maxHeight: Int? = null, // 예: 2400
|
||||
val compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
val compressQuality: Int = 90, // 0~100
|
||||
val useExternalCache: Boolean = false // 외부 캐시 사용 여부
|
||||
)
|
||||
|
||||
private var lastCroppedFile: File? = null
|
||||
|
||||
// 13+ : 시스템 Photo Picker
|
||||
private val pickPhoto: ActivityResultLauncher<PickVisualMediaRequest> =
|
||||
caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
|
||||
if (uri == null) onError(CancellationException("User cancelled picking."))
|
||||
else startCrop(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)
|
||||
}
|
||||
|
||||
// uCrop 결과 수신
|
||||
private val cropResult =
|
||||
caller.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
when (result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
val out = UCrop.getOutput(result.data!!)
|
||||
val file = lastCroppedFile
|
||||
if (out != null && file != null && file.exists()) {
|
||||
onSuccess(file, out)
|
||||
} else {
|
||||
onError(IllegalStateException("Crop finished but no output file/uri"))
|
||||
}
|
||||
}
|
||||
|
||||
UCrop.RESULT_ERROR -> onError(
|
||||
UCrop.getError(result.data!!) ?: RuntimeException("Crop error")
|
||||
)
|
||||
|
||||
else -> onError(CancellationException("User cancelled cropping."))
|
||||
}
|
||||
}
|
||||
|
||||
/** 외부에서 호출: 선택 → 크롭 시작 */
|
||||
fun launch() {
|
||||
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 startCrop(source: Uri) {
|
||||
val destFile = createTempCropFile()
|
||||
lastCroppedFile = destFile
|
||||
|
||||
val destUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider", // ★ Manifest와 동일
|
||||
destFile
|
||||
)
|
||||
|
||||
val options = UCrop.Options().apply {
|
||||
// 압축 포맷 & 품질 (uCrop 2.2.11 기준)
|
||||
setCompressionFormat(config.compressFormat)
|
||||
setCompressionQuality(config.compressQuality)
|
||||
|
||||
// 제스처/컨트롤 (필요시 조절)
|
||||
setFreeStyleCropEnabled(false)
|
||||
setHideBottomControls(false)
|
||||
setAllowedGestures(
|
||||
UCropActivity.SCALE, // Aspect 줄이진 못하지만 확대/축소
|
||||
UCropActivity.ROTATE, // 회전
|
||||
UCropActivity.ALL
|
||||
)
|
||||
|
||||
// (선택) UI 커스텀: 툴바/상태바/위젯 컬러 등
|
||||
// setToolbarColor(...)
|
||||
// setStatusBarColor(...)
|
||||
// setActiveControlsWidgetColor(...)
|
||||
}
|
||||
|
||||
var u = UCrop.of(source, destUri)
|
||||
.withAspectRatio(config.aspectX, config.aspectY)
|
||||
.withOptions(options)
|
||||
|
||||
if (config.maxWidth != null && config.maxHeight != null) {
|
||||
u = u.withMaxResultSize(config.maxWidth, config.maxHeight)
|
||||
}
|
||||
|
||||
cropResult.launch(u.getIntent(context))
|
||||
}
|
||||
|
||||
private fun createTempCropFile(): File {
|
||||
val base = if (config.useExternalCache) context.externalCacheDir ?: context.cacheDir
|
||||
else context.cacheDir
|
||||
|
||||
val ext = when (config.compressFormat) {
|
||||
Bitmap.CompressFormat.PNG -> "png"
|
||||
Bitmap.CompressFormat.WEBP -> "webp" // 일부 기기에서 deprecated 경고 가능
|
||||
else -> "jpg" // JPEG default
|
||||
}
|
||||
return File(base, "crop_${System.currentTimeMillis()}.$ext")
|
||||
}
|
||||
}
|
||||
|
||||
private class CancellationException(msg: String) : RuntimeException(msg)
|
||||
Reference in New Issue
Block a user