前言
最近需求里面要实现一个卡片堆叠的视图,通过卡片上下移动来切换阅读的条目,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 in state.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
- }
- }
- }
复制代码 这样刷新一下界面,子视图分列就会变成这样
这样就算是一个基础的视图了,但目前来说还不能进行滑动,所以接下来我们要实现滑动子视图的滑动
实现滑动
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 downTo 0){
- /**
- * 以当前堆叠卡片的最上方视图为基准
- * 从这里往上的视图需要跟随滑动事件向上进行滑动处理
- * 大于当前视图的会是堆叠视图,就跟随最上方视图滑动的百分比,同步向上传递
- */
- 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)
- }
- }
复制代码 这样就可以或许实现滑动效果
以上就能实现基础的堆叠视图效果和滑动变乱的处理,但这样并不能达成需求
所以添上一些优化,包括绘制数量、卡片的透明度缩放值等都跟随堆叠顶部的卡片位置变化
可以修改成以下代码
- 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)
- }
- }
复制代码 滑动效果现在就会修改成这样
这样离要求就很接近了,但还有不足,这里还需要让滑动之后的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] = 0
- out[1] = (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) return pos
- if (velocityY <= -600) return pos -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())
- }
- }
- ······
- }
复制代码 那么终极效果就如下图
这样就完全实现了需求
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |