之前遇到一个需求,设计师希望的图片轮播切换效果如下图所示。
(图片有点大,加载稍微等一小会)
最开始我的想法是采用ViewPager2的PageTransformer来实现,但是稍微实践了一下便发现,单独只用PageTransformer来实现不太行,因为PageTransformer本质上类似是对view的属性动画操作,如tranalationX,Y,Z以及等等其他属性。
不过,先一步一步来实现吧。
堆叠效果
首先,PageTransformer可以实现堆叠滑动效果的,实现逻辑也很简单,本质上就是抵消滑动的位移,其原理的示意图如下:
具体实现代码如下:
具体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自定义处理绘制流程来实现的。
所以这里我们也可以采用类似的方案,但是不用去实现复杂的卷曲效果,所以我们需要实现的只有两点
- 用一个OverlayView来覆盖ViewPager2
- OverlayView展示的是当前Item(至于展示的内容依照需要而定,也可以是item的View.drawToBitmap())
- 对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)