目录
前言
文章属于学习总结 ,如有错漏之处,敬请指正。
同系列文章:
Android控件RecyclerView(一)——大家都知道的RecyclerView
Android控件RecyclerView(二)——LayoutManager及其自定义
Android控件RecyclerView(三)——ItemDecoration的使用与自定义
1 SnapHelper基本使用
SnapHelper是RecyclerView的辅助类,是个抽象类,它有两个子类,LinearSnapHelper 与 PagerSnapHelper
1.1 LinearSnapHelper
作用:让RecyclerView滚动停止时相应的Item停留中间位置
用法:
LinearSnapHelper().attachToRecyclerView(recyclerView)
效果图:LinearLayoutManager + LinearSnapHelper 类似ViewPager效果 不过可以连续滑动
1.2 PagerSnapHelper
作用:让RecyclerView像ViewPager一样,一次只能滑一页,而且Item居中显示
用法:
PagerSnapHelper().attachToRecyclerView(recyclerView)
效果图:LinearLayoutManager + PagerSnapHelper 一次只能滑动一页,ViewPager相铜效果
2 SnapHelper解析
简书上的一篇文章写的很好,我在写也就是复制粘贴了。
文章地址:让你明明白白的使用RecyclerView——SnapHelper详解
需要强调的内容:在SnapHelper源码createSnapScroller方法中可以看到,RecyclerView的LayoutManager是需要实现 ScrollVectorProvider接口的。官方推出的LayoutManager也都实现了该接口。
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
...
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
3 无限循环滑动Banner
3.1 HorizontalLayoutManager
在前文 Android控件RecyclerView(二)——LayoutManager及其自定义 中,自定义了一个可以横向无限滑动的HorizontalLayoutManager,现在让该类实现 ScrollVectorProvider接口,重写 computeScrollVectorForPosition方法。
写法可以完全参考LinearLayoutManager源码中的computeScrollVectorForPosition方法,贴出HorizontalLayoutManager代码
import android.graphics.PointF
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
class HorizontalLayoutManager : RecyclerView.LayoutManager(),
RecyclerView.SmoothScroller.ScrollVectorProvider {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
if (childCount == 0) {
return null
}
val firstChildPos = getPosition(getChildAt(0)!!)
val direction = if (targetPosition < firstChildPos) -1 else 1
return PointF(direction.toFloat(), 0f)
}
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
//分离并且回收当前附加的所有View
detachAndScrapAttachedViews(recycler)
if (itemCount == 0) {
return
}
//横向绘制子View,则需要知道 X轴的偏移量
var offsetX = 0
//绘制并添加view
for (i in 0 until itemCount) {
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(view)
val viewHeight = getDecoratedMeasuredHeight(view)
layoutDecorated(view, offsetX, 0, offsetX + viewWidth, viewHeight)
offsetX += viewWidth
if (offsetX > width) {
break
}
}
}
//是否可横向滑动
override fun canScrollHorizontally(): Boolean {
return true
}
override fun scrollHorizontallyBy(
dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State
): Int {
recycleViews(dx, recycler)
fill(dx, recycler)
offsetChildrenHorizontal(dx * -1)
return dx
}
private fun fill(dx: Int, recycler: RecyclerView.Recycler) {
//左滑
if (dx > 0) {
while (true) {
//得到当前已添加(可见)的最后一个子View
val lastVisibleView = getChildAt(childCount - 1) ?: break
//如果滑动过后,View还是未完全显示出来就 不进行绘制下一个View
if (lastVisibleView.right - dx > width)
break
//得到View对应的位置
val layoutPosition = getPosition(lastVisibleView)
/**
* 例如要显示20个View,当前可见的最后一个View就是第20个,那么下一个要显示的就是第一个
* 如果当前显示的View不是第20个,那么就显示下一个,如当前显示的是第15个View,那么下一个显示第16个
* 注意区分 childCount 与 itemCount
*/
val nextView: View = if (layoutPosition == itemCount - 1) {
recycler.getViewForPosition(0)
} else {
recycler.getViewForPosition(layoutPosition + 1)
}
addView(nextView)
measureChildWithMargins(nextView, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = lastVisibleView.right
layoutDecorated(nextView, offsetX, 0, offsetX + viewWidth, viewHeight)
}
} else { //右滑
while (true) {
val firstVisibleView = getChildAt(0) ?: break
if (firstVisibleView.left - dx < 0) break
val layoutPosition = getPosition(firstVisibleView)
/**
* 如果当前第一个可见View为第0个,则左侧显示第20个View 如果不是,下一个就显示前一个
*/
val nextView = if (layoutPosition == 0) {
recycler.getViewForPosition(itemCount - 1)
} else {
recycler.getViewForPosition(layoutPosition - 1)
}
addView(nextView, 0)
measureChildWithMargins(nextView, 0, 0)
val viewWidth = getDecoratedMeasuredWidth(nextView)
val viewHeight = getDecoratedMeasuredHeight(nextView)
val offsetX = firstVisibleView.left
layoutDecorated(nextView, offsetX - viewWidth, 0, offsetX, viewHeight)
}
}
}
private fun recycleViews(dx: Int, recycler: RecyclerView.Recycler) {
for (i in 0 until itemCount) {
val childView = getChildAt(i) ?: return
//左滑
if (dx > 0) {
//移除并回收 原点 左侧的子View
if (childView.right - dx < 0) {
removeAndRecycleViewAt(i, recycler)
}
} else { //右滑
//移除并回收 右侧即RecyclerView宽度之以外的子View
if (childView.left - dx > width) {
removeAndRecycleViewAt(i, recycler)
}
}
}
}
}
3.2 搭配PagerSnapHelper
val imageAdapter = ImageAdapter().apply {
items = arrayListOf(
R.mipmap.image_page_1,
R.mipmap.image_page_2,
R.mipmap.image_page_3,
R.mipmap.image_page_4
)
}
PagerSnapHelper().attachToRecyclerView(recyclerView)
recyclerView.apply {
layoutManager = HorizontalLayoutManager()
adapter = imageAdapter
}
如上代码,使用自定义的HorizontalLayoutManager + PagerSnapHelper 实现效果如下
可以发现前面几张图滑动都没问题,滑倒最后一张时再次滑倒,还是会回到最后一张,按照无限循环的逻辑,应该是滑倒第一张,而前文中的效果也证明了HorizontalLayoutManager横向滑动是没问题的,可以猜测PagerSnapHelper的判断逻辑是滑倒最后一张时,就应该滑不动了,目标位置还是最后一个,此时我们就需要自定义一下SnapHelper了。
3.3 自定义ViewPagerSnapHelper
针对3.2 中的猜想,我们只需要更改 SnapHelper 获取目标位置的逻辑即可,即修改 findTargetSnapPosition 方法,targetSnapPosition是惯性滑动时RecyclerView应该停留的目标位置。
新建ViewPagerSnapHelper继承于 PagerSnapHelper,重写其 findTargetSnapPosition方法,打印一下获得的位置信息。
class ViewPagerSnapHelper : PagerSnapHelper() {
override fun findTargetSnapPosition(
layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int
): Int {
val position = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
Log.i("TAG","------------position:$position")
return position
}
}
日志
看日志信息可以发现,滑倒最后时,findTargetSnapPosition得到的目标位置一直是 4,而理论上4应该替换为0,实现无限循环滑动,所以修改逻辑即可。
class ViewPagerSnapHelper : PagerSnapHelper() {
override fun findTargetSnapPosition(
layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int
): Int {
val position = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
return if (position >= layoutManager.itemCount) {
0
} else {
super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
}
}
}
3.3 实现无限循环滚动Banner
将 PagerSnapHelper 替换为 ViewPagerSnapHelper 再次查看效果。
3.4 源码
ViewPagerSnapHelper 与 HorizontalLayoutManager文中有。
Adapter
class ImageAdapter : RecyclerView.Adapter<ImageAdapter.ViewHolder>() {
var items: List<Int> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recycle_item_image, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.showImage(items[position])
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun showImage(@DrawableRes res: Int) {
itemView.imageView.setImageResource(res)
}
}
}
recycle_item_image.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="3:2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
Activity
class SnapHelperActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycle_view)
val imageAdapter = ImageAdapter().apply {
items = arrayListOf(
R.mipmap.image_page_1,
R.mipmap.image_page_2,
R.mipmap.image_page_3,
R.mipmap.image_page_4
)
}
ViewPagerSnapHelper().attachToRecyclerView(recyclerView)
recyclerView.apply {
layoutManager = HorizontalLayoutManager()
adapter = imageAdapter
}
}
}