Android ViewPager2 实现阅读器横向翻页效果(一)— ViewPager2的原理及堆叠效果实现
ViewPager2相较于ViewPager作出了很多改进,采用了RecyclerView作为内部实现,提高了应用的灵活性,使得横向翻页效果可以使用它来实现。最终整体效果如下:
下面先对其滑动原理进行介绍:
ViewPager2的滑动机理
假设我们在ViewPager的Adapter中添加了4个页面的内容: 当前可见的页面是页面id 0,它对应的position是0,注意这里的position与官方文档中所描述的the position index of xxx一致,position与页面的id号(显示在页面上的红色数字)并非是一一对应的。官方文档对于position的描述如下: 当我们进行了一次向左滑动后,页面变为: 可以看到,对于position来说,当前可见的页面(即id为1的页面)所对应的position始终为0,在这页之前的页的position小于0,在这页之后的大于0。 如果不作任何改动,原生的显示效果如下: 显然,这样的效果达不到我们的要求,我们需要将左边的页面叠放到当前页面之下,并随屏幕滑动的动作依次移开。下面的动图展示了这一过程:
ViewPager2堆叠滑动原理
图中,立方体为页面的堆叠区域,其顶层(z坐标为0)为当前可见的页面。下方为当前状态viewpager内部的position变化情况。
- 初始状态时,可见的页仅为id为0的页面,其他页面通过设置一定的偏移依次隐藏在下方;
- 第一次向左滑动时,id为0的页面被移动到pos=-1的位置处,这个动作与原生viewpager2的效果一致,接着其下方的页面1上移至顶层,这就要求将原生的x轴方向的运动变为z方向的运动。而其他的页面2、3则也需要依次上移;
- 类似的,第二次左滑时,页面1移动至pos=-1的位置处,页面2被置于顶层可见,其他页面则依序移动;
- 右滑的逻辑与此类似,这里不再赘述。
由此可见,要实现堆叠移动需要实现:在初始状态时对页面设置偏移使之依次序堆叠;在后续移动中将x轴向的运动变为z轴方向的运动
幸运的是,viewpager2为我们提供了相应的接口来实现这两个目标:
ViewPager2.PageTransformer
abstract void transformPage(@NonNull View page, float position)
这里的参数 page为在adapter中设置的viewholder所对应的itemview;参数 position为上文所提到的页面的索引,但这里的position不是整数,而是随着屏幕的滑动而逐渐变化。 接口transformPage会在初次create的时候以及每次页面被滑动时调用(滑动时将调用多次),这就同时满足了我们既要对初始状态进行改变又要在移动中进行变动的要求。 在下图中,我给出了这种改动的规律: 如图所示,页面可以被分为两类,一类是 pos<=0(即 i<=0) 的页面,这些页面都处于表面(蓝色立方体左侧的平面上,符合viewpager原生的运动规律),第二类是 pos>0(即i>0) 的页面,这些页面处于堆叠状态(蓝色立方体内部,需要将x轴向的运动变为z轴方向的运动)。
如何实现堆叠?
这里以页面3为例: 需要将其向左移动其宽度w的距离,使其叠放在页面2之下,故有:
Δ
X
3
ΔX_3
ΔX3?=
?
w
?
1
-w*1
?w?1 若还存在页面3右侧的页面4,则:
Δ
X
4
ΔX_4
ΔX4?=
?
w
?
2
-w*2
?w?2 以此类推,可得:
如何实现x轴向的运动变为z轴方向的运动?
观察上面的公式,可知当i=0时,将取消x轴向的运动,于是只需要在此基础上添加ΔZ即可,通过上面的原理分析易得: 这样我们就得到了适用与普遍情形的堆叠移动的变化公式。
transformPage接口的调用过程
前面讲解了viewpager移动和堆叠的原理,要具体实现这样的效果,必须清晰理解接口transformPage是如何调用的。 我在viewholder中添加了textview page_id一边区分不同的页面,实验所用到的关键代码如下,代码将当前页面的id和position打印到logcat中:
@Override
public void transformPage(@NonNull View page, float position) {
TextView viewId = page.findViewById(R.id.page_id);
String s_id = viewId.getText().toString();
Log.d("transformPage", s_id + " | pos = " + position);
}
- create时:
- 一次滑动后 (结果太长,截取部分)
由此可知,在初始化时,transformPage会对每一个在预加载区域内的页面进行回调,其position值与我们前面分析的一致(预加载数量可以通过函数 setOffscreenPageLimit(5) 设置)。在滑动时,所有的页面(预加载区域内的)都会随滑动回调,其值从
i
i
i 逐步变化到
i
?
1
i-1
i?1 ,这使得我们的变化效果可以是连续的。
代码实现
上面的讲解已经比较详细了,我们得到下面的代码就很自然了:
pageView.setPageTransformer(new ViewPager2.PageTransformer() {
@Override
public void transformPage(@NonNull View page, float position) {
TextView viewId = page.findViewById(R.id.page_id);
String s_id = viewId.getText().toString();
if (position <= 0.0f) {
page.setTranslationX(0.0f);
page.setTranslationZ(0.0f);
} else {
page.setTranslationX((-page.getWidth() * position));
page.setTranslationZ(-position);
}
}
});
此处的 setTranslationX 和 setTranslationZ 即对页面施加一个ΔX、ΔZ的运动,需要注意的是,这里的坐标系是每个页面独立且相对于其左边缘的。
边缘阴影
在页面添加阴影会使得堆叠滑动效果更加明显,这里采用 androidx.cardview.widget.CardView 作为布局,通过设置属性cardBackgroundColor 添加阴影:
app:cardBackgroundColor="#CCE8CF"
这么做之后你会发现阴影似乎并没有显示出来,这是因为阴影在边缘区域被隐藏了,还需要在CardView下添加如下属性,;
android:layout_marginEnd="2dp"
至此,简单的堆叠滑动效果就实现了,这只是横向翻页效果实现的第一步。下面第二章我将会讲解如何实现滑动中动态增加新章节,以满足流畅的阅读体验。
|