Crash with "trying to use a recycled bitmap" using Glide 4.11, can transformations cause this?

Viewed 483

I know there are already a bunch of questions related to the "trying to use a recycled bitmap" crash, but none helped me.

Details:

  • There are no calls to Bitmap.recycle() anywhere in this project
  • All images are loaded using Glide (4.11.0)
  • Glide calls are all simple, and not using placeholders
  • The crash seems to happen randomly when switching fragments

Only thing I could think of were the transformations. There are only 2 transformations in this project.

CircleTransformation (clip image as a circle with a custom radius):

class CircleTransformation(private val radius: Float) : BitmapTransformation() {
    companion object {
        private const val ID = "com.project.transformation.circle"
        private val ID_BYTES: ByteArray = ID.toByteArray()
    }

    public override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val paint = Paint()
        paint.isAntiAlias = true
        paint.shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)

        val halfWidth = source.width / 2f
        val output = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(output)
        canvas.drawCircle(
            halfWidth,
            (source.height / 2).toFloat(),
            halfWidth * radius,
            paint
        )
        return output
    }

    // Caching helpers
    override fun equals(other: Any?): Boolean {
        return other is CircleTransformation && other.hashCode() == hashCode()
    }
    override fun hashCode(): Int {
        return  ID.hashCode()
    }
    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        messageDigest.update(ID_BYTES)
    }
}

And ClipWhiteTransformation (remove white border from image):

class ClipWhiteTransformation() : BitmapTransformation() {
    companion object {
        private const val ID = "com.project.transformation.clipWhite"
        private val ID_BYTES: ByteArray = ID.toByteArray()

        // Config
        const val white = 253       // White pixel, if all channels are equal or greater than this
        const val transparent = 50  // Transparent pixel, if Less than this
    }

    public override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
        val width = source.width - 1
        val height = source.height - 1
        val halfX = width / 2
        val halfY = height / 2
        var startY = 0


        // Left Margin
        var left = 0
        for (x in 0 until halfX) {
            val pixel = source.getPixel(x, halfY)

            // Transparent?
            if (Color.alpha(pixel) < transparent) continue

            // Not white?
            if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
                left = x
                if (x > 2) {
                    startY = 2
                }
                break
            }
        }

        // Right Margin
        var right = 0
        for (x in 0 until halfX) {
            val pixel = source.getPixel(width - x, halfY)

            // Transparent?
            if (Color.alpha(pixel) < transparent) continue

            // Not white?
            if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
                right = x
                if (x > 2) {
                    startY = 2
                }
                break
            }
        }

        // Top Margin
        var top = 0
        for (y in startY until halfY) {
            val pixel = source.getPixel(halfX, y)

            // Transparent?
            if (Color.alpha(pixel) < transparent) continue

            // Not white?
            if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
                top = y
                break
            }
        }

        // Bottom Margin
        var bottom = 0
        for (y in startY until halfY) {
            val pixel = source.getPixel(halfX, height - y)

            // Transparent?
            if (Color.alpha(pixel) < transparent) continue

            // Not white?
            if (Color.red(pixel) < white || Color.green(pixel) < white || Color.blue(pixel) < white) {
                bottom = y
                break
            }
        }

        // Clip, scale and return
        val newWidth = width - (left + right)
        val newHeight = height - (top + bottom)
        val scale = if (abs(newWidth - outWidth) > abs(newHeight - outHeight)) outWidth / newWidth.toFloat() else outHeight / newHeight.toFloat()
        val matrix = Matrix().apply { setScale(scale, scale) }
        return Bitmap.createBitmap(source, left, top, newWidth, newHeight, matrix, false)
    }



    // Caching helpers
    override fun equals(other: Any?): Boolean {
        return other is ClipWhiteTransformation && other.hashCode() == hashCode()
    }
    override fun hashCode(): Int {
        return  ID.hashCode()
    }
    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
        messageDigest.update(ID_BYTES)
    }
}

Was using the BitmapPool initially, removing it didn't stop the crash.

By the way, this is the extension used to load images:

fun ImageView.setURL(url: String, 
                     @DrawableRes error: Int? = null, 
                     @DrawableRes placeholder: Int? = null, 
                     size: Int? = null, 
                     options: ((RequestBuilder<Drawable>) -> Unit)? = null, 
                     completion: ((resource: Drawable?) -> Unit)? = null) {

    // No URL, use Placeholder if exists, if not, use the error image
    if (url.isEmpty()) {
        placeholder?.also{ setImageResource(it) } ?: run { error?.also{ setImageResource(it) } }
        return
    }

    Glide.with(applicationInstance) // (I'm using an application instance directly here)
        .load(url).apply {
            completion?.also { completion ->
                this.listener(object : RequestListener<Drawable> {
                    override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
                        completion(null)
                        return false
                    }
                    override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                        completion(resource)
                        return false
                    }
                })
            }
        }
        .apply { size?.also { this.override(it)} }
        .apply { options?.invoke(this) }
        .placeholder(placeholder ?: 0)
        .error(error ?: 0)
        .transition(DrawableTransitionOptions.withCrossFade(350))
        .into(this)
}

Sorry for pasting so much code here (hope it's useful to someone). Can these transformations or loader cause the crash?

1 Answers

To shape(circle/square/oval) your image You do not need to Transform your image . MaterialDesign has introduce ShapeableImageView which let you shape your image at runtime, also you can add border with color .

  1. add matrial dependecies :

    implementation 'com.google.android.material:material:1.3.0-alpha01'
    
  2. Add shapeableImageView in your xyz.xml:

    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/imgStudent"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:shapeAppearanceOverlay="@style/circleImageView"
        android:padding="2dp"
        app:strokeColor="@color/white"
        app:strokeWidth="5dp"
        android:scaleType="centerCrop"
        android:adjustViewBounds="true"
        tools:srcCompat="@drawable/ic_kid_placeholder"
        />
    
  3. Add style inside res/values/style.xml file

    <style name="circleImageView" parent="">
            <item name="cornerFamily">rounded</item>
            <item name="cornerSize">50%</item>
            <item name="android:shadowRadius">100</item>
            <item name="android:shadowColor">@color/gray</item>
            <item name="backgroundOverlayColorAlpha">12</item>
    </style>
    

And at last load your image with glide .

Related