Android RecyclerView实现卡片堆叠视图

打印 上一主题 下一主题

主题 866|帖子 866|积分 2598

前言

最近需求里面要实现一个卡片堆叠的视图,通过卡片上下移动来切换阅读的条目,github和csdn找了不少都不能完全符合需求,所以末了还是看着别人代码一步步学着怎么实现的
需要完全实现重要是需要自界说以下几个类:
LayoutManager 用于实现堆叠视图以及视图的滑动
SnapHelper 用于帮助视图定位,可以让视图可以或许向前或者向后停顿在刚好一页的位置
实现RecyclerView的卡片视图

我这里直接通过CardView来实现
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
  3.     android:layout_width="360dp"
  4.     android:layout_height="500dp"
  5.     app:cardCornerRadius="20dp"
  6.     app:cardElevation="20dp"
  7.     xmlns:app="http://schemas.android.com/apk/res-auto">
  8.     <ImageView
  9.         android:layout_width="match_parent"
  10.         android:layout_height="match_parent"
  11.         android:id="@+id/isa_iv"
  12.         android:scaleType="fitStart"
  13.         android:src="@drawable/xm9"
  14.         />
  15. </androidx.cardview.widget.CardView>
复制代码
然后写个简单的Adapter
  1. class StackCardAdapter: RecyclerView.Adapter<StackCardViewHolder>() {
  2.     public var data = arrayListOf(
  3.         R.drawable.xm1,
  4.         R.drawable.xm2,
  5.         R.drawable.xm3,
  6.         R.drawable.xm4,
  7.         R.drawable.xm5,
  8.         R.drawable.xm6,
  9.         R.drawable.xm7,
  10.         R.drawable.xm8,
  11.         R.drawable.xm9,
  12.     )
  13.     override fun onCreateViewHolder(viewgroup: ViewGroup, type: Int): StackCardViewHolder {
  14.         return StackCardViewHolder( LayoutInflater.from(viewgroup.context)
  15.             .inflate(R.layout.item_sc_ad, viewgroup, false))
  16.     }
  17.     override fun getItemCount(): Int {
  18.         return data.size
  19.     }
  20.     override fun onBindViewHolder(viewHolder: StackCardViewHolder, position: Int) {
  21.         viewHolder.itemView.findViewById<ImageView>(R.id.isa_iv).setImageResource(data.get(position))
  22.     }
  23. }
  24. class StackCardViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
  25. }
复制代码
但目前用的还是基本的LinearLayoutManager,这样分列就会是线性布局,那么就需要我们自界说LayoutManager来实现差别的分列方式
LayoutManager会在每次视图发生变化的时间调用onLayoutChildren,所以我们在这里通过measureChild和measureChildWithMargins来确认子view的高宽的信息,前者不会丈量分割线长度,后者是带上分割线的丈量
实当代码如下
  1. class StackCardLayoutTestManager: RecyclerView.LayoutManager(){
  2.     private val TAG = "StackCardLayoutTestManager"
  3.     //子视图的高宽
  4.     private var mChildHeight = 0
  5.     private var mChildWidth = 0
  6.     //基准中心坐标
  7.     private var mBaseCenterX = 0
  8.     private var mBaseCenterY = 0
  9.     //每个视图的偏移量
  10.     private var mBaseOffSetY = 50
  11.    
  12.     //必须要实现此方法
  13.     override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
  14.         return RecyclerView.LayoutParams(
  15.             RecyclerView.LayoutParams.WRAP_CONTENT,
  16.             RecyclerView.LayoutParams.WRAP_CONTENT
  17.         )
  18.     }
  19.     override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
  20.         initialize(recycler)
  21.         drawChildren(recycler, state)
  22.     }
  23.     private fun initialize(recycler: Recycler){
  24.         //移除所绘制的所有view
  25.         detachAndScrapAttachedViews(recycler)
  26.         //这里视图里面默认每个子视图都是相同的高宽大小
  27.         var itemView = recycler.getViewForPosition(0)
  28.         addView(itemView)
  29.         measureChildWithMargins(itemView, 0, 0)
  30.         mChildHeight = getDecoratedMeasuredHeight(itemView)
  31.         mChildWidth = getDecoratedMeasuredWidth(itemView)
  32.         mBaseCenterX = width/2
  33.         mBaseCenterY = height/2
  34.         detachAndScrapAttachedViews(recycler)
  35.     }
  36.     private fun drawChildren(recycler: Recycler, state: State){
  37.         detachAndScrapAttachedViews(recycler)
  38.         /**
  39.          * 这里后绘制的视图会重叠在先绘制的视图之上
  40.          * 所以这里采用倒序,先绘制后面的视图,再绘制之前的
  41.          */
  42.         for (i in  state.itemCount -1 downTo 0){
  43.             var itemView = recycler.getViewForPosition(i)
  44.             addView(itemView)
  45.             measureChildWithMargins(itemView, 0, 0)
  46.             layoutDecoratedWithMargins(itemView,
  47.                 mBaseCenterX - mChildWidth/2,
  48.                 mBaseCenterY - mChildHeight/2 + mBaseOffSetY * i,
  49.                 mBaseCenterX + mChildWidth/2,
  50.                 mBaseCenterY + mChildHeight/2 + mBaseOffSetY * i)
  51.             itemView.alpha = 1f - 0.05f * i
  52.             itemView.scaleX = 1f - 0.05f * i
  53.             itemView.scaleY = 1f - 0.05f * i
  54.         }
  55.     }
  56. }
复制代码
这样刷新一下界面,子视图分列就会变成这样

这样就算是一个基础的视图了,但目前来说还不能进行滑动,所以接下来我们要实现滑动子视图的滑动
实现滑动

LayoutManager会通过canScrollHorizontallx和canScrollHorizontally方法来判断是否可以或许进行横向或者纵向滑动,然后分别在scrollVerticallyBy和scrollHorizontallyBy来分别处理纵向和横向的滑动变乱,我们可以根据传入的dx和dy参数来更改绘制子视图的位置信息,从而达到滑动的效果,因此代码修改如下:
  1. class StackCardLayoutTestManager: RecyclerView.LayoutManager(){
  2.     private val TAG = "StackCardLayoutTestManager"
  3.     //目前就只考虑纵向滑动了
  4.     private val mScrollDirection = LinearLayout.VERTICAL
  5.     //子视图的高宽
  6.     private var mChildHeight = 0
  7.     private var mChildWidth = 0
  8.     //基准中心坐标
  9.     private var mBaseCenterX = 0
  10.     private var mBaseCenterY = 0
  11.     //每个视图的偏移量
  12.     private var mBaseOffSetY = 50
  13.     //滑动的总距离
  14.     private var mTotalScrollY = 0
  15.     //当前的卡片位置
  16.     private var mCurrentPosition = 0
  17.     //当前卡片滑动的百分比
  18.     private var mCurrentRatio = 0f
  19.     //基础的透明度
  20.     private val mBaseAlpha = 1.0f
  21.     //基础的缩放值
  22.     private val mBaseScale = 1.0f
  23.     //每张堆叠卡片的透明度变化
  24.     private val mBaseAlphaChange = 0.05f
  25.     //每张堆叠卡片的缩放变化
  26.     private val mBaseScaleChange = 0.05f
  27.     //每张卡片绘制时的Y轴位移
  28.     private val mBaseOffSetYChange = 60
  29.    
  30.     //必须要实现此方法
  31.     override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
  32.         return RecyclerView.LayoutParams(
  33.             RecyclerView.LayoutParams.WRAP_CONTENT,
  34.             RecyclerView.LayoutParams.WRAP_CONTENT
  35.         )
  36.     }
  37.     override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
  38.         initialize(recycler)
  39.         drawChildren(recycler, state)
  40.     }
  41.     //是否能够水平滑动
  42.     override fun canScrollHorizontally(): Boolean {
  43.         return mScrollDirection == LinearLayoutManager.HORIZONTAL
  44.     }
  45.     //是否能够竖向滑动
  46.     override fun canScrollVertically(): Boolean {
  47.         return mScrollDirection == LinearLayoutManager.VERTICAL
  48.     }
  49.     private fun initialize(recycler: Recycler){
  50.         //移除所绘制的所有view
  51.         detachAndScrapAttachedViews(recycler)
  52.         //这里视图里面默认每个子视图都是相同的高宽大小
  53.         var itemView = recycler.getViewForPosition(0)
  54.         addView(itemView)
  55.         measureChildWithMargins(itemView, 0, 0)
  56.         mChildHeight = getDecoratedMeasuredHeight(itemView)
  57.         mChildWidth = getDecoratedMeasuredWidth(itemView)
  58.         mBaseCenterX = width/2
  59.         mBaseCenterY = height/2
  60.         detachAndScrapAttachedViews(recycler)
  61.     }
  62.     private fun drawChildren(recycler: Recycler, state: State , dy: Int = 0): Int{
  63.         detachAndScrapAttachedViews(recycler)
  64.         //向上滑动,滑动距离为负数
  65.         mTotalScrollY += dy * -1
  66.         //第一张图禁止向下滑动
  67.         if (mTotalScrollY >= 0) mTotalScrollY = 0
  68.         //最后一张图禁止向上滑动
  69.         if (mTotalScrollY <= -(state.itemCount-1) * mChildHeight) mTotalScrollY = -(state.itemCount-1) * mChildHeight
  70.         mCurrentPosition = Math.abs(mTotalScrollY / mChildHeight)
  71.         //偏移量
  72.         var offSetY = 0
  73.         //透明度
  74.         var alpha = 1.0f
  75.         //缩放大小
  76.         var scale = 1.0f
  77.         //百分比,当前卡片剩余进行长度占总长度的比例
  78.         mCurrentRatio = 1- Math.abs((mTotalScrollY + mCurrentPosition* mChildHeight).toFloat() / mChildHeight.toFloat())
  79.         /**
  80.          * 这里后绘制的视图会重叠在先绘制的视图之上
  81.          * 所以这里采用倒序,先绘制后面的视图,再绘制之前的
  82.          * 关于回收问题,直接仅绘制所在位置以及之后的四张即可
  83.          */
  84.         for (i in state.itemCount -1 downTo  0){
  85.             /**
  86.              * 以当前堆叠卡片的最上方视图为基准
  87.              * 从这里往上的视图需要跟随滑动事件向上进行滑动处理
  88.              * 大于当前视图的会是堆叠视图,就跟随最上方视图滑动的百分比,同步向上传递
  89.              */
  90.             if (i <= mCurrentPosition){
  91.                 offSetY = mTotalScrollY - -1 *mChildHeight * i
  92.             }else{
  93.                 offSetY = (mBaseOffSetYChange * (i-1) - mCurrentRatio * mBaseOffSetYChange * -1 + (mCurrentPosition) * mBaseOffSetYChange * -1).toInt()
  94.             }
  95.             alpha = mBaseAlpha - mBaseAlphaChange * i
  96.             scale = mBaseScale - mBaseScaleChange * i
  97.             var view = recycler.getViewForPosition(i)
  98.             measureChildWithMargins(view, 0, 0)
  99.             addView(view)
  100.             layoutDecoratedWithMargins(view, mBaseCenterX - mChildWidth/2, mBaseCenterY - mChildHeight/2 + offSetY, mBaseCenterX + mChildWidth/2, mBaseCenterY+ mChildHeight/2 + offSetY)
  101.             view.alpha = alpha
  102.             view.scaleX = scale
  103.             view.scaleY = scale
  104.         }
  105.         //位置滑动到最底部的时候不再进行滑动处理,返回值为0
  106.         return if (mTotalScrollY == 0 || mTotalScrollY == -(state.itemCount -1) * mChildHeight) 0 else dy
  107.     }
  108.     override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: State): Int {
  109.         return drawChildren(recycler, state, dy)
  110.     }
  111.     override fun scrollHorizontallyBy(dx: Int, recycler: Recycler, state: State): Int {
  112.         return super.scrollHorizontallyBy(dx, recycler, state)
  113.     }
  114. }
复制代码
这样就可以或许实现滑动效果

以上就能实现基础的堆叠视图效果和滑动变乱的处理,但这样并不能达成需求
所以添上一些优化,包括绘制数量、卡片的透明度缩放值等都跟随堆叠顶部的卡片位置变化
可以修改成以下代码
  1. class StackCardLayoutTestManager: RecyclerView.LayoutManager(){
  2.     private val TAG = "StackCardLayoutTestManager"
  3.     //目前就只考虑纵向滑动了
  4.     private val mScrollDirection = LinearLayout.VERTICAL
  5.     //子视图的高宽
  6.     private var mChildHeight = 0
  7.     private var mChildWidth = 0
  8.     //基准中心坐标
  9.     private var mBaseCenterX = 0
  10.     private var mBaseCenterY = 0
  11.     //每个视图的偏移量
  12.     private var mBaseOffSetY = 50
  13.     //滑动的总距离
  14.     private var mTotalScrollY = 0
  15.     //当前的卡片位置
  16.     private var mCurrentPosition = 0
  17.     //当前卡片滑动的百分比
  18.     private var mCurrentRatio = 0f
  19.     //基础的透明度
  20.     private val mBaseAlpha = 1.0f
  21.     //基础的缩放值
  22.     private val mBaseScale = 1.0f
  23.     //当卡片滑动出去时的缩放值
  24.     private val mOutCardScale = 0.8f
  25.     //当卡片滑动出去时的透明度
  26.     private val mOutCardAlpha = 0.0f
  27.     //每张堆叠卡片的透明度变化
  28.     private val mBaseAlphaChange = 0.3f
  29.     //每张堆叠卡片的缩放变化
  30.     private val mBaseScaleChange = 0.05f
  31.     //每张卡片绘制时的Y轴位移
  32.     private val mBaseOffSetYChange = 60
  33.    
  34.     //必须要实现此方法
  35.     override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
  36.         return RecyclerView.LayoutParams(
  37.             RecyclerView.LayoutParams.WRAP_CONTENT,
  38.             RecyclerView.LayoutParams.WRAP_CONTENT
  39.         )
  40.     }
  41.     override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
  42.         initialize(recycler)
  43.         drawChildren(recycler, state)
  44.     }
  45.     //是否能够水平滑动
  46.     override fun canScrollHorizontally(): Boolean {
  47.         return mScrollDirection == LinearLayoutManager.HORIZONTAL
  48.     }
  49.     //是否能够竖向滑动
  50.     override fun canScrollVertically(): Boolean {
  51.         return mScrollDirection == LinearLayoutManager.VERTICAL
  52.     }
  53.     private fun initialize(recycler: Recycler){
  54.         //移除所绘制的所有view
  55.         detachAndScrapAttachedViews(recycler)
  56.         //这里视图里面默认每个子视图都是相同的高宽大小
  57.         var itemView = recycler.getViewForPosition(0)
  58.         addView(itemView)
  59.         measureChildWithMargins(itemView, 0, 0)
  60.         mChildHeight = getDecoratedMeasuredHeight(itemView)
  61.         mChildWidth = getDecoratedMeasuredWidth(itemView)
  62.         mBaseCenterX = width/2
  63.         mBaseCenterY = height/2
  64.         detachAndScrapAttachedViews(recycler)
  65.     }
  66.     private fun drawChildren(recycler: Recycler, state: State , dy: Int = 0): Int{
  67.         detachAndScrapAttachedViews(recycler)
  68.         //向上滑动,滑动距离为负数
  69.         mTotalScrollY += dy * -1
  70.         //第一张图禁止向下滑动
  71.         if (mTotalScrollY >= 0) mTotalScrollY = 0
  72.         //最后一张图禁止向上滑动
  73.         if (mTotalScrollY <= -(state.itemCount-1) * mChildHeight) mTotalScrollY = -(state.itemCount-1) * mChildHeight
  74.         mCurrentPosition = Math.abs(mTotalScrollY / mChildHeight)
  75.         //偏移量
  76.         var offSetY = 0
  77.         //透明度
  78.         var alpha = 1.0f
  79.         //缩放大小
  80.         var scale = 1.0f
  81.         //百分比,当前卡片剩余进行长度占总长度的比例
  82.         mCurrentRatio = 1- Math.abs((mTotalScrollY + mCurrentPosition* mChildHeight).toFloat() / mChildHeight.toFloat())
  83.         /**
  84.          * 这里后绘制的视图会重叠在先绘制的视图之上
  85.          * 所以这里采用倒序,先绘制后面的视图,再绘制之前的
  86.          * 关于回收问题,直接仅绘制所在位置以及之后的四张即可
  87.          */
  88.         for (i in Math.min(mCurrentPosition + 4, state.itemCount -1) downTo mCurrentPosition){
  89.             if (i == mCurrentPosition){
  90.                 offSetY = mTotalScrollY - -1 *mChildHeight *i
  91.                 alpha = mBaseAlpha
  92.                 scale = mOutCardScale + (mBaseScale - mOutCardScale)* mCurrentRatio
  93.             } else if (i < mCurrentPosition){
  94.                 offSetY = mTotalScrollY - -1 *mChildHeight *i
  95.                 alpha = mOutCardAlpha
  96.                 scale = mOutCardScale
  97.             }else{
  98.                 alpha = mBaseAlpha - mBaseAlphaChange * (i-1) - mCurrentRatio* mBaseAlphaChange + mCurrentPosition* mBaseAlphaChange
  99.                 scale = mBaseScale- mBaseScaleChange * (i-1) - mCurrentRatio* mBaseScaleChange + (mCurrentPosition)* mBaseScaleChange
  100.                 offSetY = (mBaseOffSetYChange * (i-1) - mCurrentRatio * mBaseOffSetYChange * -1 + (mCurrentPosition) * mBaseOffSetYChange * -1).toInt()
  101.             }
  102.             var view = recycler.getViewForPosition(i)
  103.             measureChildWithMargins(view, 0, 0)
  104.             addView(view)
  105.             layoutDecoratedWithMargins(view, mBaseCenterX - mChildWidth/2, mBaseCenterY - mChildHeight/2 + offSetY, mBaseCenterX + mChildWidth/2, mBaseCenterY+ mChildHeight/2 + offSetY)
  106.             view.alpha = alpha
  107.             view.scaleX = scale
  108.             view.scaleY = scale
  109.         }
  110.         //位置滑动到最底部的时候不再进行滑动处理,返回值为0
  111.         return if (mTotalScrollY == 0 || mTotalScrollY == -(state.itemCount -1) * mChildHeight) 0 else dy
  112.     }
  113.     override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: State): Int {
  114.         return drawChildren(recycler, state, dy)
  115.     }
  116.     override fun scrollHorizontallyBy(dx: Int, recycler: Recycler, state: State): Int {
  117.         return super.scrollHorizontallyBy(dx, recycler, state)
  118.     }
  119. }
复制代码
滑动效果现在就会修改成这样

这样离要求就很接近了,但还有不足,这里还需要让滑动之后的RecyclerView自动对齐位置
所以接下来就需要处理自动对齐的题目
自动对齐

Android将手指脱离屏幕后视图自动滑动的变乱界说为fling变乱,也就是抛掷变乱实现自动对齐可以有两个方法,一是直接利用RecyclerView.SetOnFlingListener来处理抛掷变乱,二是通过SnapHelper来处理抛掷变乱,这两个方法实际上是同一变乱的处理,利用了SnapHelper就不能在设置抛掷监听,否则要么不收效要么会闪退
我这里继承SnapHelper来处理
继承之后要实现三个方法,分别是 findSnapView,calculateDistanceToFinalSnap, findTargetSnapPosition三个方法,第一个方法将会根据LayoutManager来确定目标视图在子视图组里的位置,然后需要通过 calculateDistanceToFinalSnap 来盘算当前位置和目标视图位置隔断,之后SnapHelper在滑动开始和竣事时分别调用一次 calculateDistanceToFinalSnap 如果不能得到隔断为0就会连续进行调用, 末了是findTargetSnapPosition 方法,这里传入的值意思是抛掷本身应该滑动到的位置,可以根据这个值来确定是否需要滑动到下一页,同时,利用SnapHelper会要求Layoutmanager必须继承RecyclerView.SmoothScroller.ScrollVectorProvider
以代码如下
  1. class StackCardSnapHelper_2: PagerSnapHelper() {
  2.     override fun calculateDistanceToFinalSnap(
  3.         layoutManager: RecyclerView.LayoutManager,
  4.         targetView: View
  5.     ): IntArray? {
  6.         var out = intArrayOf(0, 0)
  7.         if (layoutManager.canScrollVertically()){
  8.             out[0] = 0
  9.             out[1] = (layoutManager as StackCardLayoutTestManager)
  10.                 .getDistanceToCenter(layoutManager.getPosition(targetView))
  11.             return out
  12.         }
  13.         return null
  14.     }
  15.     override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
  16.         if (layoutManager is StackCardLayoutTestManager){
  17.             var position = layoutManager.confirmTargetPosition()
  18.             if (position != RecyclerView.NO_POSITION){
  19.                 return layoutManager.findViewByPosition(position)
  20.             }
  21.         }
  22.         return null
  23.     }
  24.     override fun findTargetSnapPosition(
  25.         layoutManager: RecyclerView.LayoutManager?,
  26.         velocityX: Int,
  27.         velocityY: Int
  28.     ): Int {
  29.         var position = (layoutManager as StackCardLayoutTestManager).findTargetPosition(velocityY)
  30.         return position
  31.     }
  32. }
复制代码
  1. //在Layoutmanager中添加
  2. class StackCardLayoutTestManager:
  3.     RecyclerView.LayoutManager(),
  4.     RecyclerView.SmoothScroller.ScrollVectorProvider
  5. {
  6.         ······
  7.     fun getDistanceToCenter(targetPosition: Int): Int{
  8.         var distance = 0
  9.         distance = mTotalScrollY - mChildHeight * targetPosition * -1
  10.         return distance
  11.     }
  12.     fun findTargetPosition(velocityY: Int):Int{
  13.         val pos = Math.abs(Math.floor(mTotalScrollY.toDouble() / mChildHeight).toInt())
  14.         if (velocityY >=600) return  pos
  15.         if (velocityY <= -600) return  pos -1
  16.         return if (mCurrentRatio > 0.5f) {
  17.             pos - 1
  18.         } else {
  19.             pos
  20.         }
  21.     }
  22.     fun confirmTargetPosition(): Int{
  23.         var positionOffset = if(mCurrentRatio >= 0.5f) 0 else 1
  24.         return mCurrentPosition + positionOffset
  25.     }
  26.     override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
  27.         if (childCount == 0) {
  28.             return null
  29.         }
  30.         val firstChildPos = getPosition(getChildAt(0)!!)
  31.         val direction = if (targetPosition < firstChildPos != true) -1 else 1
  32.         return if (mScrollOrientation == LinearLayoutManager.HORIZONTAL) {
  33.             PointF(direction.toFloat(), 0f)
  34.         } else {
  35.             PointF(0f, direction.toFloat())
  36.         }
  37.     }
  38.         ······
  39. }
复制代码
那么终极效果就如下图

这样就完全实现了需求

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

我可以不吃啊

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表