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

醉酒当歌 提交于 2020-11-30 07:37:47

问题


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 that it uses above the horizontal list, but could be nice to have it as an option.

The problem

While technically I succeeded to have the RecyclerView center its items, it doesn't let you actually have the all items to be able to be on the center (example is the first/last one). When you try to scroll to the first or last items, it doesn't let you , because you've reached the edge of the RecyclerView :

Not only that, but in the beginning it doesn't really center, and if I have the RecyclerView have few items, it becomes a problem because I want them to be centered, but having android:layout_width="match_parent" (because all of it should be touch-able) produces this:

while having android:layout_width="wrap_content" get me this:

On both cases, I can't scroll at all. When it's "wrap_content", it's a problem because I won't be able to scroll on the sides.

What I've tried

It's possible to snap the items so that there will always be an item in the center of RecyclerView as such :

val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(categoriesRecyclerView)

We can also get which is the item in the center (as shown here), by having a scroll listener and using snapHelper.findSnapView(layoutManagaer).

But as I wrote, I can't really have the first/last item being selected this way, because I can't scroll to it so that it will be at the middle.

I tried to look at the docs of the related classes, but I can't find such a thing.

Here's the current code (sample available here) :

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        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 {
                }
                return holder
            }

            override fun getItemCount(): Int = 20

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                holder.itemView.textView.text = "pos:$position"
            }
        }
        val snapHelper = LinearSnapHelper()
        snapHelper.attachToRecyclerView(recyclerView)
    }
}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView android:background="#66000000"
                                               android:id="@+id/recyclerView"
                                               android:layout_width="match_parent"
                                               android:layout_height="@dimen/list_item_size"
                                               android:orientation="horizontal"
                                               app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                                               app:layout_constraintBottom_toBottomOf="parent"
                                               app:layout_constraintEnd_toEndOf="parent"
                                               app:layout_constraintStart_toStartOf="parent"
                                               app:layout_constraintTop_toTopOf="parent"
                                               tools:listitem="@layout/list_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>

list_item.xml

<TextView
    android:id="@+id/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:layout_width="wrap_content" android:layout_height="@dimen/list_item_size"
    android:background="?attr/selectableItemBackground" android:breakStrategy="balanced" 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"
    app:autoSizeTextType="uniform" tools:targetApi="m" tools:text="@tools:sample/lorem"/>

The question

How can I let the user freely scroll inside, so that the edge will be determined by whether the first/last item is in the middle? How can I always have the items centered, including when I just started seeing the RecyclerView), and including when there are few of them?


回答1:


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




回答2:


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();
    }
}


来源:https://stackoverflow.com/questions/53483268/how-to-have-recyclerview-snapped-to-center-and-yet-be-able-to-scroll-to-all-item

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!