本文记录一个关于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;
}
}