diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/image/BlurTransformation.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/image/BlurTransformation.kt new file mode 100644 index 00000000..ca6d3a53 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/image/BlurTransformation.kt @@ -0,0 +1,79 @@ +package kr.co.vividnext.sodalive.common.image + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Bitmap.Config.ARGB_8888 +import android.graphics.Paint +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicBlur +import androidx.annotation.RequiresApi +import androidx.core.graphics.applyCanvas +import coil.size.Size +import coil.transform.Transformation + +/** + * Coil 2.x 환경에서 기존 blur 호출부를 유지하기 위한 Android blur transformation이다. + */ +@RequiresApi(18) +class BlurTransformation @JvmOverloads constructor( + private val context: Context, + private val radius: Float = DEFAULT_RADIUS, + private val sampling: Float = DEFAULT_SAMPLING +) : Transformation { + + init { + require(radius in 0.0f..25.0f) { "radius must be in [0, 25]." } + require(sampling > 0f) { "sampling must be > 0." } + } + + override val cacheKey: String = "${BlurTransformation::class.java.name}-$radius-$sampling" + + @Suppress("DEPRECATION") + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) + val scaledWidth = (input.width / sampling).toInt().coerceAtLeast(1) + val scaledHeight = (input.height / sampling).toInt().coerceAtLeast(1) + val output = Bitmap.createBitmap(scaledWidth, scaledHeight, input.config ?: ARGB_8888) + + output.applyCanvas { + scale(1f / sampling, 1f / sampling) + drawBitmap(input, 0f, 0f, paint) + } + + var renderScript: RenderScript? = null + var inputAllocation: Allocation? = null + var outputAllocation: Allocation? = null + var blurScript: ScriptIntrinsicBlur? = null + + try { + renderScript = RenderScript.create(context) + inputAllocation = Allocation.createFromBitmap( + renderScript, + output, + Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SCRIPT + ) + outputAllocation = Allocation.createTyped(renderScript, inputAllocation.type) + blurScript = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)) + + blurScript.setRadius(radius) + blurScript.setInput(inputAllocation) + blurScript.forEach(outputAllocation) + outputAllocation.copyTo(output) + } finally { + blurScript?.destroy() + inputAllocation?.destroy() + outputAllocation?.destroy() + renderScript?.destroy() + } + + return output + } + + private companion object { + private const val DEFAULT_RADIUS = 10f + private const val DEFAULT_SAMPLING = 1f + } +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/common/image/BlurTransformationTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/common/image/BlurTransformationTest.kt new file mode 100644 index 00000000..554d44e2 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/common/image/BlurTransformationTest.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.common.image + +import android.content.Context +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class BlurTransformationTest { + + @Test + fun `cacheKey includes radius and sampling`() { + val transformation = BlurTransformation( + context = mockk(relaxed = true), + radius = 25f, + sampling = 2.5f + ) + + assertEquals( + "kr.co.vividnext.sodalive.common.image.BlurTransformation-25.0-2.5", + transformation.cacheKey + ) + } + + @Test + fun `constructor rejects radius above max`() { + assertThrows(IllegalArgumentException::class.java) { + BlurTransformation( + context = mockk(relaxed = true), + radius = 26f, + sampling = 2.5f + ) + } + } +} diff --git a/docs/20260420_BlurTransformation오류수정.md b/docs/20260420_BlurTransformation오류수정.md new file mode 100644 index 00000000..547299a6 --- /dev/null +++ b/docs/20260420_BlurTransformation오류수정.md @@ -0,0 +1,34 @@ +# 20260420 BlurTransformation 오류 수정 + +## 작업 체크리스트 +- [x] `BlurTransformation` 컴파일 오류의 실제 원인을 의존성 해석 기준으로 확정한다. + QA: `dependencyInsight` 결과에서 어떤 경로가 `io.coil-kt:coil`을 2.x로 올리는지 확인되어야 한다. +- [x] 최소 수정 방향을 정하고 실패 재현 경로를 확보한다. + QA: `:app:compileDebugKotlin` 또는 관련 단위 테스트가 수정 전 실패해야 한다. +- [x] Coil 2.x 환경에서 사용할 수 있는 로컬 `BlurTransformation` 호환 구현을 추가한다. + QA: 기존 호출부 시그니처 `(context, 25f, 2.5f)`를 유지한 채 컴파일 가능해야 한다. +- [x] `BlurTransformation` 사용처 import를 최소 범위로 교체한다. + QA: 기존 blur 사용 화면만 수정되고, 다른 Coil transform 사용처는 변경되지 않아야 한다. +- [x] 관련 테스트/컴파일 검증을 수행하고 결과를 기록한다. + QA: 관련 단위 테스트와 `:app:compileDebugKotlin` 결과를 문서 하단에 남겨야 한다. + +## 검증 기록 +- 2026-04-20 + - 무엇: `BlurTransformation` unresolved 오류의 원인을 `Daro -> Moloco -> coil-compose:2.2.2` 경로로 확정하고, Coil 2.x에서 사용할 로컬 `BlurTransformation` 호환 구현을 추가했다. + - 왜: `so.daro:daro-a:1.5.3` 추가 후 Moloco SDK가 `io.coil-kt:coil-compose:2.2.2`를 끌어오면서 앱의 `io.coil-kt:coil:1.4.0`이 2.2.2로 승격됐고, Coil 2에서 제거된 `coil.transform.BlurTransformation`만 컴파일 오류가 발생했기 때문이다. + - 어떻게: + - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/common/image/BlurTransformation.kt` + - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/audio_content/series/detail/SeriesDetailActivity.kt` + - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/chat/original/detail/OriginalWorkDetailActivity.kt` + - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt` + - 수정 파일: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/CreatorCommunityAdapter.kt` + - 추가 파일: `app/src/test/java/kr/co/vividnext/sodalive/common/image/BlurTransformationTest.kt` + - 실행 명령: `./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath --dependency io.coil-kt:coil` + - 결과: `so.daro:daro-a:1.5.3 -> com.google.ads.mediation:moloco -> com.moloco.sdk:moloco-sdk:4.1.1 -> io.coil-kt:coil-compose:2.2.2 -> io.coil-kt:coil:2.2.2` 경로를 확인했다. + - 실행 명령: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.image.BlurTransformationTest"` + - 결과: 수정 전에는 main source의 `BlurTransformation` unresolved로 실패했고, 호환 구현 추가 후 테스트 포함 `BUILD SUCCESSFUL`로 통과했다. + - 실행 명령: `./gradlew :app:compileDebugKotlin` + - 결과: `BUILD SUCCESSFUL` + - 실행 명령: `./gradlew :app:assembleDebug` + - 결과: `BUILD SUCCESSFUL` + - 진단 도구: Kotlin(`.kt`)용 LSP 서버 미구성으로 `lsp_diagnostics` 실행 불가 확인