我可以不吃啊 发表于 2024-10-12 03:16:31

Android RecyclerView实现卡片堆叠视图

前言

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

我这里直接通过CardView来实现
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="360dp"
    android:layout_height="500dp"
    app:cardCornerRadius="20dp"
    app:cardElevation="20dp"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:id="@+id/isa_iv"
      android:scaleType="fitStart"
      android:src="@drawable/xm9"
      />
</androidx.cardview.widget.CardView>
然后写个简单的Adapter
class StackCardAdapter: RecyclerView.Adapter<StackCardViewHolder>() {
    public var data = arrayListOf(
      R.drawable.xm1,
      R.drawable.xm2,
      R.drawable.xm3,
      R.drawable.xm4,
      R.drawable.xm5,
      R.drawable.xm6,
      R.drawable.xm7,
      R.drawable.xm8,
      R.drawable.xm9,
    )
    override fun onCreateViewHolder(viewgroup: ViewGroup, type: Int): StackCardViewHolder {
      return StackCardViewHolder( LayoutInflater.from(viewgroup.context)
            .inflate(R.layout.item_sc_ad, viewgroup, false))
    }

    override fun getItemCount(): Int {
      return data.size
    }

    override fun onBindViewHolder(viewHolder: StackCardViewHolder, position: Int) {
      viewHolder.itemView.findViewById<ImageView>(R.id.isa_iv).setImageResource(data.get(position))
    }
}
class StackCardViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
}
但目前用的还是基本的LinearLayoutManager,这样分列就会是线性布局,那么就需要我们自界说LayoutManager来实现差别的分列方式
LayoutManager会在每次视图发生变化的时间调用onLayoutChildren,所以我们在这里通过measureChild和measureChildWithMargins来确认子view的高宽的信息,前者不会丈量分割线长度,后者是带上分割线的丈量
实当代码如下
class StackCardLayoutTestManager: RecyclerView.LayoutManager(){
    private val TAG = "StackCardLayoutTestManager"
    //子视图的高宽
    private var mChildHeight = 0
    private var mChildWidth = 0
    //基准中心坐标
    private var mBaseCenterX = 0
    private var mBaseCenterY = 0
    //每个视图的偏移量
    private var mBaseOffSetY = 50
   
    //必须要实现此方法
    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
      return RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
      )
    }
    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
      initialize(recycler)
      drawChildren(recycler, state)
    }
    private fun initialize(recycler: Recycler){
      //移除所绘制的所有view
      detachAndScrapAttachedViews(recycler)
      //这里视图里面默认每个子视图都是相同的高宽大小
      var itemView = recycler.getViewForPosition(0)
      addView(itemView)
      measureChildWithMargins(itemView, 0, 0)
      mChildHeight = getDecoratedMeasuredHeight(itemView)
      mChildWidth = getDecoratedMeasuredWidth(itemView)
      mBaseCenterX = width/2
      mBaseCenterY = height/2
      detachAndScrapAttachedViews(recycler)
    }
    private fun drawChildren(recycler: Recycler, state: State){
      detachAndScrapAttachedViews(recycler)
      /**
         * 这里后绘制的视图会重叠在先绘制的视图之上
         * 所以这里采用倒序,先绘制后面的视图,再绘制之前的
         */
      for (i instate.itemCount -1 downTo 0){
            var itemView = recycler.getViewForPosition(i)
            addView(itemView)
            measureChildWithMargins(itemView, 0, 0)
            layoutDecoratedWithMargins(itemView,
                mBaseCenterX - mChildWidth/2,
                mBaseCenterY - mChildHeight/2 + mBaseOffSetY * i,
                mBaseCenterX + mChildWidth/2,
                mBaseCenterY + mChildHeight/2 + mBaseOffSetY * i)
            itemView.alpha = 1f - 0.05f * i
            itemView.scaleX = 1f - 0.05f * i
            itemView.scaleY = 1f - 0.05f * i
      }
    }
}
这样刷新一下界面,子视图分列就会变成这样https://i-blog.csdnimg.cn/blog_migrate/1d0973881679528c40645f6406dd1adb.jpeg
这样就算是一个基础的视图了,但目前来说还不能进行滑动,所以接下来我们要实现滑动子视图的滑动
实现滑动

LayoutManager会通过canScrollHorizontallx和canScrollHorizontally方法来判断是否可以或许进行横向或者纵向滑动,然后分别在scrollVerticallyBy和scrollHorizontallyBy来分别处理纵向和横向的滑动变乱,我们可以根据传入的dx和dy参数来更改绘制子视图的位置信息,从而达到滑动的效果,因此代码修改如下:
class StackCardLayoutTestManager: RecyclerView.LayoutManager(){
    private val TAG = "StackCardLayoutTestManager"
    //目前就只考虑纵向滑动了
    private val mScrollDirection = LinearLayout.VERTICAL
    //子视图的高宽
    private var mChildHeight = 0
    private var mChildWidth = 0
    //基准中心坐标
    private var mBaseCenterX = 0
    private var mBaseCenterY = 0
    //每个视图的偏移量
    private var mBaseOffSetY = 50
    //滑动的总距离
    private var mTotalScrollY = 0
    //当前的卡片位置
    private var mCurrentPosition = 0
    //当前卡片滑动的百分比
    private var mCurrentRatio = 0f
    //基础的透明度
    private val mBaseAlpha = 1.0f
    //基础的缩放值
    private val mBaseScale = 1.0f
    //每张堆叠卡片的透明度变化
    private val mBaseAlphaChange = 0.05f
    //每张堆叠卡片的缩放变化
    private val mBaseScaleChange = 0.05f
    //每张卡片绘制时的Y轴位移
    private val mBaseOffSetYChange = 60
   
    //必须要实现此方法
    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
      return RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
      )
    }
    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
      initialize(recycler)
      drawChildren(recycler, state)
    }
    //是否能够水平滑动
    override fun canScrollHorizontally(): Boolean {
      return mScrollDirection == LinearLayoutManager.HORIZONTAL
    }
    //是否能够竖向滑动
    override fun canScrollVertically(): Boolean {
      return mScrollDirection == LinearLayoutManager.VERTICAL
    }

    private fun initialize(recycler: Recycler){
      //移除所绘制的所有view
      detachAndScrapAttachedViews(recycler)
      //这里视图里面默认每个子视图都是相同的高宽大小
      var itemView = recycler.getViewForPosition(0)
      addView(itemView)
      measureChildWithMargins(itemView, 0, 0)
      mChildHeight = getDecoratedMeasuredHeight(itemView)
      mChildWidth = getDecoratedMeasuredWidth(itemView)
      mBaseCenterX = width/2
      mBaseCenterY = height/2
      detachAndScrapAttachedViews(recycler)
    }

    private fun drawChildren(recycler: Recycler, state: State , dy: Int = 0): Int{
      detachAndScrapAttachedViews(recycler)
      //向上滑动,滑动距离为负数
      mTotalScrollY += dy * -1
      //第一张图禁止向下滑动
      if (mTotalScrollY >= 0) mTotalScrollY = 0
      //最后一张图禁止向上滑动
      if (mTotalScrollY <= -(state.itemCount-1) * mChildHeight) mTotalScrollY = -(state.itemCount-1) * mChildHeight
      mCurrentPosition = Math.abs(mTotalScrollY / mChildHeight)
      //偏移量
      var offSetY = 0
      //透明度
      var alpha = 1.0f
      //缩放大小
      var scale = 1.0f
      //百分比,当前卡片剩余进行长度占总长度的比例
      mCurrentRatio = 1- Math.abs((mTotalScrollY + mCurrentPosition* mChildHeight).toFloat() / mChildHeight.toFloat())
      /**
         * 这里后绘制的视图会重叠在先绘制的视图之上
         * 所以这里采用倒序,先绘制后面的视图,再绘制之前的
         * 关于回收问题,直接仅绘制所在位置以及之后的四张即可
         */
      for (i in state.itemCount -1 downTo0){
            /**
             * 以当前堆叠卡片的最上方视图为基准
             * 从这里往上的视图需要跟随滑动事件向上进行滑动处理
             * 大于当前视图的会是堆叠视图,就跟随最上方视图滑动的百分比,同步向上传递
             */
            if (i <= mCurrentPosition){
                offSetY = mTotalScrollY - -1 *mChildHeight * i
            }else{
                offSetY = (mBaseOffSetYChange * (i-1) - mCurrentRatio * mBaseOffSetYChange * -1 + (mCurrentPosition) * mBaseOffSetYChange * -1).toInt()
            }
            alpha = mBaseAlpha - mBaseAlphaChange * i
            scale = mBaseScale - mBaseScaleChange * i
            var view = recycler.getViewForPosition(i)
            measureChildWithMargins(view, 0, 0)
            addView(view)
            layoutDecoratedWithMargins(view, mBaseCenterX - mChildWidth/2, mBaseCenterY - mChildHeight/2 + offSetY, mBaseCenterX + mChildWidth/2, mBaseCenterY+ mChildHeight/2 + offSetY)

            view.alpha = alpha
            view.scaleX = scale
            view.scaleY = scale
      }
      //位置滑动到最底部的时候不再进行滑动处理,返回值为0
      return if (mTotalScrollY == 0 || mTotalScrollY == -(state.itemCount -1) * mChildHeight) 0 else dy
    }

    override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: State): Int {
      return drawChildren(recycler, state, dy)
    }

    override fun scrollHorizontallyBy(dx: Int, recycler: Recycler, state: State): Int {
      return super.scrollHorizontallyBy(dx, recycler, state)
    }
}
这样就可以或许实现滑动效果
https://i-blog.csdnimg.cn/blog_migrate/4cdf537afd177c89dcc69d5278a70436.gif
以上就能实现基础的堆叠视图效果和滑动变乱的处理,但这样并不能达成需求
所以添上一些优化,包括绘制数量、卡片的透明度缩放值等都跟随堆叠顶部的卡片位置变化
可以修改成以下代码

class StackCardLayoutTestManager: RecyclerView.LayoutManager(){
    private val TAG = "StackCardLayoutTestManager"
    //目前就只考虑纵向滑动了
    private val mScrollDirection = LinearLayout.VERTICAL
    //子视图的高宽
    private var mChildHeight = 0
    private var mChildWidth = 0
    //基准中心坐标
    private var mBaseCenterX = 0
    private var mBaseCenterY = 0
    //每个视图的偏移量
    private var mBaseOffSetY = 50
    //滑动的总距离
    private var mTotalScrollY = 0
    //当前的卡片位置
    private var mCurrentPosition = 0
    //当前卡片滑动的百分比
    private var mCurrentRatio = 0f
    //基础的透明度
    private val mBaseAlpha = 1.0f
    //基础的缩放值
    private val mBaseScale = 1.0f
    //当卡片滑动出去时的缩放值
    private val mOutCardScale = 0.8f
    //当卡片滑动出去时的透明度
    private val mOutCardAlpha = 0.0f
    //每张堆叠卡片的透明度变化
    private val mBaseAlphaChange = 0.3f
    //每张堆叠卡片的缩放变化
    private val mBaseScaleChange = 0.05f
    //每张卡片绘制时的Y轴位移
    private val mBaseOffSetYChange = 60
   
    //必须要实现此方法
    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
      return RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
      )
    }
    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
      initialize(recycler)
      drawChildren(recycler, state)
    }
    //是否能够水平滑动
    override fun canScrollHorizontally(): Boolean {
      return mScrollDirection == LinearLayoutManager.HORIZONTAL
    }
    //是否能够竖向滑动
    override fun canScrollVertically(): Boolean {
      return mScrollDirection == LinearLayoutManager.VERTICAL
    }

    private fun initialize(recycler: Recycler){
      //移除所绘制的所有view
      detachAndScrapAttachedViews(recycler)
      //这里视图里面默认每个子视图都是相同的高宽大小
      var itemView = recycler.getViewForPosition(0)
      addView(itemView)
      measureChildWithMargins(itemView, 0, 0)
      mChildHeight = getDecoratedMeasuredHeight(itemView)
      mChildWidth = getDecoratedMeasuredWidth(itemView)
      mBaseCenterX = width/2
      mBaseCenterY = height/2
      detachAndScrapAttachedViews(recycler)
    }

    private fun drawChildren(recycler: Recycler, state: State , dy: Int = 0): Int{
      detachAndScrapAttachedViews(recycler)
      //向上滑动,滑动距离为负数
      mTotalScrollY += dy * -1
      //第一张图禁止向下滑动
      if (mTotalScrollY >= 0) mTotalScrollY = 0
      //最后一张图禁止向上滑动
      if (mTotalScrollY <= -(state.itemCount-1) * mChildHeight) mTotalScrollY = -(state.itemCount-1) * mChildHeight
      mCurrentPosition = Math.abs(mTotalScrollY / mChildHeight)
      //偏移量
      var offSetY = 0
      //透明度
      var alpha = 1.0f
      //缩放大小
      var scale = 1.0f
      //百分比,当前卡片剩余进行长度占总长度的比例
      mCurrentRatio = 1- Math.abs((mTotalScrollY + mCurrentPosition* mChildHeight).toFloat() / mChildHeight.toFloat())
      /**
         * 这里后绘制的视图会重叠在先绘制的视图之上
         * 所以这里采用倒序,先绘制后面的视图,再绘制之前的
         * 关于回收问题,直接仅绘制所在位置以及之后的四张即可
         */
      for (i in Math.min(mCurrentPosition + 4, state.itemCount -1) downTo mCurrentPosition){
            if (i == mCurrentPosition){
                offSetY = mTotalScrollY - -1 *mChildHeight *i
                alpha = mBaseAlpha
                scale = mOutCardScale + (mBaseScale - mOutCardScale)* mCurrentRatio
            } else if (i < mCurrentPosition){
                offSetY = mTotalScrollY - -1 *mChildHeight *i
                alpha = mOutCardAlpha
                scale = mOutCardScale
            }else{
                alpha = mBaseAlpha - mBaseAlphaChange * (i-1) - mCurrentRatio* mBaseAlphaChange + mCurrentPosition* mBaseAlphaChange
                scale = mBaseScale- mBaseScaleChange * (i-1) - mCurrentRatio* mBaseScaleChange + (mCurrentPosition)* mBaseScaleChange
                offSetY = (mBaseOffSetYChange * (i-1) - mCurrentRatio * mBaseOffSetYChange * -1 + (mCurrentPosition) * mBaseOffSetYChange * -1).toInt()
            }
            var view = recycler.getViewForPosition(i)
            measureChildWithMargins(view, 0, 0)
            addView(view)
            layoutDecoratedWithMargins(view, mBaseCenterX - mChildWidth/2, mBaseCenterY - mChildHeight/2 + offSetY, mBaseCenterX + mChildWidth/2, mBaseCenterY+ mChildHeight/2 + offSetY)

            view.alpha = alpha
            view.scaleX = scale
            view.scaleY = scale
      }
      //位置滑动到最底部的时候不再进行滑动处理,返回值为0
      return if (mTotalScrollY == 0 || mTotalScrollY == -(state.itemCount -1) * mChildHeight) 0 else dy
    }

    override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: State): Int {
      return drawChildren(recycler, state, dy)
    }

    override fun scrollHorizontallyBy(dx: Int, recycler: Recycler, state: State): Int {
      return super.scrollHorizontallyBy(dx, recycler, state)
    }
}

滑动效果现在就会修改成这样
https://i-blog.csdnimg.cn/blog_migrate/eca7b4c9f3ebd46927fd410a9128fc43.gif
这样离要求就很接近了,但还有不足,这里还需要让滑动之后的RecyclerView自动对齐位置
所以接下来就需要处理自动对齐的题目
自动对齐

Android将手指脱离屏幕后视图自动滑动的变乱界说为fling变乱,也就是抛掷变乱实现自动对齐可以有两个方法,一是直接利用RecyclerView.SetOnFlingListener来处理抛掷变乱,二是通过SnapHelper来处理抛掷变乱,这两个方法实际上是同一变乱的处理,利用了SnapHelper就不能在设置抛掷监听,否则要么不收效要么会闪退
我这里继承SnapHelper来处理
继承之后要实现三个方法,分别是 findSnapView,calculateDistanceToFinalSnap, findTargetSnapPosition三个方法,第一个方法将会根据LayoutManager来确定目标视图在子视图组里的位置,然后需要通过 calculateDistanceToFinalSnap 来盘算当前位置和目标视图位置隔断,之后SnapHelper在滑动开始和竣事时分别调用一次 calculateDistanceToFinalSnap 如果不能得到隔断为0就会连续进行调用, 末了是findTargetSnapPosition 方法,这里传入的值意思是抛掷本身应该滑动到的位置,可以根据这个值来确定是否需要滑动到下一页,同时,利用SnapHelper会要求Layoutmanager必须继承RecyclerView.SmoothScroller.ScrollVectorProvider
以代码如下
class StackCardSnapHelper_2: PagerSnapHelper() {
    override fun calculateDistanceToFinalSnap(
      layoutManager: RecyclerView.LayoutManager,
      targetView: View
    ): IntArray? {
      var out = intArrayOf(0, 0)
      if (layoutManager.canScrollVertically()){
            out = 0
            out = (layoutManager as StackCardLayoutTestManager)
                .getDistanceToCenter(layoutManager.getPosition(targetView))
            return out
      }
      return null
    }
    override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
      if (layoutManager is StackCardLayoutTestManager){
            var position = layoutManager.confirmTargetPosition()
            if (position != RecyclerView.NO_POSITION){
                return layoutManager.findViewByPosition(position)
            }
      }
      return null
    }
    override fun findTargetSnapPosition(
      layoutManager: RecyclerView.LayoutManager?,
      velocityX: Int,
      velocityY: Int
    ): Int {
      var position = (layoutManager as StackCardLayoutTestManager).findTargetPosition(velocityY)
      return position
    }
}
//在Layoutmanager中添加
class StackCardLayoutTestManager:
    RecyclerView.LayoutManager(),
    RecyclerView.SmoothScroller.ScrollVectorProvider
{
        ······

    fun getDistanceToCenter(targetPosition: Int): Int{
      var distance = 0
      distance = mTotalScrollY - mChildHeight * targetPosition * -1
      return distance
    }
    fun findTargetPosition(velocityY: Int):Int{
      val pos = Math.abs(Math.floor(mTotalScrollY.toDouble() / mChildHeight).toInt())
      if (velocityY >=600) returnpos
      if (velocityY <= -600) returnpos -1
      return if (mCurrentRatio > 0.5f) {
            pos - 1
      } else {
            pos
      }
    }
    fun confirmTargetPosition(): Int{
      var positionOffset = if(mCurrentRatio >= 0.5f) 0 else 1
      return mCurrentPosition + positionOffset
    }
    override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
      if (childCount == 0) {
            return null
      }
      val firstChildPos = getPosition(getChildAt(0)!!)
      val direction = if (targetPosition < firstChildPos != true) -1 else 1
      return if (mScrollOrientation == LinearLayoutManager.HORIZONTAL) {
            PointF(direction.toFloat(), 0f)
      } else {
            PointF(0f, direction.toFloat())
      }
    }

        ······
}
那么终极效果就如下图
https://i-blog.csdnimg.cn/blog_migrate/a9a0a9298a028c84e58eac71e1c72033.gif
这样就完全实现了需求

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Android RecyclerView实现卡片堆叠视图