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

前端 未结 2 1729
心在旅途
心在旅途 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<RecyclerView.ViewHolder>() {
                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

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    
        <View
            android:layout_width="4dp"
            android:layout_height="0dp"
            android:background="@color/colorAccent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    list_item.xml

    <TextView
            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:id="@+id/textView"
            android:layout_width="wrap_content" android:layout_height="@dimen/list_item_size"
            android:background="?attr/selectableItemBackground" android:clickable="true"
            android:focusable="true" android:gravity="center" android:maxLines="1" android:padding="8dp"
            android:shadowColor="#222" android:shadowDx="1" android:shadowDy="1" android:textColor="#fff"
            tools:targetApi="m" tools:text="@tools:sample/lorem"/>
    

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

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

    0 讨论(0)
  • 2021-02-03 15:57

    This is how i solved it. The problem on custom snaphelper and decorators is that they dont work with other libaries and custom Views. It also works with items with variable widths.

    If you want to snap the items, just use the classic snaphelper on the recyclerview

    public class CenterRecyclerView extends RecyclerView {
    
        public CenterRecyclerView(@NonNull Context context) {
            super(context);
        }
    
        public CenterRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public CenterRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public void updatePadding() {
            post(() -> {
                final DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
                final int screenWidth = displayMetrics.widthPixels;
                final int screenHeight = displayMetrics.heightPixels;
    
                ViewHolder firstViewHolder = findViewHolderForAdapterPosition(0);
                if (firstViewHolder != null) {
                    firstViewHolder.itemView.measure(WRAP_CONTENT, WRAP_CONTENT);
                    int viewWidth = firstViewHolder.itemView.getMeasuredWidth();
                    int padding;
                    if (screenHeight > screenWidth) {
                        //Portrait
                        padding = screenWidth / 2 - viewWidth / 2;
                    } else {
                        //Landscape
                        padding = screenHeight / 2 - viewWidth / 2;
                    }
                    setPadding(padding, 0, padding, 0);
                } else {
                    Log.e("CenterRecyclerView", "Could not get first ViewHolder");
                }
            });
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            updatePadding();
        }
    
        @Override
        protected void onConfigurationChanged(Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            updatePadding();
        }
    }
    
    0 讨论(0)
提交回复
热议问题