ViewPager作为ListView的Item时的疑难杂症

前文

长复杂页面在项目中采用listview实现的形式,利用mergeAdapter来对界面做控制,偏偏有个itemViewviewpager的形式,于是该itemView里有个ViewPager作为子view来做横滑操作。做出后,看上去很美。但是,突然发现当该item划出屏幕,再划回来的时候,第一次切换viewPager总会秒切,而且不丝滑,但之后的切换操作并无什么异常,都很丝滑。强迫症受不了,必须搞。而且只在7.x的版本上有问题,在4.X,6.X上都很perfect。

分析

  • 首先是冥想阶段
  1. 既然和划出屏幕有关系,那么可以认为:listViewItem回收机制造成的影响。
  2. 可能是ViewPager在划出划回的状态有问题。

排查阶段

  • 先看看ViewPagerOnPageChangeListener 在正常与异常情况下切换时回调的状态。发现在onPageScrollStateChanged的回调中,异常状态少了 IDLE(0) 和SETTLING (2),只有DRAGGING(1)。

  • 查看源码 IDLE(0) 只在smoothScrollTo方法中设置过此状态,依次上溯,发现在setCurrentItemInternal方法中有代码块:

1
2
3
4
5
6
7
8
9
10
11
if (mFirstLayout) {
mCurItem = item;
if (dispatchSelected) {
dispatchOnPageSelected(item);
}
requestLayout();
} else {
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
  • 很大概率是此代码块的if else 语句造成的,换言之就是 mFirstLayout 变量造成的。打个断点试试看!
  • 哇,果然是 当划出屏幕后再回来 走的是 mFirstLayout=true,而正常情况下则是走的else方法。

  • 接下来看看 mFirstLayout在哪被赋值过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void setAdapter(PagerAdapter adapter) {
...
mFirstLayout = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
mFirstLayout = false;
}

mFirstLayout在上述方法中赋值过,那么问题就显而易见了。

原因:当该itemView被划出屏幕时,会被detach掉,再划回来则会走onAttachedToWindow方法,此时mFirstLayout被设置为true,所以在划回来,第一次切换时会造成前文提到的情况。

解决方案也随之出现(回字有几种写法?):

  1. 设置viewpagerOnAttachStateChangeListener侦听,在onViewAttachedToWindow回调中,调用ViewPager.requestLayout();方法将mFirstLayout 置为false。(对应onLayout
  2. 利用反射,在onViewAttachedToWindow回调中,将mFirstLayout置为false。这个方法有人会有疑问就是 那岂不是在第一次onViewAttachedToWindow的时候mFirstLayout也为false?答案是yes,但是别忘了,setAdapter里会置为true。往往我们第一次onViewAttachedToWindow之后都会给ViewPager setAdapter 啊。所以整体还是对的。
  3. 投机取巧法。当itemView划出时调用两次setCurrentItem(currentItem)方法,这样第二次就走else的代码了。但是此方法需要写的代码有点多,而且罗嗦,耦合高,我压根就没试。

方法一:

1
2
3
4
5
6
7
8
9
10
11
viewPager.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
viewPager.requestLayout();
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});

方法二:

1
2
3
4
5
6
7
8
9
10
viewPager.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
resetFirstLayout();
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});

resetFirstLayout的主要语句为:

1
Field mFirstLayout = ViewPager.class.getDeclaredField("mFirstLayout");

mFirstLayout.setAccessible(true);
mFirstLayout.set(viewPager, false);
(这两句加到代码块生成不了html,蛋疼,拿出来意会就好)

好了,就这么多吧。。