Building Hipmunk’s Price Per Night Hotel Filter ✈︎

Hipmunk’s hotel filters got a facelift in a recent Android update. In this post, I’m going to describe what changed, and walk through the Android fundamentals of implementing the price histogram filter.

The goal of the redesign was to add new features and to modernize the look and feel. Here on the left is the old version, and on the right is the new version:

Android old Android new

The new filters are more pleasing to look at, and offer the user more control to fine tune the hotel search to find the perfect stay. And there’s a reset button! Assets were swapped out, font styles and colors were tweaked, and a couple of custom views were created to achieve this makeover.

My favorite features to implement during this project were the Review Ratings filter and the Price Per Night filter. They are both custom views that use Android’s SeekBar, and a combination of other Android widgets for each filter.

Let’s dive into the development of the price filter.

Price filter

The price filter has a histogram of the hotel prices from the results list. The axis ranges from $0 to $n+. n is calculated as 90th percentile to remove outliers. This is necessary because the graph would otherwise be skewed to the left most of the time and will rarely get a nice bell curve. This is important because it gives users a better idea of the average price per night of the hotels in the selected city. The SeekBar drawn right below the histogram can be controlled by the user to select the ceiling price per night, filtering out only hotels that fall under this number.

We began developing this feature by investigating how to render the graph. After about an hour of browsing online for libraries, I came to the conclusion that there were no good existing solutions for our needs. So I decided to implement my own histogram.

Problem

Draw a histogram given an array of numbers, and an integer of number of bins. Include a slider for the user to drag to set the maximum price of a hotel per night.

Implementation

I will use the following variables and values to walk through the implementation:

Integer[] data = { 1, 1, 4, 3, 5, 8, 4, 10, 20, 18, 14,
                   12, 12, 11, 8, 18, 19, 14, 14, 14, 14, 12, 11};
int numBins = 10;
HashMap<Integer, Integer> histogramData = new HashMap(numBins);

Step 0: Find the highest value in the array.

Step 1: Calculate the interval.

int interval = highestValue / numBins;

Step 2: Iterate through data and place each value in the corresponding bin (in histogramData). This will be the used to render the actual histogram.

int bucketIndex = (currentValue - 1) / interval;    // -1 bc we start with index 0
if (histogramData.get(bucketIndex) == null) {
    histogramData.put(bucketIndex, 0);
}
int count = histogramData.get(bucketIndex);
histogramData.put(bucketIndex, ++count);

Step 3: Find the most occurrence/tallest bin in histogramData. This is used to determine the height of each bin accordingly when rendering.

Step 4: Create an XML layout file for rendering a bar in a bin. I named this file, histogram_bar.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_weight="1">
    <View
        android:id="@+id/filled_space"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="#28f" />
</LinearLayout>

Step 5: Rendering the histogram. Create a class that extends LinearLayout. I named this class HistogramView. Override the invalidate() method to draw the graph. Iterate through each bin to inflate a bar using the histogram_bar.xml and add to this class (LinearLayout).

final LayoutInflater inflater = LayoutInflater.from(context);
final int barWidth = layoutWidth / numBins;
for (int i=0; i < numBins; i++) {
    int barHeight = 0;
    if (histogramData.get(i) != null) {
        barHeight = (int)(1f * histogramData.get(i) / mostOccurrence * layoutHeight);
    }
    final View bar = inflater.inflate(R.layout.histogram_bar, null);
    bar.findViewById(R.id.filled_space)
        .setLayoutParams(new LayoutParams(barWidth, barHeight));
    if (barHeight == 0) {
        bar.setVisibility(View.INVISIBLE);
    }
    addView(bar);
}

The result should look like this:

Price histogram

The width of each bar is constant, and the height of each bar is calculated by getting the ratio of the most occurrences in the data. This implementation uses all of the data points, but if you wish to ignore any outliers there is a slight modification to step 0. The highest value can be the nth percentile of the data.

Now that the HistogramView is created, we can add it to a XML layout along with a SeekBar.

Step 6: Piecing it together. Include a HistogramView and SeekBar to your layout.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <com.hipmunk.HistogramView
        android:id="@+id/price_graph_view"
        android:layout_width="match_parent"
        android:layout_height="50dp" />
    <SeekBar
        android:id="@+id/price_filter_seek_bar"
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:layout_marginTop="-16dp"
        android:progress="100" />
</LinearLayout>

Notice the negative margin for the SeekBar and this is purely designed so that the thumb sits centered at the bottom of the histogram. You may also achieve this with other ViewGroups aside from a LinearLayout. Set an OnSeekBarChangeListener to the SeekBar to listen for the progress changes. The callback updates the maximum price per night and filters out the more expensive hotels from a given list. This concludes the implementation of Hipmunk’s Price Per Night Hotel Filter.

The integration of the new hotel filters was a fun project. Making custom views is a creative process as one takes available widgets on a platform and combine them to create an elegant user interface.