Implementing Search Filter Animation in Kotlin for Quora Meets LinkedIn, Our App Design Concept

Not a long time ago we published Fit (female in IT), a design concept that supports women in technology. The main idea of the Fit app concept is to build a community of women in IT committed to helping each other succeed. This app was intended to empower women to explore job opportunities and connect with other women who share similar professional interests.

We wanted Fit to be something like Quora plus Linkedin where users can post both questions and job vacancies. The questions that users could ask on Fit and the jobs that they could post can relate to one of the following professional fields: design, development, management, customer support, research, business analysis, sales, and quality assurance.

When we started building wireframes we realised that we needed filters to make sure users can quickly find questions and jobs they are looking for.

Before posting a job or writing a question a user must choose one or two categories or filters for her post. Categories and filters are the same thing. They work like hashtags when you’re filtering Questions and Job results.

We created a bar for filters right under the navigation bar. This solution offers an easy and non obtrusive way of interaction with the app’s content.

After we finished our design concept, and published a case study and a few shots on Dribbble, we started thinking how we can implement our filtering solution.

We decided to develop our Search Filter component for Android and make our code available under an open source licence just like a bunch of our other libraries. In this case though we decided to experiment with Kotlin programming language. But don’t worry, you can easily use our library in your own project because Kotlin is designed to interoperate with Java code.

search filter animation for Android

How we implemented Search Filter animation

The architecture of the filter view is more difficult than it appears. We had to make it scalable so it can be independent from the filter items count. We chose the combination of ScrollView and HorizontalScrollView as a solution to this problem.

The ScrollView contains an expanded filter view that places items in such a way that they take all the possible width of the parent view. The HorizontalScrollView has a collapsed filter view where filter items are placed one after another.

 

Filter.png

 

Search Filter item animation

After a user has choosen filters, they collapse and turn into circles with a cancel icon in the center. To animate filter items collapsing we used ValueAnimator. The implementation of this feature is much easier in Kotlin than in Java because the apply() function calls the specified function block with this value as its receiver and returns this value. In other words, you don’t have to create a new variable to add the update listener like in Java.

ValueAnimator.ofFloat(0f, duration.toFloat()).setDuration(duration).apply {
   addUpdateListener {
       val ratio = it.animatedValue as Float / duration
       …
       mSelectedFilters.keys.forEachIndexed { index, filterItem ->
           filterItem.decrease(ratio)
           …
       }
       …
   }
}.start()

To explain the following code I need to describe the filter item’s architecture. Basically every filter contains two circles, a cancel icon, and a text view with its background view. To animate filter collapsing we are moving circles to the centre of the view until they merge into one circle. At the same time we’re decreasing text view background hiding the text and put the cancel icon on top of the circle.

fun decrease(ratio: Float) {
   text.scaleX = 1 - 0.2f * ratio
   text.alpha = 1 - ratio
   buttonCancel.alpha = ratio
   textBackground.scaleX = 1 - ratio
   viewLeft.translationX = getCirclePosition().toFloat() * ratio
   viewRight.translationX = -getCirclePosition().toFloat() * ratio
   if (ratio == 0f) {
       buttonCancel.visibility = View.VISIBLE
       buttonCancel.alpha = 0f
   }

   if (ratio == 1f) {
       text.scaleX = 0f
   }

   isIncreased = false
}

To increase filters we just revert to the view’s previous state:

fun increase(ratio: Float) {
   text.scaleX = 1f
   text.alpha = ratio
   buttonCancel.alpha = 1 - alpha
   textBackground.scaleX = ratio
   viewLeft.translationX = getCirclePosition().toFloat() * (1 - ratio)
   viewRight.translationX = -getCirclePosition().toFloat() * (1 - ratio)

   if (ratio == 1f) {
       buttonCancel.visibility = View.GONE
       fullSize = measuredWidth
   }
   isIncreased = true
}


Removing filter items

Our filters that now look like circles may in fact take more place than just one circle. Sometimes the root view of one item overlaps the cancel icon of another item. This means that removing a filter from the bar might be difficult for a user.

We decided to intercept all the touch events in our CollapsedFilterView. To do that we need to override onInterceptTouchEvent() method to return ‘true’ when we need to receive the touch event so we can handle it in onTouchEvent() method.

override fun onInterceptTouchEvent(event: MotionEvent?): Boolean = childCount > 0
override fun onTouchEvent(event: MotionEvent): Boolean {
   when (event.action) {
       MotionEvent.ACTION_DOWN -> {
           mStartX = event.x
           mStartY = event.y
       }
       MotionEvent.ACTION_UP -> {
           if (isClick(mStartX, mStartY, event.x, event.y)) {
               findViewByCoord(event.x)?.dismiss()
           }
       }
…
   }
   return true
}

When the touch has just started we save its initial coordinates and compare them with the coordinates of the previous touch position. This way we can determine if this event is a click or not. If it’s a click, we find the view that contains the needed coordinates and remove this view from the container.

private fun findViewByCoord(x: Float): FilterItem? {
   for (i in 0..childCount - 1) {
       val item: FilterItem = getChildAt(i) as FilterItem
       if (containsCoord(item, x)) {
           return item
       }
   }
   return null
}
private fun containsCoord(item: FilterItem, x: Float): Boolean
       = item.x + item.fullSize / 2 - item.getCollapsedSize() / 2 <= x && x <= item.x + item.fullSize / 2 + item.getCollapsedSize() / 2

To sum up

Even though Search Filter animation was my first try with Kotlin, working with a new programming language wasn’t difficult for me. I didn’t face any Kotlin and Java compatibility issues. Kotlin is intuitive and clear for a developer who already knows Java well.

We plan to add some new cool features to our Search Filter animation such as beautiful press effects for filter items and advanced pull animation for the filter container. These features are already in the development!

Check out Filter animation on:

4.3/ 5.0
Article rating
16
Reviews
Remember those Facebook reactions? Well, we aren't Facebook but we love reactions too. They can give us valuable insights on how to improve what we're doing. Whould you tell us how you feel about this article?