ViewPager与CoordinatorLayout一起使用的一个Bug

简介: 本文记录一个关于ViewPager与CoordinatorLayout一起使用的Bug,目前虽然有解决问题的方法,但是引起这个bug的原因依然没有找到。最初的布局是正常的项目最初的布局树是这样的:CoordinatorLayout--RelativeL...

本文记录一个关于ViewPagerCoordinatorLayout一起使用的Bug,目前虽然有解决问题的方法,但是引起这个bug的原因依然没有找到。

最初的布局是正常的

项目最初的布局树是这样的:

CoordinatorLayout
--RelativeLayout(height:match_parent)
----各种View
----FrameLayout(height:wrap_content)
------layout
------WrapHeightViewPager
----ImageView

这里的布局要求是这样的:
layoutWrapHeightViewPager同一时间最多只有其中一个会显示,也可能两个都不显示。当它们之间有任何一个显示的时候,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

也就是把FrameLayoutImageViewRelativeLayout中抽到外面来了。
然后为ImageView写了一个Behavior,使用Behavior来控制ImageView的UI逻辑。
然后发现,把布局抽出来后,一个Bug就来了。

Bug出现及原因追查

我发现WrapHeightViewPager在第一次数据更新,有了数据之后,并没有被显示出来,但是第二次更新数据就出来了。
打印了FrameLayout高度,为0。
我以为是调用setVisibility(int)没起作用,打印了WrapHeightViewPagergetVisibility(),值为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本身的业务无关的界面逻辑却要在业务逻辑里处理,代码耦合度大。

于是今天早上继续探寻此问题。

继续求解

我打印了WrapHeightViewPageronLayout(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更新时会回调到BehavioronLayoutChild(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;
    }
}
目录
相关文章
|
7月前
|
Android开发
两种方法,教你解决 ViewPager 嵌套 ViewPager滑动冲突(一)
两种方法,教你解决 ViewPager 嵌套 ViewPager滑动冲突
|
7月前
|
Android开发 UED 开发者
两种方法,教你解决 ViewPager 嵌套 ViewPager滑动冲突(二)
两种方法,教你解决 ViewPager 嵌套 ViewPager滑动冲突
|
8月前
|
XML Android开发 数据格式
Android 使用ViewPager实现基本的翻页效果
Android 使用ViewPager实现基本的翻页效果
94 0
|
Android开发
Android笔记:ViewPager和TabLayout连用时,去除ViewPager预加载
Android笔记:ViewPager和TabLayout连用时,去除ViewPager预加载
172 0
|
Android开发 iOS开发
Android底部导航——BottomNavigationView+ViewPager+Fragment
Android底部导航——BottomNavigationView+ViewPager+Fragment
401 0
Android底部导航——BottomNavigationView+ViewPager+Fragment
|
Android开发 容器
Android ViewPager和PagerAdapter简单代码写法
Android ViewPager和PagerAdapter简单代码写法 总是忘记,记下来备忘: package zhangphil.
1499 0