Jamalzahid / Horizontal-Vertical-Slider-Picker

Implementing Horizontal and Vertical Slider in Android using RecyclerView and subclassing LinearLayoutManager.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Create Your Own Horizontal (& Vertical) Slider/Picker

Recently at Reali we needed to create a Horizontal Slider. I was looking around the web and learned what's the minimum skeleton your code need to have. All the rest - you can customize.

What is the Picker functionality?

A Slider must have the following functionality:

  • Scroll / Fling with snapping (snapping = putting the item's center in the Slider's center)
  • Clicking on an item will smoothly-scroll the item to the Slider's center

The bare minimum we need

There minimal parts that we need to create are:

  • RecyclerView: Nothing new here. Adapter, Views…
  • Extending LinearLayoutManager: Here we will handle the smooth-scrolling & snapping. We will also expose a callback for when item is being selected: click / scroll / fling.

First Step: boilerplate code

Need to setup an activity, layout, RecyclerView, adapter and all that. After adding the boilerplate, with some styling, our the result will look like:

As you can see, there are multiple problems here:

  1. The items near the edges ("1" and "20" and more) cannot be in the center, since the RecyclerView is attached to parent-left and parent-right.
  2. Clicks doesn't put the item in the center.
  3. Scrolling / Flinging can stop between two items (no snapping)
  4. Notifying on item selection.

Second Step: padding to items "1" and "20"

This simple - we only need to set "clipToPadding=false" in the XML, and give left-padding and right-padding to the RecyclerView:

clipToPadding:

<android.support.v7.widget.RecyclerView
    android:id="@+id/rv_horizontal_picker"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:clipToPadding="false" />

Padding for "1" and "20"

In order to have "1" and "20" in the center we need to give padding to the RecyclerView. But because the screen-sizes and density variety, the padding needs to be calculated. In our example, the slider stretches to the parent edges, so the formula is:

val padding: Int = ScreenUtils.getScreenWidth(this)/2
rvHorizontalPicker.setPadding(padding, 0, padding, 0)

As you can see it's not enough because we moved the "1" and "20" too much:

This is because we need to consider the padding of the slider-item, which currently is set to 40dp. So let's modify the formula above:

val padding: Int = ScreenUtils.getScreenWidth(this)/2 - ScreenUtils.dpToPx(this, 40)
rvHorizontalPicker.setPadding(padding, 0, padding, 0)

And now we're much better:

Still there's some padding work to do, but the concept is clear.

Third Step: click handling

On a click event, we want the slider to have the clicked-item in the center. We just call the RecyclerView smoothScrollToPosition(…) function in the onClick():

// This code is in the adapter onCreateViewHolder(...):
itemView.setOnClickListener(object : View.OnClickListener {
    override fun onClick(v: View?) {
        v?.let { callback?.onItemClicked(it) }
    }
}
...
...
...
// This code is in the activity / fragment
override fun onItemClicked(view: View) {
    val position = rvHorizontalPicker.getChildLayoutPosition(view)
    rvHorizontalPicker.smoothScrollToPosition(position)
}

And the result is:

Fourth Step: Snapping

When the user scrolls the screen, we want the selected item to be exactly in the RecyclerView's center. Here LinearSnapHelper comes to our aid:

// Smart snapping
LinearSnapHelper().attachToRecyclerView(recyclerView)

You can add it in the RecyclerView creation. The result is:

Now you can see, that the slider isn't stopping between to items. This is what the LinearSnapHelper is doing.

Fifth Step: notifying on item selection

Here's where our custom LinearLayoutManager comes handy. The concept is: when scroll is finished, we search which item is closest to the slider center, and return it's position:

class SliderLayoutManager(context: Context?) : LinearLayoutManager(context) {

    var callback: OnItemSelectedListener? = null

    override fun onScrollStateChanged(state: Int) {
        super.onScrollStateChanged(state)

        // When scroll stops we notify on the selected item
        if (state.equals(RecyclerView.SCROLL_STATE_IDLE)) {

            // Find the closest child to the recyclerView center --> this is the selected item.
            val recyclerViewCenterX = getRecyclerViewCenterX()
            var minDistance = recyclerView.width
            var position = -1
            for (i in 0 until recyclerView.childCount) {
                val child = recyclerView.getChildAt(i)
                val childCenterX = getDecoratedLeft(child) + (getDecoratedRight(child) - getDecoratedLeft(child)) / 2
                var childDistanceFromCenter = Math.abs(childCenterX - recyclerViewCenterX)
                if (childDistanceFromCenter < minDistance) {
                    minDistance = childDistanceFromCenter
                    position = recyclerView.getChildLayoutPosition(child)
                }
            }

            // Notify on the selected item
            callback?.onItemSelected(position)
        }
    }

    private fun getRecyclerViewCenterX() : Int {
        return (recyclerView.right - recyclerView.left)/2 + recyclerView.left
    }

    interface OnItemSelectedListener {
        fun onItemSelected(layoutPosition: Int)
    }
}

All the logic happen inside onScrollStateChanged(…). We search the slider item, which his center is closest to the slider's center (AKA, the item in the center) and notify it via callback?.onItemSelected(position).

And in the activity we handle the click. We set the callback when creating the layout manager:

``` // Setting layout manager rvHorizontalPicker.layoutManager = SliderLayoutManager(this).apply { callback = object : SliderLayoutManager.OnItemSelectedListener { override fun onItemSelected(layoutPosition: Int) { tvSelectedItem.setText(data[layoutPosition]) } } } ``` And the result is:

Extra Effects

scaling:

Scaling is simple: the item in the center has scale 1.0f, and further items are scaled according to their distance from the center. During a scroll we calculate the scaling (inside the LayoutManager):

class SliderLayoutManager(context: Context?) : LinearLayoutManager(context) {

    ...

    override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State) {
        super.onLayoutChildren(recycler, state)
        scaleDownView()
    }

    override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
        if (orientation == LinearLayoutManager.HORIZONTAL) {
            val scrolled = super.scrollHorizontallyBy(dx, recycler, state)
            scaleDownView()
            return scrolled
        } else {
            return 0
        }
    }

    private fun scaleDownView() {
        val mid = width / 2.0f
        for (i in 0 until childCount) {
        
            // Calculating the distance of the child from the center
            val child = getChildAt(i)
            val childMid = (getDecoratedLeft(child) + getDecoratedRight(child)) / 2.0f
            val distanceFromCenter = Math.abs(mid - childMid)
        
            // The scaling formula
            val scale = 1-Math.sqrt((distanceFromCenter/width).toDouble()).toFloat()*0.66f
        
            // Set scale to view
            child.scaleX = scale
            child.scaleY = scale
        }
    }
    ...
    ...
}

I chose the squared root function to determine the scale. The result is:
Because of it's shape, the items that are closer the the center are scaled down less, and further items are scaled harder. This is just me. A linear approach would be:

val scale = 1-distanceFromCenter/width

How to make the slider vertical?

Very simple: instead of caluclating the center and width using "centerX" and "width" we will use their vertical counterparts - "centerY" and "height". The rest of the logic stays the same.

Summary

I hope you enjoyed and learned from this post. Instead of bloating your code with unnecessary code, here you have the basic functionality you need. Happy coding :)

About

Implementing Horizontal and Vertical Slider in Android using RecyclerView and subclassing LinearLayoutManager.


Languages

Language:Kotlin 100.0%