Bitmap playground

ninad thakare
6 min readMar 11, 2021

There is a feature in Apna app which is one of my favourites. You can share your card along with your profile deep link. A gif for demo.

One could build on this to generate resumes or add more templates. Improve the cards UI, make it look like one of those debit cards or I don’t know anything really. But this looks great to me. You’re not going overboard here and the end user feels a part of something bigger. I know because that’s what I felt.

How it works is it will take a screenshot of the view, add that banner at the bottom, save it to the storage and share it.

Post Item

To create a bitmap from the layout was relatively easy.

To create a bitmap from a view
Post Item

Rest of it :

Now I had to create the banner at the bottom before appending it. This was challenging since I had never worked on bitmaps and canvas. However little I had worked was 4 years back.

To create the purple banner bitmap for appending to the above generated ‘screenshot’ I used the width of the bitmap generated from the above helper method and spread the colour on it. The code is as followed.

To create the purple banner

Now I had to add the apna logo to the left centre of this purple banner with 16dp margin at the start and the icon size of 40dp.

Add logo to the left of the banner

I had the playstore logo as well on the right side of the banner. The margin right 22dp for this icon and I calculated the height from the vertical margin and width from height keeping the ratio the same.

Playstore logo on the right

Now to add the text. This was again a little bit of challenge. For adding text you need a Paint instance. With paint you can set the text size, typeface and colour of the text. The tricky part was the placement of the text. So you can’t just add text to the centre of the bitmap. Turns out the text height is not what you set it to be. Even if you convert it from sp to px and adjust accordingly, it won’t fix it. There is something called descent and ascent which you need to get the centre of the canvas right for it to show up exactly at the centre of it. Finding the efficient solution for multiline text was even a bigger challenge.

To add a multi-line text.

The solution I implemented is quite hacky here. The assumptions are that the text is going to be 2 lined. The text bounds is taken from the length of the first line of the multiline text. This is an issue if the font style is changed or if the first line has more text. This is bad implementation, no matter how you look at it. The only good news is that it’s working.

And that’s it. It worked.

If you haven’t noticed till now. This implementation was just a bomb waiting to go off. Just because everything worked didn’t mean it was going to continue working.

The first bomb

If you run the code above, it will not work for some devices under some of the conditions not sure what they are though. For example if you remove the png icon and replace it with a vector icon, it works perfectly.

This was fixed using RectF. This suggestion came from Sujith Niraikulathan who also works with me here at Apna. This will help us define the dimensions on the image and let the canvas know where it should draw the bitmap. The earlier code was also doing the same thing but for some reason it was not working on some devices.

The resultant code for adding the apna icon looks as follows:

val dest = RectF(0f, 0f, topImage.width.toFloat(),     
topImage.height.toFloat())
dest.offset(marginLogo, marginLogo) comboImage.drawBitmap(topImage, null, dest, null)

I’ll omit the code for the playstore icon to avoid redundancies.

The above code fixed the issue for good.

The second bomb

Overlapping text. A somewhat expected problem. To write the multiline text I found an easier implementation.

val textWidth: Int = bitmap.width - (250 * scale).toInt()val textLayout = StaticLayout(
text, paint, textWidth, Layout.Alignment.ALIGN_NORMAL,
1.0f, 0.0f, false)
val textHeight = textLayout.heightval x: Float = (bitmap.width - textWidth) / 2f
val y: Float = (bitmap.height - textHeight) / 2f
val savePoint = canvas.save()
canvas.translate(x, y)
textLayout.draw(canvas)
canvas.restoreToCount(savePoint)

Assuming textWidth to be 250dp (Should be calculated based on available space).

StaticLayout makes life much easy when you’re trying to add text to canvas. More on StaticLayout can be found here.

Optimisations

Try to create fewer bitmaps. Bitmaps are expensive and can run out of memory on lower end devices or in low memory state. Try to create the only one bitmap and pass the canvas around maybe. I got the image bounds defined by Rect using methods while keeping the aspect ratio constant.

Filling the ambient colour was a low hanging fruit which can be done by the following code

val rect = Rect(0,            
originalBitmap.height,
originalBitmap.width,
(originalBitmap.height + bannerHeight).toInt())
val paint = Paint()
paint.isAntiAlias = true
paint.color = ContextCompat.getColor(context, R.color.colorPrimary)
paint.style = Paint.Style.FILL
canvas.drawRect(rect, paint)

To get the Rect for the apna icon will look like

fun getApnaIconRect(logoD: Drawable, height: Int): Rect {
// aspect ratio of the image
val ratio = logoD.intrinsicWidth / logoD.intrinsicHeight

val expectedHeight = height - 2 * margin
val expectedWidth = expectedHeight * ratio return Rect(0, 0, expectedWidth, expectedHeight).apply {
offset(margin, margin)
}
}

To add the image to the canvas simply add the offset, declare bounds and pass the canvas to the drawable

val logoD: Drawable = ContextCompat.getDrawable(
this, R.drawable.ic_logo)
?: throw Exception("Unable to fetch the drawable ic_logo")
val apnaIconRect = getApnaIconRect(logoD, bannerHeight.toInt())
apnaIconRect.offset(0, originalBitmap.height)
logoDrawable.bounds = apnaIconRect logoDrawable.draw(canvas)

And voila we have an optimised way to generate the image as per the requirements and share it.

Special thanks to Sujith Niraikulathan for suggesting the optimisations and working with me on this one.

Things work differently in the canvas land. For example you pass an bitmap to a canvas and then return the bitmap or you give canvas to the drawable loaded from the asset to draw itself on the canvas. This is different from how we do things. Instead of giving canvas to drawable we are used to giving the drawable to canvas to draw the drawable on itself. It certainly takes getting used to this mode of thinking.

The project can be found at https://github.com/ninad458/potential-octo-spoon. Optimised code can be found on main branch and code discussed in the article can be found in uno branch.

In closing I will leave this quote here.

Remember, a few hours of trial and error can save you several minutes of going through the documentation ~Lao Tzu

P.S Apna is currently hiring rockstars across all verticals. If you want to be part of a mission that endeavours to create livelihood opportunities for Bharat, please write to us at careers@apna.co with a short line or two about why you would be a perfect fit.

--

--