ViewPager与CoordinatorLayout一起使用的一个Bug
本文记录一个关于ViewPager与CoordinatorLayout一起使用的Bug,目前虽然有解决问题的方法,但是引起这个bug的原因依然没有找到。
最初的布局是正常的
项目最初的布局树是这样的:
CoordinatorLayout
--RelativeLayout(height:match_parent)
----各种View
----FrameLayout(height:wrap_content)
------layout
------WrapHeightViewPager
----ImageView
这里的布局要求是这样的: layout与WrapHeightViewPager同一时间最多只有其中一个会显示,也可能两个都不显示。当它们之间有任何一个显示的时候,ImageView要在它们上面,当它们两个都不显示的时候,ImageView要在底部。
所以我把这两个View放在了一个FrameLayout中,并且让ImageView在其之上来实现这个需求。
WrapHeightViewPager继承自ViewPager,以实现按内容自适应高度。它重写了以下方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int h = child.getMeasuredHeight();
if (h > height) height = h;
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
也就是在onMeasure的时候去测量子View,以子View的最大高度作为ViewPager的高度。
因交互需求而做的改动
由于接下来在本次版本迭代中,交互会做比较大的改动,layout这一部分可能需要使用CoordinatorLayout中的Behavior来实现一些效果,ImageView也会影响到。所以我将涉及到的这两块View都抽出到CoordinatorLayout下,为后续的改动先做一个铺垫。改动后的布局树如下:
CoordinatorLayout
--RelativeLayout(height:match_parent)
--FrameLayout(heiht:wrap_content)
----layout
----WrapHeightViewPager
--ImageView
也就是把FrameLayout和ImageView从RelativeLayout中抽到外面来了。
然后为ImageView写了一个Behavior,使用Behavior来控制ImageView的UI逻辑。
然后发现,把布局抽出来后,一个Bug就来了。
Bug出现及原因追查
我发现WrapHeightViewPager在第一次数据更新,有了数据之后,并没有被显示出来,但是第二次更新数据就出来了。
打印了FrameLayout高度,为0。
我以为是调用setVisibility(int)没起作用,打印了WrapHeightViewPager的getVisibility(),值为VISIBLE。也就是生效了。
打印了WrapHeightViewPager的高度,为0。
换成ViewPager,能显示出来,但是高度已经是占满父布局了,看来应该是高度测量的问题。
继续换回来找原因。
打印里面的addView(View)和onMeasure(int, int)方法。发现前者有被调用,但后者只在界面初始化的时候被调用一次,这时没有数据所以计算出来的高度为0,在更新数据之后并没有跟着被调用,所以导致WrapHeightViewPager的高度仍然为0。
尝试解决
然后尝试。
在adapter更新数据之后,调用viewpager的requestLayout(),无效。
在addView(View)之后调用requestLayout(),无效。
改成forceLayout(),无效。
在addView(View)之后,调用requestApplyInsets(),居然起作用了!!
但是—— requestApplyInsets()需要在API 20之后才可以使用,我们应用的minSdkVersion为15。
继续寻找其他方法。
google,没找到一样的问题。
stackoverflow,同样没找到一样的问题。但其中有一个问题的答案给了我灵感:https://stackoverflow.com/a/33253976/2673757
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mViewPager.requestLayout();
}
}, 100);
由于在第二次的时候,WrapHeightViewPager会显示出来,也就是它的高度最终还是会测量的。但是在第一次一更新就去调用requestLayout()并没有效,所以我尝试也加了个延迟:
mBillPager.postDelayed(new Runnable() {
@Override
public void run() {
mBillPager.requestLayout();
}
}, 100);
结果果然显示出来了。但是这样却还有两个问题:
1. 这里是写死的固定100毫秒的延迟,但是实际上我并不需要多少才够,100这个魔数让我看着并不舒服。
2. 有多个地方会更新adapter里的数据,WrapContentViewPager本身的业务无关的界面逻辑却要在业务逻辑里处理,代码耦合度大。
于是今天早上继续探寻此问题。
继续求解
我打印了WrapHeightViewPager的onLayout(boolean, int, int, int, int)方法,发现它是有被调用的。然后我试着把requestLayout()的调用放到它这里。直接调用还是不行,改为如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed) {
post(new Runnable() {
@Override
public void run() {
requestLayout();
}
);
}
}
同样问题解决。现在业务逻辑的代码与改动布局之前基本一样,也就是布局上的重构没有影响到业务代码,两者分离。Bug暂先这样处理,不给业务逻辑带来其他代码。
2017-10-26补充:
使用Behavior来解决
今天又发现可以使用CoordinatorLayout.Behavior来解决这一问题。由于ViewPager更新时会回调到Behavior的onLayoutChild(CoordinatorLayout parent, ViewPager child, int layoutDirection)方法,所以可以在该方法重新对ViewPager进行layout操作。
代码如下:
/*
* Copyright (c) 2017. Xi'an iRain IOT Technology service CO., Ltd (ShenZhen). All Rights Reserved.
*/
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import com.githang.android.snippet.view.WrapHeightViewPager;
/**
* @since 2017-10-25 4.2.3
*/
public class ViewPagerBehavior extends CoordinatorLayout.Behavior<ViewPager> {
public ViewPagerBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onLayoutChild(CoordinatorLayout parent, ViewPager child, int layoutDirection) {
parent.onLayoutChild(child, layoutDirection);
if (child instanceof WrapHeightViewPager) {
int height = 0;
int measureHeight = parent.getHeight();
int measureWidth = parent.getWidth();
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(measureWidth, View.MeasureSpec.AT_MOST);
for (int i = 0; i < child.getChildCount(); i++) {
View item = child.getChildAt(i);
item.measure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(measureHeight, View.MeasureSpec.UNSPECIFIED));
int h = item.getMeasuredHeight();
if (h > height) {
height = h;
}
}
height += child.getPaddingTop() + child.getPaddingBottom();
child.layout(parent.getLeft(), child.getBottom() - height, child.getRight(), child.getBottom());
}
return true;
}
}