How to have RecyclerView snapped to center and yet be able to scroll to all items, while the center is “selected”?

前端 未结 2 1725
心在旅途
心在旅途 2021-02-03 15:27

Background

I\'m try to achieve something similar to what the Camera app has for its modes:

I might probably not need to have a ViewPager, as seems tha

2条回答
  •  不思量自难忘°
    2021-02-03 15:37

    I gave this a try

    5 items: https://drive.google.com/open?id=1RPyiY9UndXcrbfBDWLB-UklxjPKMiR8- 2 items: https://drive.google.com/open?id=1HkG8NShxQ3illFupK-urSPwsUhag74WS

    First, apply an item decoration to center the first and last items:

    class CenterDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
    
        private var firstViewWidth = -1
        private var lastViewWidth = -1
    
        override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
            super.getItemOffsets(outRect, view, parent, state)
            val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition
            val lm = parent.layoutManager as LinearLayoutManager
            if (adapterPosition == 0) {
                // Invalidate decorations when this view width has changed
                if (view.width != firstViewWidth) {
                    view.doOnPreDraw { parent.invalidateItemDecorations() }
                }
                firstViewWidth = view.width
                outRect.left = parent.width / 2 - view.width / 2
                // If we have more items, use the spacing provided
                if (lm.itemCount > 1) {
                    outRect.right = spacing / 2
                } else {
                    // Otherwise, make sure this to fill the whole width with the decoration
                    outRect.right = outRect.left
                }
            } else if (adapterPosition == lm.itemCount - 1) {
                // Invalidate decorations when this view width has changed
                if (view.width != lastViewWidth) {
                    view.doOnPreDraw { parent.invalidateItemDecorations() }
                }
                lastViewWidth = view.width
                outRect.right = parent.width / 2 - view.width / 2
                outRect.left = spacing / 2
            } else {
                outRect.left = spacing / 2
                outRect.right = spacing / 2
            }
        }
    
    }
    

    Now, LinearSnapHelper determines the center of a view and includes its decorations. You can create a custom one that excludes the decorations from the calculation to center the view only:

    /**
     * A LinearSnapHelper that ignores item decorations to determine a view's center
     */
    class CenterSnapHelper : LinearSnapHelper() {
    
        private var verticalHelper: OrientationHelper? = null
        private var horizontalHelper: OrientationHelper? = null
        private var scrolled = false
        private var recyclerView: RecyclerView? = null
        private val scrollListener = object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolled) {
                    if (recyclerView.layoutManager != null) {
                        val view = findSnapView(recyclerView.layoutManager)
                        if (view != null) {
                            val out = calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view)
                            if (out != null) {
                                recyclerView.smoothScrollBy(out[0], out[1])
                            }
                        }
                    }
                    scrolled = false
                } else {
                    scrolled = true
                }
            }
        }
    
        fun scrollTo(position: Int, smooth: Boolean) {
            if (recyclerView?.layoutManager != null) {
                val viewHolder = recyclerView!!.findViewHolderForAdapterPosition(position)
                if (viewHolder != null) {
                    val distances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager!!, viewHolder.itemView)
                    if (smooth) {
                        recyclerView!!.smoothScrollBy(distances!![0], distances[1])
                    } else {
                        recyclerView!!.scrollBy(distances!![0], distances[1])
                    }
                } else {
                    if (smooth) {
                        recyclerView!!.smoothScrollToPosition(position)
                    } else {
                        recyclerView!!.scrollToPosition(position)
                    }
                }
            }
        }
    
        override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
            if (layoutManager == null) {
                return null
            }
            if (layoutManager.canScrollVertically()) {
                return findCenterView(layoutManager, getVerticalHelper(layoutManager))
            } else if (layoutManager.canScrollHorizontally()) {
                return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
            }
            return null
        }
    
        override fun attachToRecyclerView(recyclerView: RecyclerView?) {
            this.recyclerView = recyclerView
            recyclerView?.addOnScrollListener(scrollListener)
        }
    
        override fun calculateDistanceToFinalSnap(
            layoutManager: RecyclerView.LayoutManager,
            targetView: View
        ): IntArray? {
            val out = IntArray(2)
            if (layoutManager.canScrollHorizontally()) {
                out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager))
            } else {
                out[0] = 0
            }
            if (layoutManager.canScrollVertically()) {
                out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager))
            } else {
                out[1] = 0
            }
            return out
        }
    
        private fun findCenterView(
            layoutManager: RecyclerView.LayoutManager,
            helper: OrientationHelper
        ): View? {
            val childCount = layoutManager.childCount
            if (childCount == 0) {
                return null
            }
            var closestChild: View? = null
            val center: Int = if (layoutManager.clipToPadding) {
                helper.startAfterPadding + helper.totalSpace / 2
            } else {
                helper.end / 2
            }
            var absClosest = Integer.MAX_VALUE
    
            for (i in 0 until childCount) {
                val child = layoutManager.getChildAt(i)
                val childCenter = if (helper == horizontalHelper) {
                    (child!!.x + child.width / 2).toInt()
                } else {
                    (child!!.y + child.height / 2).toInt()
                }
                val absDistance = Math.abs(childCenter - center)
    
                if (absDistance < absClosest) {
                    absClosest = absDistance
                    closestChild = child
                }
            }
            return closestChild
        }
    
        private fun distanceToCenter(
            layoutManager: RecyclerView.LayoutManager,
            targetView: View,
            helper: OrientationHelper
        ): Int {
            val childCenter = if (helper == horizontalHelper) {
                (targetView.x + targetView.width / 2).toInt()
            } else {
                (targetView.y + targetView.height / 2).toInt()
            }
            val containerCenter = if (layoutManager.clipToPadding) {
                helper.startAfterPadding + helper.totalSpace / 2
            } else {
                helper.end / 2
            }
            return childCenter - containerCenter
        }
    
        private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
            if (verticalHelper == null || verticalHelper!!.layoutManager !== layoutManager) {
                verticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
            }
            return verticalHelper!!
        }
    
        private fun getHorizontalHelper(
            layoutManager: RecyclerView.LayoutManager
        ): OrientationHelper {
            if (horizontalHelper == null || horizontalHelper!!.layoutManager !== layoutManager) {
                horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
            }
            return horizontalHelper!!
        }
    }
    

    Usage:

    class MainActivity : AppCompatActivity() {
    
        private val snapHelper = CenterSnapHelper()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            recyclerView.addItemDecoration(CenterDecoration(0))
            snapHelper.attachToRecyclerView(recyclerView)
            recyclerView.adapter = object : RecyclerView.Adapter() {
                override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
                    val holder = object : RecyclerView.ViewHolder(
                        LayoutInflater.from(this@MainActivity).inflate(
                            R.layout.list_item,
                            parent,
                            false
                        )
                    ) {}
                    holder.itemView.setOnClickListener {
                        if (holder.adapterPosition != RecyclerView.NO_POSITION) {
                            snapHelper.scrollTo(holder.adapterPosition, true)
                        }
                    }
                    return holder
                }
    
                override fun getItemCount(): Int = 20
    
                override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                    holder.itemView.textView.text = "pos:$position"
                }
            }
    
        }
    }
    

    Posting XML here in case someone wants to check this out:

    MainActivity

    
    
    
        
    
        
    
    
    

    list_item.xml

    
    

    EDIT: here's a sample of how to use this:

    http://s000.tinyupload.com/?file_id=01184747175525079378

提交回复
热议问题