Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
20
Help us understand the problem. What is going on with this article?
@nakker1218

RecyclerViewでFastScrollを実装する

More than 1 year has passed since last update.

この記事はAndroidその2 Advent Calendar 2016 の13日目の記事です。
2018/08/17リファクタリングとKotlin化しました。

はじめに

みなさん、RecyclerViewでもListViewと同じようにFastScrollしたいですよね?
ListViewでFastScrollをするためには、 setFastScrollEnabled()true にすすだけで良いですが、RecyclerViewにはそのようなメソッドは用意されていないため自分で実装する必要があります。
そこで、この記事ではRecyclerViewにFastScrollを実装するベストプラクティスをまとめました。

実装方法

RecyclerFastScroller.kt
class RecyclerFastScroller @JvmOverloads constructor(context: Context,
                                                     attrs: AttributeSet? = null,
                                                     defStyleAttr: Int = 0)
    : LinearLayout(context, attrs, defStyleAttr) {

    private val onFastScrollListener: OnFastScrollListener = OnFastScrollListener()

    private var scrollerHeight: Int = 0
    private var recyclerView: RecyclerView? = null
    private var currentAnimator: ObjectAnimator? = null

    companion object {
        private const val BUBBLE_ANIMATION_DURATION: Long = 100
        private const val TRACK_SNAP_RANGE: Int = 5
    }

    init {
        LayoutInflater.from(context).inflate(R.layout.item_fast_scroller, this, true)
        bubble.visibility = View.GONE

        orientation = HORIZONTAL
        clipChildren = false
    }

    override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
        super.onSizeChanged(width, height, oldWidth, oldHeight)

        scrollerHeight = height
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val action = event?.action ?: return false
        when (action) {
            MotionEvent.ACTION_DOWN -> {
                if (event.x < handle.x - ViewCompat.getPaddingStart(handle)) return false
                currentAnimator?.cancel()

                if (bubble.visibility == View.GONE) {
                    showBubble()
                }
                handle.isSelected = true
                val y = event.y
                setFastScrollerPosition(y)
                setRecyclerViewPosition(y)
                return true
            }
            MotionEvent.ACTION_MOVE -> {
                val y = event.y
                setFastScrollerPosition(y)
                setRecyclerViewPosition(y)
                return true
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                handle.isSelected = false
                hideBubble()
                return true
            }
        }
        return super.onTouchEvent(event)
    }

    override fun onDetachedFromWindow() {
        recyclerView?.removeOnScrollListener(onFastScrollListener)
        super.onDetachedFromWindow()
    }

    fun setRecyclerView(recyclerView: RecyclerView) {
        this.recyclerView = recyclerView
        this.recyclerView?.addOnScrollListener(onFastScrollListener)
    }

    private fun showBubble() {
        bubble.visibility = View.VISIBLE
        currentAnimator?.cancel()
        currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).apply {
            duration = BUBBLE_ANIMATION_DURATION
            start()
        }
    }

    private fun hideBubble() {
        currentAnimator?.cancel()
        currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).apply {
            duration = BUBBLE_ANIMATION_DURATION
            addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator?) {
                    super.onAnimationEnd(animation)

                    hide()
                }

                override fun onAnimationCancel(animation: Animator?) {
                    super.onAnimationCancel(animation)

                    hide()
                }

                private fun hide() {
                    bubble.visibility = View.GONE
                    currentAnimator = null
                }
            })

            start()
        }
    }

    private fun updateFastScrollerPosition() {
        if (handle.isSelected) return
        recyclerView?.let {
            val verticalScrollOffset = it.computeVerticalScrollOffset()
            val verticalScrollRange = it.computeVerticalScrollRange()

            val proportion = verticalScrollOffset.toFloat() / verticalScrollRange
            setFastScrollerPosition(scrollerHeight * proportion)
        }
    }

    private fun setFastScrollerPosition(y: Float) {
        val handleHeight = handle.height
        handle.y = getValueInRange(0, scrollerHeight - handleHeight, (y - (handleHeight / 2)).toInt())

        val bubbleHeight = bubble.height
        bubble.y = getValueInRange(0, scrollerHeight - bubbleHeight - handleHeight / 2, (y - bubbleHeight).toInt())
    }

    private fun setRecyclerViewPosition(y: Float) {
        recyclerView?.let {
            val itemCount = it.adapter.itemCount
            val proportion = when {
                handle.y == 0f -> 0f
                handle.y + handle.height >= scrollerHeight - TRACK_SNAP_RANGE -> 1f
                else -> y / scrollerHeight.toFloat()
            }

            val targetPosition = getValueInRange(0, itemCount - 1, (proportion * itemCount.toFloat()).toInt()).toInt()
            (it.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPosition, 0)

            if (it.adapter is FastScrollable) {
                val bubbleText = (it.adapter as FastScrollable).setBubbleText(targetPosition)
                bubble.text = bubbleText
            }
        }
    }

    private fun getValueInRange(min: Int, max: Int, adjust: Int): Float {
        val minimum = Math.max(min, adjust)
        return Math.min(minimum, max).toFloat()
    }

    private inner class OnFastScrollListener : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
            updateFastScrollerPosition()
        }
    }

    interface FastScrollable {
        fun setBubbleText(position: Int): String
    }
}
item_fast_scroller.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/bubble"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/background_fast_scroller_bubble"
        android:gravity="center"
        android:textColor="@android:color/white"
        android:textSize="36sp"
        android:visibility="gone"/>

    <ImageView
        android:id="@+id/handle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:paddingStart="8dp"
        android:src="@drawable/background_fast_scroller_handle"/>

</merge>
background_fast_scroller_bubble.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="50dp"
       android:layout_height="50dp"
       android:shape="rectangle">

    <corners
        android:bottomLeftRadius="44dp"
        android:bottomRightRadius="0px"
        android:topLeftRadius="44dp"
        android:topRightRadius="44dp"/>

    <solid android:color="@color/colorAccent"/>

    <size
        android:width="88dp"
        android:height="88dp"/>
</shape>
background_fast_scroller_handle.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">

            <corners android:radius="2dp"/>

            <solid android:color="@color/colorAccent"/>

            <size android:width="4dp" android:height="32dp"/>
        </shape>
    </item>

    <item>
        <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">

            <corners android:radius="2dp"/>

            <solid android:color="#666666"/>

            <size android:width="4dp" android:height="32dp"/>
        </shape>
    </item>
</selector>

使い方

MainActivity.kt
fast_scroller.setRecyclerView(recycler_view)

RecyclerViewのAdapterにRecyclerFastScroller.FastScrollableを実装してください。setBubbleTextで返した文字列がBubbleのところに表示されます。

ItemAdapter.kt
class ItemAdapter(private val context: Context, list: List<String>? = null) : RecyclerView.Adapter<ItemAdapter.ViewHolder>(), RecyclerFastScroller.FastScrollable {

    override fun setBubbleText(position: Int): String {
        return items[position]
    }

    // 中略
}

最後に

サンプルプロジェクトです。
https://github.com/nakker1218/FastScroller

参考

20
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
nakker1218
Androidエンジニアしながら学生しています

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
20
Help us understand the problem. What is going on with this article?