Implementing Horizontal picker in Android using RecyclerView and LinearLayoutManager with kotlin.
Its a number picker which has the below functionality.
>left to right, right to left scrolling, snapping
>middle item will be selected, clicking on item will also be selected.
github link is below:
- Create an android studio project
- Add an adapter and a recyclerView in xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/dp_6"
android:paddingTop="@dimen/dp_5"
android:paddingRight="@dimen/dp_6"
android:paddingBottom="@dimen/dp_5">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
style="@style/TextStyle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="10"
android:textSize="@dimen/sp_36"
/>
</androidx.constraintlayout.widget.ConstraintLayout>this is the recycler view in main_activity.xml<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_horizontal_picker"
android:layout_width="@dimen/dp_0"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="5"
tools:listitem="@layout/row_view_slider_item"
/>
3. Copy the code for pickerViewHolder class
class PickerItemViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
val tvItem: TextView? = itemView?.findViewById(com.sazib.mypicker.R.id.tv_item)
}and the picker row_view_picker_item.xml is below<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/dp_6"
android:paddingTop="@dimen/dp_5"
android:paddingRight="@dimen/dp_6"
android:paddingBottom="@dimen/dp_5">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
style="@style/TextStyle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="10"
android:textSize="@dimen/sp_36"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
4. Write the below code for picker adapter
class PickerAdapter : RecyclerView.Adapter<PickerItemViewHolder>() {
private val data: ArrayList<String> = ArrayList()
var callback: Callback? = null
private val clickListener = View.OnClickListener { v -> v?.let { callback?.onItemClicked(it) } }
private var selectedItem: Int? = -1
private var ctx: Context? = null
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PickerItemViewHolder {
ctx = parent.context
val itemView: View = LayoutInflater.from(parent.context)
.inflate(R.layout.row_view_slider_item, parent, false)
itemView.setOnClickListener(clickListener)
return PickerItemViewHolder(itemView)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(
holder: PickerItemViewHolder,
position: Int
) {
holder.tvItem?.text = data[position]
when (selectedItem) {
position -> {
holder.tvItem?.setTextColor(ContextCompat.getColor(ctx!!, R.color.colorBlack))
selectedItem = -1
}
else -> holder.tvItem?.setTextColor(ContextCompat.getColor(ctx!!, R.color.text_color))
}
}
fun setSelectedItem(position: Int) {
selectedItem = position
notifyDataSetChanged()
}
fun setData(data: ArrayList<String>) {
this.data.clear()
this.data.addAll(data)
notifyDataSetChanged()
}
interface Callback {
fun onItemClicked(view: View)
}
}
5. ScreenUtils.kt for measuring the screeen width. It is needed too measure the middle point to the screen.
class ScreenUtils {
companion object {
fun getScreenWidth(context: Context): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val dm = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(dm)
return dm.widthPixels
}
fun dpToPx(
context: Context,
value: Int
): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), context.resources.displayMetrics
)
.toInt()
}
}
}
6. The magic class PickerLayoutManager.kt
class PickerLayoutManager(context: Context?) : LinearLayoutManager(context) {
init {
orientation = HORIZONTAL
}
var callback: OnItemSelectedListener? = null
private lateinit var recyclerView: RecyclerView
override fun onAttachedToWindow(view: RecyclerView?) {
super.onAttachedToWindow(view)
recyclerView = view!!
// snapping
LinearSnapHelper().attachToRecyclerView(recyclerView)
}
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 {
return if (orientation == HORIZONTAL) {
val scrolled = super.scrollHorizontallyBy(dx, recycler, state)
scaleDownView()
scrolled
} else {
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 = abs(mid - childMid)
// The scaling formula
val scale = 1 - sqrt((distanceFromCenter / width).toDouble()).toFloat() * 0.66f
// Set scale to view
child.scaleX = scale
child.scaleY = scale
}
}
override fun onScrollStateChanged(state: Int) {
super.onScrollStateChanged(state)
// When scroll stops we notify on the selected item
if (state == 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
val newDistance = abs(childCenterX - recyclerViewCenterX)
if (newDistance < minDistance) {
minDistance = newDistance
position = recyclerView.getChildLayoutPosition(child)
}
}
// Notify on item selection
callback?.onItemSelected(position)
}
}
private fun getRecyclerViewCenterX(): Int {
return (recyclerView.right - recyclerView.left) / 2 + recyclerView.left
}
interface OnItemSelectedListener {
fun onItemSelected(layoutPosition: Int)
}
}
7. init the adapter and set the layout manager in MainActivity.kt
class MainActivity : AppCompatActivity() {
private val data = (1..10).toList()
.map { it.toString() } as ArrayList<String>
private lateinit var rvHorizontalPicker: RecyclerView
private lateinit var sliderAdapter: PickerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setHorizontalPicker()
}
private fun setHorizontalPicker() {
rvHorizontalPicker = findViewById(R.id.rv_horizontal_picker)
// Setting the padding such that the items will appear in the middle of the screen
val padding: Int = ScreenUtils.getScreenWidth(this) / 2 - ScreenUtils.dpToPx(this, 40)
rvHorizontalPicker.setPadding(padding, 0, padding, 0)
// Setting layout manager
rvHorizontalPicker.layoutManager = PickerLayoutManager(this).apply {
callback = object : PickerLayoutManager.OnItemSelectedListener {
override fun onItemSelected(layoutPosition: Int) {
sliderAdapter.setSelectedItem(layoutPosition)
Log.d("selected text", data[layoutPosition])
Toast.makeText(this@MainActivity, data[layoutPosition], Toast.LENGTH_SHORT).show()
}
}
}
// Setting Adapter
sliderAdapter = PickerAdapter()
rvHorizontalPicker.adapter = sliderAdapter.apply {
setData(data)
callback = object : PickerAdapter.Callback {
override fun onItemClicked(view: View) {
rvHorizontalPicker.smoothScrollToPosition(
rvHorizontalPicker.getChildLayoutPosition(
view
)
)
}
}
}
}
}
A deep dive into it:
>formula for padding of the each item:
// Setting the padding such that the items will appear in the middle //of the screen
val padding: Int = ScreenUtils.getScreenWidth(this) / 2 - ScreenUtils.dpToPx(this, 40)
rvHorizontalPicker.setPadding(padding, 0, padding, 0)
>For snapping
LinearSnapHelper().attachToRecyclerView(recyclerView)
>adapter callback for click handling
// Setting Adapter
sliderAdapter = PickerAdapter()
rvHorizontalPicker.adapter = sliderAdapter.apply {
setData(data)
callback = object : PickerAdapter.Callback {
override fun onItemClicked(view: View) {
rvHorizontalPicker.smoothScrollToPosition(
rvHorizontalPicker.getChildLayoutPosition(
view
)
)
}
}
}
>scaling: the item in the center has scale 1F, and further items are scaled according to their distance from the center.
val scale = 1 - sqrt((distanceFromCenter / width).toDouble()).toFloat() * 0.66f
> lastly I edited the code and style from this link given below:
github.com/nbtk123/Horizontal-Vertical-Slider-Picker
Bonus: There is a another picker using textView in the below repository. You can use it and modify it if you wish.
https://github.com/sazibislam/MyHorizontal-Picker
Download my app from play store:
https://play.google.com/store/apps/details?id=com.sazib.my_feedback
Ping me: Linkedin