Behind the Scene of Our New Android Refine Page

As you all may have known, we have done quite an overhaul to both our iOS and Android apps. It has been a long ongoing project and it’s still a work in progress as we’re continually trying to improve the customer experience. We released our overhaul for the Android app right before the holiday. The journey for that release has been challenging to say the least, but in a really good way. Just as any programmer out there, I too love a good challenge. The most challenging part of the whole project has been our refine page. It has taken the longest and requires the most thought as filtering is one of the most important (if not the most important) features within our app.

General Refine page before applying any filter

Our designers always have our customers first in their mind, so they came up with a simple design that would also be the most user friendly (as you can see on the picture on the left). But make no mistake, this simple design is actually quite complicated to implement as there are a lot of things to consider. For this post, I will dive into the UI challenges in particular.

If you are familiar with Android, you will know that the “sort by” design isn’t as easy as it is for our iOS counterpart. In iOS, there is already a class called UISegmentedControl for that design. For Android, however, you have to make your own custom view for it. I was planning on creating one myself when I stumbled across this library. This library does a great job of making the segmented group extend from RadioGroup with each child being a radio button. It did exactly what I needed it to do for the “sort by” UI.

Another challenge that we faced is that we have more filters than the 6 shown. Our filters are contextual, which means once a category is applied, the filters are dynamically brought in. For example: when the ‘Handbag’ category is selected, size filter should be hidden or when the ‘Tops’ category is selected, there will be additional filters such as ‘Style’, ‘Sleeve length’, and ‘Neckline’. After each filter selection, we reach out to our internal API which returns all the contextual filters to be shown to our users. To handle this, we have to make the whole page as flexible as possible from coding perspective.

Refine Page with Tops Category Applied

On your left is an example of how the refine page will look when the tops category is applied. Keep in mind that if you scroll down, the “Sort by” section will still be shown. In order to make the whole page flexible, each section has to be added manually and the layout has to be flexible enough to handle any number of filters returned from the API.

I made one layout that will work for every section type (except ‘sort by’) such that a fragment can be added to each one once the section is expanded. Bare in mind that we will need to be able to access every section for when that section is tapped and then show the correct fragments.

The code snippet on the left is the layout I came up with. It’s not the most efficient as it layers a RelativeLayout inside of a LinearLayout, but this is the best solution I came up with. The RelativeLayout contains the black view to show whether a filter is applied or not, a title and subtitle of the filter, and an ImageView for the arrow down or up to show if the section is expanded. The LinearLayout is the outer layout where each section’s fragment is added programmatically. SimpleTextViewAnimator is a custom TextView that shows an animation when the visibility is set to visible or to gone. For the purpose of this section, I treated our custom TextView only as a simple View with animations.

The layout for the activity itself only contains a ScrollView with a LinearLayout within it where all filter sections are added.

To add a fragment into the section, I have to create a FrameLayout programmatically and set a contentId to it. Next I add that FrameLayout to the root LinearLayout of that section and then do fragmentTransaction.replace(contentId, frag, fragmentTag). Now, we’re faced with another problem. Since we don’t know how many sections there will be and we need to assign a unique contentId to the FrameLayout that will be replaced with Fragment, we have to figure out a way to create a unique id using the info that we have. I thought about creating constants of IDs to be assigned, but I kept running into the issue of how many I should create. I decided to use the hashcode of each section’s title as the unique ID. This worked for awhile, but at some point one of the titles had a negative integer value for the hashcode and caused a crashed. I then set the contentId to be the absolute value of the hashcode and it now works perfectly. With this, I can also show and hide the fragment as intended by doing FindViewById. Here is a code snippet for how I hide or show section fragment:

int contentId = (Integer) sectionLayout.getTag(LAYOUT_ID);
FrameLayout content = (FrameLayout)sectionLayout.findViewById(contentId);
if (content != null) {
content.setVisibility(View.GONE);
// or content.setVisibility(View.VISIBLE);
}

The last issue that needed to be addressed was to have a layout_height of WRAP_CONTENT assigned for each of the fragments. All fragments contain either a grid or a list of textview or checkbox or a combination of both. For a simple list, I can just use a vertical LinearLayout and set the height to be WRAP_CONTENT. For a more complicated list where we need to able to add and remove a view, however, I have to resolve to a RecyclerView with a LinearLayoutManager. This is where the hard part came. In order to make a RecyclerView to adapt its height to the content, some hacking to LinearLayoutManager needed to be done. After a lot of researching, I finally stumbled upon a StackOverflow post where I got this solution for a WrapContentLinearLayoutManager from. The other form of content for the fragments is a gridview. Same issue with GridView where we can’t simply set WRAP_CONTENT to its height. Fortunately, based on this StackOverflow post, the solution for this is simpler than RecyclerView. I only need to create a custom GridView where I override the onMeasure method:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK, MeasureSpec.AT_MOST));
getLayoutParams().height = getMeasuredHeight();
}

I am, by no means, saying that this is the best solution. In fact, I am almost certain that there are other people out there that can came up with a better solution for all these issues. However, this is the way I managed to figure it out and hopefully this blog post can help anyone else who struggles to find similar solutions. Please feel free to comment below or contact me at shinta@thredup.com. :)