Creating uneven grids with FlexboxLayoutManager on android

echoeyecodes

EchoEye

Posted on December 6, 2021

Creating uneven grids with FlexboxLayoutManager  on android

I was looking through the DeviantArt android application for features I could implement in my latest project that could be useful to have in the mobile application, and came across this layout format I liked a lot. As you can see in the image below, the images are arranged in a grid-like form, but each image has different sizes based on their aspect ratio.

Deviantart application screenshot

I initially tried using a workaround with the GridLayoutManager android provides, but that didn't work as expected because this layout wouldn't allow feed items to fill up extra spaces that might be available during positioning.

Turns out the answer was in the FlexboxLayoutManager - A layout manager based off of CSS' flex box. I've used this quite a number of times for basic layout, especially the "wrap" feature, but never had a use case for something specific like DeviantArt's. I set out to implement this layout manager, but of course it didn't come off as easy as I thought it would. The problem I had at first was that I couldn't control the maximum number of columns that should be displayed on a single row before wrapping, so all hundreds of images were arranged on a single row. Crazy right?😅

Anyway I did a bit of research here and there for how to achieve this form of layout in CSS Flexbox since the layout manager is based on this. A few minutes later, I came across this post on stackoverflow about setting the min-width and max-width of each flex item, then allowing the default behavior of these flex items to fill up remaining space with the flex-grow property set to 1. I applied this concept on the FlexboxLayoutManager in android, and I'm glad I was able to finally achieve the same results with DeviantArt😄

Here's how I implemented it on android; since the sizes of the various images to be rendered is based on the aspect ratio, I would return 3 different view-types based on this faactor. The viewTypes are PORTRAIT, LANDSCAPE, and SQUARE. So in the getItemViewType() method of the RecyclerView adapter, this check is performed to determine the view type to be used as here:



override fun getItemViewType(position: Int): Int {
        val item = getItem(position)
        return if (item.width > item.height) {
            LANDSCAPE
        } else if (item.width < item.height) {
            PORTRAIT
        } else {
            SQUARE
        }
    }


Enter fullscreen mode Exit fullscreen mode

Now that this is available, the minimum and maximum possible width and height would be determined in the onCreateViewHolder() and these values would be different based on the view types:



    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShotFlexAdapterViewHolder {
val screenWidth = getScreenSize().width

        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.layout_shot_list_item, parent, false)

        if (viewType == PORTRAIT) {
            val maximumWidth = (0.5 * screenWidth).toInt()
            val minimumWidth = (0.3 * screenWidth).toInt()
            val minimumHeight = (0.5 * screenWidth).toInt()
            val maximumHeight = (0.6 * screenWidth).toInt()

            view.updateViewLayoutParams(minimumWidth, maximumWidth, minimumHeight, maximumHeight)
        } else if (viewType == LANDSCAPE) {
            val maximumWidth = screenWidth.toInt()
            val minimumWidth = (0.55 * screenWidth).toInt()
            val minimumHeight = (0.5 * screenWidth).toInt()
            val maximumHeight = (0.55 * screenWidth).toInt()

            view.updateViewLayoutParams(minimumWidth, maximumWidth, minimumHeight, maximumHeight)
        } else {
            val maximumWidth = (0.7 * screenWidth).toInt()
            val minimumWidth = (0.4 * screenWidth).toInt()
            val minimumHeight = (0.4 * screenWidth).toInt()
            val maximumHeight = (0.7 * screenWidth).toInt()

            view.updateViewLayoutParams(minimumWidth, maximumWidth, minimumHeight, maximumHeight)
        }

        return ShotFlexAdapterViewHolder(view)
    }


Enter fullscreen mode Exit fullscreen mode

Now the extension function that updates the width and height properties:



private fun View.updateViewLayoutParams(
        minimumWidth: Int,
        maximumWidth: Int,
        minimumHeight: Int,
        maximumHeight: Int
    ) {
        (this.layoutParams as FlexboxLayoutManager.LayoutParams).apply {
            flexGrow = 1f
            this.width = minimumWidth
            this.height = minimumHeight
            this.maxWidth = maximumWidth
            this.minWidth = minimumWidth
            this.minHeight = minimumHeight
            this.maxHeight = maximumHeight
        }
    }


Enter fullscreen mode Exit fullscreen mode

Here's an explanation of what is going on above. In order not to overcrop the images that would be displayed, the container width and height holding this image should match the view types i.e portrait images should have a container height greater than the container width, landscape images should have a container width greater than the container height, and square images should have container width and height of equal sizes so that the images to be displayed fits almost* perfectly with the container, and not overcropped.

Now on top the onCreateViewHolder(). For portrait images, the container is assigned a minimum width 30% the screen width, complemented with a minimum height of half of the screen width, which is slightly higher than the minimum width. This would be a particularly useful for scenarios that allows a maximum of 3 portrait images to be rendered on a single row before wrapping. Now imagine we've got a portrait image and a landscape image competing for space on a row. Obviously the landscape image would need more width size than the portrait (say 50% of the screen width), and that means lesser spaces would be left for other images.

For this to work in this case, we can only have a maximum of 1 landscape photo, and the remaining 50% of the screen width would be left to be shared amongst other images. Remember we initially, we defined the minimum width of portrait images to be 30% of the screen width, which means only a maximum of 1 photo can go along with a landscape photo on the same row. The remaining 20% would be shared between these two images since we set the 'flex-grow' property of every item to be 1, i.e stretch to fill the remaining space.

This analogy would also be applied in cases for landscape and square images. The key lies within using the min/max width and height properties to properly configure how much space each item can be allowed to have on a single row based on their aspect ratio, before wrapping subsequent images to the next row. The min width determines the smallest possible width an item can have on a row, while the maximum width determines the maximum possible width an item should have when stretching, regardless of how much space is left on the row. I hope you find this explanation useful. I'm still recovering from the 'eureka' moment I had after I discovered this layout approach😅 and here's what it looks like on the app

Picashot Screenshot

💖 💪 🙅 🚩
echoeyecodes
EchoEye

Posted on December 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related