背景
我正在尝试实现与“相机”应用程序的模式相似的功能:
我可能不需要使用ViewPager,因为它似乎在水平列表上方使用,但最好将其作为选项。
问题
从技术上讲,我成功地将RecyclerView中心作为其项目,但实际上并不能让您将所有项目都放在中心(例如,第一个/最后一个)。当您尝试滚动到头一个或最后一个项目时,由于您已经到达RecyclerView的边缘,它不允许您使用:
不仅如此,而且一开始它并没有真正居中,而且如果我拥有RecyclerView的物品很少,那么这将成为一个问题,因为我希望将它们居中,但是要使用android:layout_width =“match_parent”(因为所有它应该是可触摸的)产生以下内容:
在拥有android:layout_width =“wrap_content”的同时给我这个:
在这两种情况下,我都无法滚动。当它是“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时的项目,以及其中很少的项目?
最佳答案
我尝试了一下
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确定 View 的中心并包括其装饰。您可以创建一个自定义项,以将装饰排除在计算之外,以仅使 View 居中:
/**
* 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