ViewPager2另辟蹊径实现图片遮罩切割页面切换效果

之前遇到一个需求,设计师希望的图片轮播切换效果如下图所示。

(图片有点大,加载稍微等一小会)

最开始我的想法是采用ViewPager2的PageTransformer来实现,但是稍微实践了一下便发现,单独只用PageTransformer来实现不太行,因为PageTransformer本质上类似是对view的属性动画操作,如tranalationX,Y,Z以及等等其他属性。

不过,先一步一步来实现吧。

堆叠效果

首先,PageTransformer可以实现堆叠滑动效果的,实现逻辑也很简单,本质上就是抵消滑动的位移,其原理的示意图如下:

image.png

具体实现代码如下:

具体page和position参数代表的含义可以参考androidx.viewpager2.widget.ViewPager2.PageTransformer#transformPage源码注释,这里便不再展开。

viewpager.setPageTransformer { page, position ->

    if (position < 0f) {

        page.translationX = 0f

        page.translationZ = 0f
    } else {
        page.translationX = page.width * -position
        page.translationZ = -position
    }
}

利用translationX来抵消后一个Item的滑动,同时利用translationZ来控制显示的层级来保证堆叠效果。
这里可以再拓展一下另外一种控制view层级的方式,ViewGroup#getChildDrawingOrder方法,简单来说就是控制子View的绘制顺序。
在RecyclerView中提供了,setChildDrawingOrderCallback,可以直接自定义绘制顺序。

recyclerView.setChildDrawingOrderCallback { childCount, i -> }

所以上面的translationZ可以去掉,改成以下形式:

viewpager.setPageTransformer { page, position ->

    if (position < 0f) {

        page.translationX = 0f

    } else {
        page.translationX = page.width * -position
    }
}
// 错开绘制顺序,让当前的层级在后一个之上
viewpager.recyclerView.setChildDrawingOrderCallback { childCount, i -> childCount - i - 1 }

viewpager.recyclerView是用了kotlin的扩展属性,补充说明一下。

private val ViewPager2.recyclerView: RecyclerView
    get() = this[0] as RecyclerView

好,完成ViewPager2的堆叠滑动只是第一步,下一步是图片的遮罩切割的效果。

遮罩切割的效果

这里我尝试了很多方式,最终放弃仅靠PageTransformer来实现的思路,其实这个效果有点类似于阅读里的卷曲翻页效果(仿真效果)。

其实现原理是依靠一个覆盖在上层的View,通过对View自定义处理绘制流程来实现的。

所以这里我们也可以采用类似的方案,但是不用去实现复杂的卷曲效果,所以我们需要实现的只有两点

  1. 用一个OverlayView来覆盖ViewPager2
  2. OverlayView展示的是当前Item(至于展示的内容依照需要而定,也可以是item的View.drawToBitmap())
  3. 对OverlayView中的Bitmap展示区域进行限制

这里我不打算直接对ViewPager2的布局进行修改,需要介绍一个不常用的内容:ViewOverlay,它提供了一种在不改变视图层次结构的情况下添加、移除或修改视图的能力,这样我们就不用改变ViewPager2本身所在的布局结构了。

先来简单实现对图片的裁剪吧,其实代码很简单,就是根据滚动的offset来进行clip裁剪。

class PageOverlayView constructor(context: Context) : View(context) {

    var overlay: Bitmap? = null
        set(value) {
            field?.recycle()
            field = value
        }
    var currentPosition: Int = -1
    var currentPositionOffsetPx: Int = 0
        set(value) {
            field = value
            invalidate()
        }


    override fun onDraw(canvas: Canvas?) {
        val overlay = overlay ?: return super.onDraw(canvas)
        canvas?.withClip(left, top, width - currentPositionOffsetPx, bottom) {
            drawBitmap(overlay, 0f, 0f, null)
        }

    }
}

注册ViewPager2的registerOnPageChangeCallback,不过这里需要主要调用以下OverlayView的layout,因为ViewOverlay是不会帮你进行layout的,所以layout需要自己来调用。

viewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        super.onPageSelected(position)
        pageOverlayView.layout(
            viewpager.left,
            viewpager.top,
            viewpager.right,
            viewpager.bottom
        )
    }

    override fun onPageScrolled(
        position: Int,
        positionOffset: Float,
        positionOffsetPixels: Int
    ) {
        super.onPageScrolled(position, positionOffset, positionOffsetPixels)
        if (position != pageOverlayView.currentPosition) {
            pageOverlayView.overlay =
                viewpager.recyclerView.findViewHolderForAdapterPosition(position)?.itemView?.drawToBitmap()
        }
        pageOverlayView.currentPosition = position
        pageOverlayView.currentPositionOffsetPx = positionOffsetPixels
    }
})

// 最后添加到ViewPager2的overlay中
viewpager.overlay.add(pageOverlayView)

这样,我们就十分简单的实现了需要的效果,当然这只是很简略的实现,具体的细节未完善,只是介绍一个可行的方案。

完整Demo

Github: Lowae/PagerOverlay (github.com)

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYpgks1N' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片