本文解决ViewPager在实现gallery时,PageTransformer特效需要滑动一下才能生效的问题。
ViewPager版本:androidx-1.0.0。
复现问题前先复习下ViewPager实现gallery的方法:
ViewPager可以实现gallery效果,核心代码就是把ViewPager及其父View的clipChild设置为false,使ViewPager的子view可以超出ViewPager的范围:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp_gallery"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_gravity="center_horizontal"
android:clipChildren="false"/>
</FrameLayout>
为了给gallery加入动效,需要使用PagerTransformer(PagerTransformer细节不再赘述,本例中两侧的视图会进行拉伸);为了左右两边的视图都能同时实例化,设置offScreenPageLimit:
vpGallery.setOffscreenPageLimit(3);
vpGallery.setPageTransformer(false, new ViewPager.PageTransformer(){
@Override
public void transformPage(@NonNull View page, float position) {
float scale = 1 - Math.abs(position) * .2f;
page.setScaleX(scale);
page.setScaleY(scale);
}
}, View.LAYER_TYPE_NONE);
问题的现象:
ViewPager初始化之后PagerTransformer不会把两侧的View变小,只有拖动之后才能使其生效。
思路:
既然初始时有问题,拖动时没问题,必然是transformPage回调情况存在差异,找出这种差异就能解决初始的问题。
追踪:
首先是拖动时,transformPage方法回调正常,对于位于两侧的View,其position参数不为0,保证这两个View的尺寸产生拉伸效果。
初始化时,追踪PagerTransformer的调用栈情况:
发现transformPage被回调时position参数都为0,再向上追踪到ViewPager的onPageScrolled方法,代码如下:
@CallSuper
protected void onPageScrolled(int position, float offset, int offsetPixels) {
// Offset any decor views if needed - keep them on-screen at all times.
if (mDecorChildCount > 0) {
final int scrollX = getScrollX();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
final int width = getWidth();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.isDecor) continue;
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
int childLeft = 0;
switch (hgrav) {
default:
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
paddingLeft += child.getWidth();
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
paddingLeft);
break;
case Gravity.RIGHT:
childLeft = width - paddingRight - child.getMeasuredWidth();
paddingRight += child.getMeasuredWidth();
break;
}
childLeft += scrollX;
final int childOffset = childLeft - child.getLeft();
if (childOffset != 0) {
child.offsetLeftAndRight(childOffset);
}
}
}
dispatchOnPageScrolled(position, offset, offsetPixels);
if (mPageTransformer != null) {
final int scrollX = getScrollX();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.isDecor) continue;
final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
mPageTransformer.transformPage(child, transformPos);
}
}
mCalledSuper = true;
}
发现这个transformPos是根据滚动以及left参数来计算的,断点进去发现left为0。
很显然,调用notifyDataSetChanged之后,会触发ViewPager的setCurrentItemInteral方法,此时还没有及时地layout生成上下左右参数,就回调了transformPage方法,才导致transformPage回调所有子View时position都为0。
此时第一个思路已经出现,就是在layout完之后再触发一下setCurrentItemInteral,进而触发transformPage回调。
阅读下ViewPager的源码,发现transformPage方法只有在onPageScrolled时才会调用,而触发onPageScrolled最方便的方法就是调用:
setCurrentItem(0, false);
方案一:
所以,一个方案就是,初始化之后,立即post一个Runnable去执行notifyDataSetChanged,此时layout已经完成,left已经生成,会回调正确的参数给transformPage方法。不过这个方法实测会产生闪动。
方案二:
回看自己的代码,ViewPager立即setAdapter(空数据),然后请求到网络数据,请求回来后才notifyDataSetChanged,transformPage走的是notifyDataSetChanged的流程。
如果Adapter初始是有数据的,流程会有不同吗?
下面是Adapter初始有数据时transformPage的回调情况:
这个就好多了,在onLayout中,left参数已经生成,所以会以准确的数据回调transformPage,进而产生拉伸效果。
结论:
使用ViewPager做gallery,在使用PageTransformer时,注意要在第一次setAdapter的时候就把PagerAdapter的相关视图的数据初始化好,以正确地回调PageTransformer。
不要在ViewPager初始化时设置空PagerAdapter然后过段时间再更新Adapter,否则就会产生本文的问题,虽然这应该算是个谷歌的bug。
有其他解法欢迎留言。