Android控件RecyclerView(四)——SnapHelper搭配自定义LayoutManager实现无限循环滑动Banner

目录

前言

1 SnapHelper基本使用

1.1 LinearSnapHelper

1.2 PagerSnapHelper

2 SnapHelper解析

3 无限循环滑动Banner

3.1 HorizontalLayoutManager

3.2 搭配PagerSnapHelper

3.3 自定义ViewPagerSnapHelper

3.3 实现无限循环滚动Banner

3.4 源码


前言

文章属于学习总结 ,如有错漏之处,敬请指正。

同系列文章:

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

 

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
这是一个相对复杂的问题,需要一定的Android开发经验和知识。我会简单地讲解一下实现的大致思路和步骤。 1. 自定义LayoutManager 首先需要自定义一个LayoutManager,它会负责RecyclerView中每个item的布局排列和滚动。在这个LayoutManager中,我们需要重载一些方法,例如`onLayoutChildren()`,`scrollHorizontallyBy()`,`generateDefaultLayoutParams()`等。这些方法的具体实现会根据我们的需求而不同。其中,`onLayoutChildren()`方法负责测量和布局RecyclerView中的每一个ItemView,`scrollHorizontallyBy()`方法负责水平滚动RecyclerView,`generateDefaultLayoutParams()`方法负责生成默认的LayoutParams。 2. 实现弧形效果 要实现弧形效果,我们需要使用贝塞尔曲线。具体来说,我们可以通过计算贝塞尔曲线上的点来实现RecyclerView中弧形排列ItemView。这个过程可以在`onLayoutChildren()`方法中实现。我们可以通过计算每个ItemView的位置和角度来获得它的坐标,然后将这些坐标传递给View进行布局。 3. 实现滑动放大效果 要实现滑动放大效果,我们可以在`scrollHorizontallyBy()`方法中监听RecyclerView滑动距离,然后根据距离计算每个ItemView的缩放比例。具体来说,我们可以在滑动过程中计算每个ItemView的中心点和RecyclerView的中心点之间的距离,然后根据距离计算缩放比例。我们可以使用`setScaleX()`和`setScaleY()`方法来设置ItemView的缩放比例。 以上是实现弧形以及滑动放大效果RecyclerView的大致思路和步骤。具体的实现过程中还需要考虑很多细节问题,例如ItemView的位置和角度计算、动画效果的处理等等。如果您需要更详细的信息,请参考相关的开发文档或者查找相关的示例代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值