Android View Collisions

How to try to prevent view intersection

Short aside: I missed new article in the end of February, because of my honeymoon vacation. I’m sorry. Now I try to make an interesting and big article with lots of code snippets. But it takes some time. While it is preparing let’s dive into world of views … ;)

There was time long ago, when I wanted to have a layout to be able to gravitate views to the top and to the bottom, but when this layout came too small, the views would not intersect with each other, but formulate a simple LinearLayout with ability to scroll.

Time passed, and my desire with forgotten and buried in my cemetery of chimerical ideas. Until one day my colleague ask for help. He needed next:

We have layout with 3 views, we want to show one at the top, one at the center and one at the bottom. But also we want to prevent their collision, so if it will be not possible, for example place second view at the center, we will just shift it little bit up or down depends on collided view.

“Sure” I’ve answered and started to think. Next thing, I remember, I came with that sketch of solution:

Which looked like (next here and in the rest of article I will use animation of collapsing and expanding layout to emulate different heights of devices):

He said, that this idea is very similar, but he also, in that solution the second view is not in the center of layout:

And also he wanted scrolling feature. And here I remembered my old forgotten wish. Now it is time to make that dream to come true :)

Base Solution

Let’s create our custom ViewGroup that can handle its children like in aforementioned gif. The main 2 methods that are important for new are onMeasure and onLayout.

The content of this method, will be similar to measuring FrameLayout with difference that we not need to find maximum height through all children, but sum all heights to resulting one:

No difficulties here. We just iterating through all children of a parent view, measuring them and getting their height (including top and bottom margins); and also adjusting the result width and height of parent view. Next we need to layout children inside their parent view.

Here I want to make a quick note. Next will be an ugly snipped of code, since I just wanted to layout children for cases of 1, 2 or 3 children. After that we will talk about any amount of child views, but for now our layout process will be split into 3 cases:

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val fixedL = l + paddingLeft
val fixedT = t + paddingTop
val fixedR = r - paddingRight
val fixedB = b - paddingBottom

when(childCount) {
1 -> layoutOneChild(fixedL, fixedT, fixedR, fixedB)
2 -> layoutTwoChildren(fixedL, fixedT, fixedR, fixedB)
else -> layoutTreeChildren(fixedL, fixedT, fixedR, fixedB)
}
}

So next we will consider all three cases. In the first case we need to position our single child in the center of parent if we have a place for that. Otherwise, we will place it at the top of the parent. Our snippet for layoutOneChild method is that:

This method is self-explaining, too. We take only one child, and if its height is more that height of parent, then we its top will be equal to t else it will be at the center, which means that top will be equal to t + (height — totalHeight) / 2. Next we need to see the snippet with layoutTwoChildren method:

For two children we want to place one at the top of parent view and one at the bottom, if there is such possibility. Otherwise we will place views one under another, like in LinearLayout. Next we need to write a method for three children. In it there will be little bit more logic. We will place first view at the top, last view at the bottom and second view at the center of parent. And we need to understand how to shift second view: to down or to top. So let’s look at the snippet of code:

Here we have slightly more difficulties. We check if sum of measuredHeights of all three children is more than height of a parent. If so, we just place all children as we would for LinearLayout. In other case we try to assume, what top and bottom would have second and third views and then adjust their y coordinates appropriatly.

Whew. Ugly internal part is over. Now let’s check interesting part: results!!!

Animation of collapsing and expanding layout with 1, 2 and 3 children to see, how view deform their positions

Our mission is over and we can exhale and drink some beer with friends…can we? May be, but I really don’t like that we have that disgusting choice for 1, 2 or 3 children and, moreover, we can not position, for instance, 10 children.

👍Boost 1️⃣: Extrapolate it for any child count

Now our task is to extrapolate our solution and invite new generic solution for any amount of children more than 1. To do that let’s think how we can approach the final algorithm.

Basically we have n children, that will be placed from screen top to bottom, and each child will be tried to place at special anchor. All those anchors will be at the same distances from neighbours and all views except the first one and the last one will placed at their middle at those anchors. Here is a sketch which visualise previous setnences:

Di is an distance between center of View_i element and View_(i + 1) element, except View1 (top) and View4 (bottom).

We can think how we can combine their positions to achieve this equality. But I suggest you to go from another corner and rethink about this problem from a different angle.

The idea is to analyze distances between views when they already placed in case, when parent is really-really big, so children can be placed on their anchors correctly, without shifting. Each D_i consists of tree values:

  • a half of height of View_i (for View_1 it will be full height);
  • a half of height of View_(i+1) (for View_(n-1) it will be full height);
  • remained height

First two values are known, so their sum is also known and determined. Let’s call it weight_i (next will be more pseudo-code):

weight[i] = (view[i].height + view[i + 1].height) / 2
weight[0] = view[0].height + view[1].height / 2
weight[n - 2] = view[n - 2].height / 2 + view[n - 1].height

Thus, we have weights array. Now we can associate each weight with child and its position and remember that info. So right now we have an ability to sort weight by ascending value with keeping original state in mind (let’s call newly created array sortedWeights).

Now let’s remember that we have height of parent view and also we know totalHeight of children:

totalHeight = children.sum { it.height }

Let’s introduce freeHeight which will be remain amount of height we need to distribute between views, to equalize distances between their centers.

How we can do that? You can image our sortedWeight array as half of pit and a freeHeight is a sand that we will pour into this pit. Firstly the bottom will be filled (means part of freeHeight value will be distributed to first item of array), and then continuously all elements will start to fill simultaneously. Here is another sketch, which illustrate that process of distribution freeHeight value between all elements of sortedWeight :

The freeHeight from the top is distributed between all elements of sortedWeights

As the result of such spreading freeHeight between elements of sortedWeights we have an array distribution with elements d_1, d_2, ..., d_(n — 2).

And now we have all we need. We have info about children: height, width, x and y; and also we have info about distance between them (distribution array).

Let’s start with implementing steps of our meta algoritm with appropriate methods and them we will little bit optimize them by memory. So let’s go step by step:

  1. Build array of weights:

2. Sort array of weights (with remembering origin positions):

3. Distribute freeHeight between sorted weights:

4. Associate result distribution (distance between views) to origin positions

5. Combine all and layout children

That’s it! Before we will check results, let’s mention few words about optimization. As you notices in createWeights, sortWeights, distributeFreeHeight, buildDistanceArray every time, we call them, we create new instances of array. But in fact we use them only once in layout phase. So we can create a fields with that array and rewrite data in them each time, when layout phase is happened. Okay, let’s go to results.

Results

Here will be shown some results for random child count with random colors and heights:

👍Boost 2️⃣: Add offsets

left: we can do that, right: we can not do that yet

With aforementioned solution you can build layout from the example left, but can not do layout from the example right.

To achieve more flexibility of positioning children in our created layout we need to add a possibility to scale ranges between centers of each children. I remind you that for now we assume that all distances between child centers are equal to each other. So let’s enhance our solution with adding scales.

So we need to transform our task to the previous one. Suppose we now have scales and weights . If we will be able to scale down every weight by its scale we will be able to equalize every weights as they were if every scale would be equal to one (i.e in a aforementioned task). To do that we need to ensure that sum of all scales are equal to 1. In other words we need to normalize our scales.

Next we need to (as it was in previous task) properly distribute freeHeight between all weights taking to account different scales. For that we need to sort our transformed weights and fill them with freeHeight value as it was previously, but also we need to properly adapt that value according to different scales.

Also, we want to provide our scales properly through children in xml layout. To do that we can use LinearLayout.LayoutParams.weight for that. But we have n views and n-1 scales and weights. So we need to properly transform view’s weight to scales .

Now as summary we need to:

  1. Take all view layout param’s weights and transform them to scales
  2. Normalize scales
  3. Adapt all weights (as we counted them earlier) with given scales
  4. Distribute freeHeights between all weights properly applying scales
  5. Build a distances
  6. Build a layout

As the implementation will be an enhancement of previous implementation we will see pretty much same lines of code, but beside that we also introduce a new structure Bucket for convenient computing:

private class Bucket(
val filled: Float,
val scales: Float,
val position: Int
)
  1. Get view’s weight and create scales:

2. Normalize scales:

3. Create and adapt all weights by given normalized scales :

4. Create buckets (gather weight, scale and original position together) and sort them by adapted weights :

5. Distribute freeHeights between all weights properly applying scales

6. Building distances is pretty much the same as for previous section.

7. Gather all previous steps and layout children:

Results

Now let’s check what we can create with our new layout:

Afterwords

In this article we solved one interesting task. And you know what? It doesn’t matter, if such layout is not needed in ordinary app development. More interesting that we came to that solution by ourself. It means that we leveled up our brain and skills and this is good :)

If you interested in full source code of result CollisionView, please, check this link.