如何将RecyclerView捕捉到中心并在“选择”中心的同时滚动到所有项目?

时间:2018-11-26 14:29:11

标签: android android-recyclerview

背景

我正在尝试实现与Camera应用程序的模式相似的功能:

enter image description here

我可能不需要使用ViewPager,因为它似乎在水平列表上方使用,但最好将其作为选项。

问题

从技术上讲,我成功地将RecyclerView中心作为其项目,但实际上并不能让所有项目都位于中心(例如,第一个/最后一个)。当您尝试滚动到第一项或最后一项时,由于您已经到达RecyclerView的边缘,所以它不允许您这样做:

enter image description here

不仅如此,而且一开始它并没有真正居中,如果我拥有RecyclerView的物品很少,那么这就会成为一个问题,因为我希望它们居中,但是具有android:layout_width =“ match_parent”(因为所有内容都应该是可触摸的)会产生以下结果:

enter image description here

在拥有android:layout_width =“ wrap_content”的同时给我这个消息:

enter image description here

在两种情况下,我都无法滚动。当它是“ wrap_content”时,这是一个问题,因为我将无法在侧面滚动。

我尝试过的

可以对齐项目,这样在RecyclerView的中心始终会有一个项目:

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

我们还可以通过使用滚动侦听器并使用snapHelper.findSnapView(layoutManagaer)来获得居中的项目(如here所示。)

但是,正如我所写的那样,我无法真正以这种方式选择第一个/最后一个项目,因为我无法滚动到该项目以使其位于中间。

我试图查看相关类的文档,但找不到这种东西。

这是当前代码(示例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"/>

问题

如何让用户自由地向内滚动,以便根据第一个/最后一个项目是否在中间来确定边缘?我如何才能始终使这些内容居中,包括刚开始看到RecyclerView时的内容,以及其中很少的内容?

2 个答案:

答案 0 :(得分:5)

我尝试了

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

首先,应用物品装饰以使第一个和最后一个物品居中:

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
        }
    }

}

现在,LinearSnapHelper确定视图的中心并包括其装饰。您可以创建一个自定义样式,以将装饰排除在计算之外,以仅使视图居中:

/**
 * 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!!
    }
}

用法:

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"
            }
        }

    }
}

在此处发布XML以防有人想要查看此信息

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"/>

编辑:这是如何使用它的示例:

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

答案 1 :(得分:0)

这就是我解决的方法。自定义snaphelper和decorator的问题在于它们不能与其他库和自定义View一起使用。它也适用于宽度可变的物品。

如果要捕捉项目,只需在recyclerview上使用经典的snaphelper

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