10 KiB
Heavy Blur Background Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Replace the Cloudy blur library with an inlined Stackblur BlurTransformation applied via Coil's pipeline, producing a heavily blurred background where album art text is completely illegible.
Architecture: A self-contained BlurTransformation class is added to the project's util package. It is applied as a Coil ImageRequest transformation in BlurredBackground, replacing the .cloudy() modifier. Cloudy is then removed from all dependency files.
Tech Stack: Kotlin, Jetpack Compose, Coil 3 (io.coil-kt.coil3), Android Bitmap APIs
Task 1: Add BlurTransformation utility class
Files:
- Create:
app/src/main/java/xyz/cottongin/radio247/ui/util/BlurTransformation.kt
Step 1: Create the file with the Stackblur implementation
package xyz.cottongin.radio247.ui.util
import android.graphics.Bitmap
import coil3.size.Size
import coil3.transform.Transformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.abs
import kotlin.math.roundToInt
class BlurTransformation(
private val radius: Int = 25,
private val scale: Float = 0.1f
) : Transformation() {
override val cacheKey: String = "${javaClass.name}-$radius-$scale"
override suspend fun transform(input: Bitmap, size: Size): Bitmap =
input.stackBlur(scale, radius) ?: input
override fun equals(other: Any?): Boolean {
if (this === other) return true
return other is BlurTransformation && radius == other.radius && scale == other.scale
}
override fun hashCode(): Int = 31 * radius.hashCode() + scale.hashCode()
}
private suspend fun Bitmap.stackBlur(scale: Float, radius: Int): Bitmap? =
withContext(Dispatchers.IO) {
var sentBitmap = this@stackBlur
val width = (sentBitmap.width * scale).roundToInt()
val height = (sentBitmap.height * scale).roundToInt()
sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false)
val bitmap = sentBitmap.copy(sentBitmap.config ?: Bitmap.Config.ARGB_8888, true)
if (radius < 1) return@withContext null
val w = bitmap.width
val h = bitmap.height
val pix = IntArray(w * h)
bitmap.getPixels(pix, 0, w, 0, 0, w, h)
val wm = w - 1
val hm = h - 1
val wh = w * h
val div = radius + radius + 1
val r = IntArray(wh)
val g = IntArray(wh)
val b = IntArray(wh)
var rsum: Int; var gsum: Int; var bsum: Int
var x: Int; var y: Int; var i: Int; var p: Int; var yp: Int; var yi: Int
val vmin = IntArray(w.coerceAtLeast(h))
var divsum = div + 1 shr 1
divsum *= divsum
val dv = IntArray(256 * divsum) { it / divsum }
yi = 0; var yw = 0
val stack = Array(div) { IntArray(3) }
var stackpointer: Int; var stackstart: Int; var sir: IntArray; var rbs: Int
val r1 = radius + 1
var routsum: Int; var goutsum: Int; var boutsum: Int
var rinsum: Int; var ginsum: Int; var binsum: Int
y = 0
while (y < h) {
bsum = 0; gsum = 0; rsum = 0; boutsum = 0; goutsum = 0; routsum = 0
binsum = 0; ginsum = 0; rinsum = 0
i = -radius
while (i <= radius) {
p = pix[yi + wm.coerceAtMost(i.coerceAtLeast(0))]
sir = stack[i + radius]
sir[0] = p and 0xff0000 shr 16; sir[1] = p and 0x00ff00 shr 8; sir[2] = p and 0x0000ff
rbs = r1 - abs(i)
rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs
if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2] }
else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2] }
i++
}
stackpointer = radius; x = 0
while (x < w) {
r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]
rsum -= routsum; gsum -= goutsum; bsum -= boutsum
stackstart = stackpointer - radius + div
sir = stack[stackstart % div]
routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]
if (y == 0) vmin[x] = (x + radius + 1).coerceAtMost(wm)
p = pix[yw + vmin[x]]
sir[0] = p and 0xff0000 shr 16; sir[1] = p and 0x00ff00 shr 8; sir[2] = p and 0x0000ff
rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]
rsum += rinsum; gsum += ginsum; bsum += binsum
stackpointer = (stackpointer + 1) % div
sir = stack[stackpointer % div]
routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]
rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]
yi++; x++
}
yw += w; y++
}
x = 0
while (x < w) {
bsum = 0; gsum = 0; rsum = 0; boutsum = 0; goutsum = 0; routsum = 0
binsum = 0; ginsum = 0; rinsum = 0
yp = -radius * w; i = -radius
while (i <= radius) {
yi = 0.coerceAtLeast(yp) + x
sir = stack[i + radius]
sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]
rbs = r1 - abs(i)
rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs
if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2] }
else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2] }
if (i < hm) yp += w
i++
}
yi = x; stackpointer = radius; y = 0
while (y < h) {
pix[yi] = -0x1000000 and pix[yi] or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum]
rsum -= routsum; gsum -= goutsum; bsum -= boutsum
stackstart = stackpointer - radius + div
sir = stack[stackstart % div]
routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]
if (x == 0) vmin[y] = (y + r1).coerceAtMost(hm) * w
p = x + vmin[y]
sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]
rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]
rsum += rinsum; gsum += ginsum; bsum += binsum
stackpointer = (stackpointer + 1) % div
sir = stack[stackpointer]
routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]
rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]
yi += w; y++
}
x++
}
bitmap.setPixels(pix, 0, w, 0, 0, w, h)
sentBitmap.recycle()
bitmap
}
Note on Coil 3 API: Coil 3 uses
coil3.transform.Transformation(abstract class, not interface) andcoil3.size.Size. Thetransform()method signature issuspend fun transform(input: Bitmap, size: Size): Bitmap. Verify these imports compile — ifTransformationis an interface in the version in use, remove the()afterTransformation.
Step 2: Verify the file compiles
cd /path/to/Android-247-Radio
./gradlew :app:compileDebugKotlin 2>&1 | tail -20
Expected: BUILD SUCCESSFUL (or only pre-existing warnings, no new errors)
Step 3: Commit
git add app/src/main/java/xyz/cottongin/radio247/ui/util/BlurTransformation.kt
git commit -m "feat: add Stackblur BlurTransformation utility"
Task 2: Update BlurredBackground to use BlurTransformation
Files:
- Modify:
app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt:359-388
Step 1: Update the BlurredBackground composable
Find this block (lines ~368–378):
AsyncImage(
model = ImageRequest.Builder(context)
.data(artworkUrl)
.size(Size(10, 10))
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.cloudy(radius = 25)
)
Replace with:
AsyncImage(
model = ImageRequest.Builder(context)
.data(artworkUrl)
.transformations(BlurTransformation(radius = 25, scale = 0.1f))
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Step 2: Update imports in NowPlayingScreen.kt
Remove:
import com.skydoves.cloudy.cloudy
Add (if not auto-resolved by IDE):
import xyz.cottongin.radio247.ui.util.BlurTransformation
Also remove the coil.size.Size import if it was only used for the size(10, 10) call and is no longer referenced. Check for any remaining Size usage in the file first.
Step 3: Verify the file compiles
./gradlew :app:compileDebugKotlin 2>&1 | tail -20
Expected: BUILD SUCCESSFUL
Step 4: Commit
git add app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt
git commit -m "feat: replace cloudy blur with BlurTransformation on player background"
Task 3: Remove Cloudy dependency
Files:
- Modify:
app/build.gradle.kts - Modify:
gradle/libs.versions.toml
Step 1: Remove from app/build.gradle.kts
Remove this line:
implementation(libs.cloudy)
Step 2: Remove from gradle/libs.versions.toml
Remove the version entry:
cloudy = "0.2.7"
Remove the library entry:
cloudy = { group = "com.github.skydoves", name = "cloudy", version.ref = "cloudy" }
Step 3: Sync and verify full build
./gradlew :app:assembleDebug 2>&1 | tail -30
Expected: BUILD SUCCESSFUL
Step 4: Commit
git add app/build.gradle.kts gradle/libs.versions.toml
git commit -m "chore: remove Cloudy blur library dependency"
Verification
After all tasks complete, install on device/emulator and navigate to the Now Playing screen. Album art text should be completely illegible and no sharp pixel edges should be visible in the background.
If the blur is still not strong enough, increase scale down (e.g. 0.05f) or increase radius (e.g. radius = 50) in BlurTransformation(radius = 25, scale = 0.1f).