How can I show a Drawable object in an Image? The ...
# compose
y
How can I show a Drawable object in an Image? The drawable is not from resources, its just an object from the Drawable class.
n
You can use a draw modifier and draw the drawble with the framework canvas
Copy code
Modifier.drawBehind { drawIntoCanvas -> drawable.setBounds(/* bounds */)
drawable.draw(canvas.nativeCanvas)
}
y
hmmm thanks ill try it
Um, I cannot make it work. I am trying to set the modifier on a Canvas but no matter what I change there is something red.
n
If you're using the Canvas composable directly you can just use the same drawing commands above in the trailing Lambda of the canvas composable
I.e. replace Modifier.drawBehind with Canvas etc.
y
Thanks I will try it now
Also wont this method cause performance issues? The app I am working on is a launcher and deals with lost of drawables (app icons)
lots*
n
It's just drawing using the same canvas facilities available within compose/traditional View framework. So there aren't any additional concerns with using Drawables here
y
Uh okay - Although I guess I will miss out on Glide/Coil features like caching and memory management!!? 🤔
n
I'm not sure I follow. This section is unrelated to image loading libraries
You can still load the drawables asynchronously and draw them using the code snippet above
You can also refer to "accompanist/coil at main · google/accompanist · GitHub" https://github.com/google/accompanist/tree/main/coil as an example for how to leverage coil with compose as well
y
Uh okay sorry, thanks anyways!
I did try coil accompanist but it does not support drawables outside of Resources
n
The same would apply for any drawable
Do you have an example for which this sample is not applicable?
y
All the examples work. I just said it would not accept actual Drawables as data. It can only handle drawables from resources. Not that it says it can. I just assumed it would since Coilt itself without accompanist can afaik.
n
Are you referring to compose or coil?
Because once you get a Drawable, regardless if the image is downloaded from the network or resources, the composable snippet above will support rendering it within a composition hierarchy
Could you share a code snippet that you are working with?
g
You can use the
Drawable.toPainter()
 function in the accompanist library
🙌 1
y
coil accompanist - but as you said its not really related to the topic of this channel
I can share a code snippet but I am not really trying to do anything special - just want to show a Drawable
I am trying to make the code you send work, I had to import a few different Canvas classes
@Gabriele Mariotti looks nice - thanks, ill try it now
n
The code snippet above will do that. You can obtain the Drawable from any source including your favorite image loading library and draw that drawable with that sample composable
🤔 1
g
Copy code
modifier = Modifier.drawBehind {

drawIntoCanvas { canvas ->
    drawable!!.setBounds(0, 0, size.width.toInt(), size.height.toInt())

    canvas.withSave {
       
        if (drawable.intrinsicWidth > 0 && drawable.intrinsicHeight > 0) {
            canvas.scale(
                sx = size.width / drawable.intrinsicWidth,
                sy = size.height / drawable.intrinsicHeight
            )
        }
        drawable.draw(canvas.nativeCanvas)
    }
}
y
@Gabriele Mariotti works perfectly thank you
@Gabriele Mariotti the new code you sent shows nothing for me - I am really new to Compose so I might be doing something wrong, I just set this modifier as on a Canvas(). Fortunately the Drawable.toPainter() method works fine but I should look more into it
n
If you are already using a Canvas composable you can replace the modifier snippet with the following:
Copy code
Canvas(my modifier){ drawIntoCanvas -> drawable.setBounds(/* bounds */)
drawable.draw(canvas.nativeCanvas)
}
The Canvas composable already accepts a DrawScope lambda to issue your drawing calls
y
🤔
n
Can you share a code snippet?
y
Copy code
@Composable
fun AppHome(
    app: App,
    activity: MainActivity
) {
    val drawable = app.requireIcon(activity)!!
    Image(
        painter = drawable.toPainter(),
        contentDescription = app.label,
        modifier = Modifier
            .requiredHeight(64.dp)
            .requiredWidth(64.dp)
            .padding(12.dp)
            .clickable {
                app.launch(activity)
            },
        alignment = Alignment.Center
    )
    
}
I just am using the toPainter method since it works fine for now. I am now trying to make a lazyVerticalGrid show these properly
n
That seems reasonable.
y
I made the lazy grid to work but now I cant properly size the images. I have ried all content scale options and the ones that should work look like this:
It looks like only 2 of the app icons are large enough - but I cannot scale them up for some reason
n
I think you want to try ContentScale.Crop
Also verify the size of your images. Looking at the screenshot it looks like there maybe a lot of additional transparent pixels
y
I dont really want crop. I just want Fit but it does not work
n
Looks like your original screenshot has FillHeight. Can you try with Fit?
y
Pretty much only fill ones have any effect
and yeah the icons might be small but I dont think that should matter since thats why I want to change the contentScale
n
It looks like some of then icons are doing the right thing but others are not. Can you verify the size of your icons? It might be the case that there are additional transparent pixels in the icon itself that is affecting the placement
y
And the extra space will be reduced later - I am just trying to make the base thing work
Ok let me see how I can get the exact size
these are the drawables intrinsic width and height
n
Now look at the icons themselves to see where the icon is relative to it's dimensions
That explains why contacts and phone look different than the others
y
Because they are smaller 😕
not larger
n
But based on the placement of the other icons it looks like they maybe placed in the upper left hand side of the icon itself
So I would recommend looking at those icons to confirm that they are centered within their bounds
y
I am sure they are centered since I have worked on the same project before on XML layouts and never had such a problem
n
I would double check
Since these icons are larger I would expect them to be centered in the drawing area but it looks like they are aligned to the top
And they are also aligned to the left instead
y
hmm let me see how I can check that
n
If you are loading the image from resources you can view them directly through android studio
y
the icons are being fetched directly from package manager - these are app icons
I think this means its centered? 🤔
Can it be related to LazyVerticalGrid?
n
I don't think so. You might be able to confirm by using column + row instead
It might be possible these icons are AdaptiveIconDrawables and behave slightly differently as well
y
n
Might be useful to log the type of drawable being parsed from packagemanager
y
I actually think I dont get the adaptive ones
The thing is that I dont have these issues on XML
let me check with Column and rows
Same result with column BUT when I remove the custom width and height it looks better
But I do want them to be in a specific size
n
Try adding a Modifier.background to the image composable as well to get a better sense of the sizing made to that composable from the parent
y
interesting result
no custom size
custom height
width* but does not matter really
n
Hmm I'm wondering if the size of the image composable is smaller than the intrinsic of the image itself. Maybe try using a larger width/height like 120dp and see if they changes something?
64dp I think would be smaller than the size of the icons you logged earlier which were 189 pixels
y
120
Yeah it is smaller but I want the icons to be small on the main screen since I have to fit 9 of them there
I should be able to down size them
n
I think you want ContentScale.Inside
y
Doesnt work
I want it to look like this - this is what I have created using xml layouts
n
I'm digging through the source of AdaptiveIconDrawable, might be handling its own scaling logic. Might be worth trying ContentScale.None to not apply any scaling algorithm at all for those Drawables
y
None looks like this
I think I have tried every single scaling mode - so potentially this is a bug in Compose? since what I want to achieve is pretty simple
n
I think it might be a corner case between using a specific drawable type. Can you share the conversion logic from Drawable to Painter?
y
Copy code
fun Drawable.toPainter(): Painter = when (this) {
    is BitmapDrawable -> BitmapPainter(bitmap.asImageBitmap())
    is ColorDrawable -> ColorPainter(Color(color))
    else -> AndroidDrawablePainter(mutate())
}
n
And AndroidDrawablePainter?
y
Its a 50 line class
Copy code
class AndroidDrawablePainter(
    private val drawable: Drawable
) : Painter() {
    private var invalidateTick by mutableStateOf(0)
    private var startedAnimatable = drawable is Animatable && drawable.isRunning

    init {
        drawable.callback = object : Drawable.Callback {
            override fun invalidateDrawable(d: Drawable) {
                // Update the tick so that we get re-drawn
                invalidateTick++
            }

            override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) {
                MAIN_HANDLER.postAtTime(what, time)
            }

            override fun unscheduleDrawable(d: Drawable, what: Runnable) {
                MAIN_HANDLER.removeCallbacks(what)
            }
        }
    }

    override fun applyAlpha(alpha: Float): Boolean {
        drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255)
        return true
    }

    override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
        drawable.colorFilter = colorFilter?.asAndroidColorFilter()
        return true
    }

    override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean {
        if (Build.VERSION.SDK_INT >= 23) {
            return drawable.setLayoutDirection(
                when (layoutDirection) {
                    LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR
                    LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL
                }
            )
        }
        return false
    }

    override val intrinsicSize: Size
        get() = Size(
            width = drawable.intrinsicWidth.toFloat(),
            height = drawable.intrinsicHeight.toFloat()
        )

    override fun DrawScope.onDraw() {
        if (!startedAnimatable && drawable is Animatable && !drawable.isRunning) {
            // If the drawable is Animatable, start it on the first draw
            drawable.start()
            startedAnimatable = true
        }

        drawIntoCanvas { canvas ->
            // Reading this ensures that we invalidate when invalidateDrawable() is called
            invalidateTick

            drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())

            canvas.withSave {
                // Painters are responsible for scaling content to meet the canvas size
                if (drawable.intrinsicWidth > 0 && drawable.intrinsicHeight > 0) {
                    canvas.scale(
                        sx = size.width / drawable.intrinsicWidth,
                        sy = size.height / drawable.intrinsicHeight
                    )
                }
                drawable.draw(canvas.nativeCanvas)
            }
        }
    }
}

/**
 * Allows wrapping of a [Drawable] into a [Painter], attempting to un-wrap the drawable contents
 * and use Compose primitives where possible.
 */
fun Drawable.toPainter(): Painter = when (this) {
    is BitmapDrawable -> BitmapPainter(bitmap.asImageBitmap())
    is ColorDrawable -> ColorPainter(Color(color))
    else -> AndroidDrawablePainter(mutate())
}
n
Thanks and the logic to parse the icons from packagemanager?
y
Copy code
private fun getIconFromPackageManager(context: Context): Drawable? {
    var drawable: Drawable? = null
    try {
        val intent = Intent(Intent.ACTION_MAIN)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        intent.setClassName(appPackageName, appClassName)
        drawable = context.packageManager.getActivityIcon(intent)
    } catch (e: Exception) {
        try {
            drawable = context.packageManager.getApplicationIcon(appPackageName)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    return drawable
}
I am actually not sure why I add a flag but basically I try to get the icon using both class and package name then if it fails I try using only package name
n
I think you want to remove the scaling logic within the AndroidDrawablePainter implementation
Painters handle their scaling internally and I think this might be effecting the way the adaptiveicon is being rendered
Ex: VectorPainters don't scale their contents down/up using the traditional canvas transformation but rather updates their internal paths to match the specified drawing size
That would explain why the contents are translated properly, however, the contents are scaled down and not centered
y
It worked! Thanks 😄
👍 1
n
No problem! Thanks for being patient and trying things out. We hit the Image composable/Painter code path pretty hard and uncovered a lot of issues early on in development that we fixed + have extensive testing to verify a large magnitude of use cases. Anything is possible but I would be surprised if there were new bugs in this part of the framework.
❤️ 1
y
Thanks again and good luck fixing the bugs 😄 I am already enjoying compose so much other than these issues
👍 1
106 Views