Messengers-like ImageView

Controlling the size and sometimes aspect ratio

Michael Spitsin
ProAndroidDev

--

In the previous story, I wrote about the Uploading animation for sending image messages and how we have built it. Today I decided to write about the images themselves. How can we show them inside the message history?

Well … ImageView. The article is over!

But hold on for a while, I will try to show, that it is not so simple. Well, it is not hard, to be honest, but still, it is not just an ImageView, but also some small calculation around to look the size of it fits to what you are showing to the user.

Measure it

So the first part is to understand what is needed to be done. The core of our solution. We want to draw the image based on the predefined size (width & height) of some picture/gif or any other showable media, but with respect to constraints of the container in which the picture will be showed (we will decrease the size of 1000x1000 picture to fit in 100x100 view container). And with respect to the aspect ratio, please. Well! Not always, but please in most situations.

So that means that our base process can be split into two parts:

  1. Defining and providing the desired size with the respect of some absolute minimum. If the image will be too small it will be scaled until its smallest side will be equal to the respective absolute minimum
  2. Giving it and the constraints of container view (say maxSize) measure the final size with respect to the aspect ratio, if that’s possible (will discuss less possible scenarios later)

Definition of Size

Let’s start with the definition of a simple class Size. It will contain information about width and height of the image and also will provide a bunch of useful methods:

A couple of clarifications here:

  • We made this class mutable because it will be used inside the view and we want to optimize the instance’s creation since it not needed because we work in one thread.
  • Instead of custom copy we could make Size as data class, but I didn’t want to mix mutability with data classes, that are supposed to be immutable

Now, having the definition of Size we can create a class ImageSizeMeasurer that will be responsible for size definition, adjustment, and measurement.

Setting up the desired size

The first part of the class will be setting up the desired size along with a minimal size. In this method, we will check if the desired size will be less than minimal, and if yes, then we will adjust it respectively:

We use copy method for not allowing the clients to change the fields, that can be potentially shared between them (so we will have no surprises if your field will be changed from somewhere).

The key point here is that after setting the size and ratio, we need to adjust it. There is no harm to call both adjustDesiredHeight and adjustDesiredWidth without any smart checks, because either first method will increase the smallest height of desiredSize to minSize (if the height is less than width), or the second method will increase the smallest width of desiredSize to minSize (if the width is less than height)

Measure respective to the constraints

We prepared the desired size with respect to the minimum size. Now it's time to measure the real size with respect to maximum size. The method itself is not hard, we just need to remember that we should make all updates not changing the aspect ratio, except cases when decreasing the image height&width leads to having one of them less than minSize.

For example, that’s true for really narrow images.

  • either will have a width which is fitted in max constraints but height is too small
  • or you will have height fitted in min constraints but width is too big
  • or you will have to fit width in max constraints, height in min constraints, and break the aspect ratio.

The last option is the most appropriate one in that case since we can not have image size more than constraints and we don’t want to have a too narrow image, because it is maybe hard to see what is showing and click on it. And we can use scaleType = imageCrop which will help here in case of breaking the aspect ratio.

Check Size gist again here to remember methods contains and update

Let’s quickly analyze the measure method.

  • When the desired size fits in max size, then everything okay. After setDesiredSize our size will be for sure not smaller than min size. And now we just make sure that it is not bigger than the max size. So we will just return it (the first predicate in when)
  • If the above predicate is not correct, then either width will be bigger than max.width or height will be bigger than max.height or both. And if in that case, the aspect ratio of the image will be the same as the aspect ratio of the max size, then we can use just max size as output size, since it will be scaled down the result of the desired size. (the else-block in when)
  • In another scenario, we need to look only at aspect ratios for comparison. Let me explain. We may have for instance width of the desired size be bigger than width of max size. But the aspect ratio of the desired size will be bigger than the aspect ratio of max size, too. Means, that when we will scale down the desired size (so width of it will be equal to width of max size) the height of the desired size will be still bigger than height of max size
  • So in case, when the aspect ratio of the desired size is less than the max size’s aspect ratio, we just update width to be max.width and height will be updated respectively. But if it will be less than minSize.height we will break the resulting aspect ratio and assign minSize.height to the out.height
  • Similarly, if the aspect ratio of the desired is more than max size’s aspect ratio, we just update height to be max.height and width will be updated respectively. But if it will be less than minSize.width we will break the resulting aspect ratio and assign minSize.width to the out.width

A bit of magic in all calculations makes everything more natural and pretty

Now we prepare everything to be measured in view:

Here everything is simple enough. In case we have unspecified measure specs, we tell the view’s parent our desired size, which we would like to have in the ideal scenario. In the other case (AT_MOST or EXACTLY) we need to use the provided width and height to set up the maxSize and pass it into our measurer.

Now let’s look at the result:

Everything looks super but it seems like small images are too small. And our wish to make them a little bit bigger than right now. You can say “Just increase the minimum size”. Well, we can do that, but in that case, we will see no difference between the small and the smaller image, since they will use equally minimum size.

Instead of that, we can add a bit of magic

The magic concludes in the increasing small images by some magic constant or formula to be a bit bigger and keep the differences in the size between small and smaller images :)

The algorithm is short: increase the desired area by 1/3 of the difference between max and desired areas and then knowing the new desired area and ratio, find the new width and height.

Here is a comparison result.

Left: without magic, Right: with magic

I like that in the new result we have bigger pictures, so it is more convenient to observe them, but at the same time you still have the understanding that some pictures are bigger (more detailed) and some are smaller.

What if I want to build sizes, knowing the ratio only

As an additional point let’s discuss the further improvement. Sometimes, you have not the final specs of the image (it’s final height and width), but specs for the thumbnails. So you can not use them as the desired size, since those specs are much smaller, but you can calculate the ratio which will be more or less the same, and then out ImageSizeMeasurer will calculate the final size by having the fixed ratio only and trying to fit the max constraints as much as possible.

So first, let’s add a new property to our Size class:

val isSpecified: Boolean get() = width > 0 && height > 0

Next, we need to add a possibility to set the desired ratio instead of the desired size:

fun setDesiredRatio(ratio: Float, min: Size) {
minSize = min.copy()
desiredSize = Size(0, 0)
fixRatio = ratio
}

And then we will update the measure by adding an additional adjustment to the desired size:

And finally, let’s update onMeasure

Let’s talk about the view

So far so good. We have the special measurer that can rule the worlds and make new universes! We even have a rough understanding of how it will be integrated with the view. But we still got no view.

Let’s first describe, what we want. Actually, it is not hard to specify minWidth and minHeight. Those attributes are part of xml. Well, maxWidth and maxHeight, too. But I don’t want to hardcode any specific size here. Instead, I want to rely more on the device screen. Means, that would be nice to specify those max constraints by let’s say percentage. Since we have ConstraintLayout it should be not hard to specify max width like that (say 70% of the screen width). But what about height?

I will quickly remind you that you can specify constraints whatever you like, I’m just giving my small thoughts to have a starting point. I decided to have a height depending on width, by some factor. So, let’s say if we will have factor = 1, it will be just a square. Just specify width and the height will be calculated automatically.

You will see, that implementation is very simple, but at the same time you have a screen size dependency, rather than having a lot of dimens.xml depending on different factors of devices, though the latter solution will be more “androidish”:

Gather all up.

Now we can look at the final class:

And the result of our work:

Afterwords

If you liked that article, don’t forget to support me by clapping and if you have any questions, comment me and let’s have a discussion. Happy coding!

Also, there are other articles, that can be interesting:

--

--

Love being creative to solve some problems with an simple and elegant ways