Messengers-like ImageView
Controlling the size and sometimes aspect ratio
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:
- 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
- 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 makeSize
asdata
class, but I didn’t want to mix mutability withdata
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.
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 inwhen
) - If the above predicate is not correct, then either width will be bigger than
max.width
or height will be bigger thanmax.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 inwhen
) - 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 thanwidth
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 (sowidth
of it will be equal towidth
of max size) theheight
of the desired size will be still bigger thanheight
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 bemax.width
and height will be updated respectively. But if it will be less thanminSize.height
we will break the resulting aspect ratio and assignminSize.height
to theout.height
- Similarly, if the aspect ratio of the desired is more than max size’s aspect ratio, we just update
height
to bemax.height
and width will be updated respectively. But if it will be less thanminSize.width
we will break the resulting aspect ratio and assignminSize.width
to theout.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.
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: