ViewPager中的高度属性设置后为什么不符合预期

分析一个Viewpager中设置高度属性wrap_content为什么不是预期的内容给大小,而是充满整个屏幕高度.

截图:

先说下简单的布局:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:layout_centerInParent="true"
    android:background="#aadcff"
    android:clipChildren="false">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/view_Pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginLeft="60dp"
        android:layout_marginRight="60dp"
        android:clipChildren="false" />

    <RadioGroup
        android:id="@+id/radio_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal|bottom"
        android:layout_marginBottom="10dp"
        android:orientation="horizontal" />

</FrameLayout>

其中ViewPager中的具体item是一个linearlayout,底部可以认为是一个bottombar,

viewpager的 android:layout_height="wrap_content" ,所以预期的效果是linearlayout的高度,而不是跟底部的bottombar重叠,

也看下linearlayout的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    >
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:gravity="center" />

</LinearLayout>

至于Viewpager怎么用,就不贴代码了,这里仅是一个简单的使用demo,分析写为什么wrap_content后,内容还会填满整个屏幕高度?

分析的思路是这样的,首先View树的遍历,有三个重要的过程,

onMeasure(从根view发起,递归测量每个view的宽高),

onLayout(view的宽高测量出来之后,就是给view指定放置的位置),

onDraw(view的大小位置都确定了,开始把view表示的内容画到Canvas上).

上面的流程,详细了解可以去从ViewRootImpl中doTraversal开始,这里是每次布局需要更新都会触发的回调.

    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

回到,ViewPager的问题上来,就要去分析ViewPager的onMeasure方法;

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) @ViewPager.java{
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));
        int size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            child.measure(widthSpec, heightSpec);
        }
}

这里的做法有点暴力,进来就设置了自己的大小,参数中这两个:int widthMeasureSpec, int heightMeasureSpec)值是父view传过来的,表示父view对子view宽高的一个限制,这个值通常是contentview的大小.

为什么说这里的onMeasure有点暴力,因为测量view的宽高,通常的做法是:父view给出限制信息,在根据子view的layoutParams信息,递归测试每一个子view的宽高,最后通过setMeasuredDimension再去设置自身的大小,如Linearlayout,FrameLayout的onMeasure都是这样做的.

问题原因找到了,这个问题怎么该?

当然也可以暴力点,直接给ViewPager的高度指定一个确定的dp值,除非只在一个固定设备上跑,或者高度不需要自适应,否则都不可能这样做.

其次,考虑去重写ViewPager,只需要重写其中onMeasure方法,

public class ViewPagerDemo extends ViewPager {
    private static final String TAG = ViewPagerDemo.class.getName();

    public ViewPagerDemo(@NonNull Context context) {
        super(context);
    }

    public ViewPagerDemo(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        Log.d(TAG,"ViewPagerDemo(Context context, AttributeSet attrs)");
    }
    public ViewPagerDemo(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) {
        super(context, attrs);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);

        int maxHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            int childHeight = child.getMeasuredHeight();
            Log.d(TAG, "onMeasure: childHeight="  + childHeight+",parentHeight="+parentHeight);
            if (childHeight > maxHeight) {
                maxHeight = childHeight;
            }
        }
        if (maxHeight > parentHeight -getBottomBarHeight()) {
            maxHeight = parentHeight -getBottomBarHeight();
        }
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight,
                MeasureSpec.getMode(heightMeasureSpec));

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private int getBottomBarHeight() {
        return 80;
    }
}

其中的构造函数会调用第二个,在xml中引用自定义的类,构造函数会调用有两个参数的那个,如果是java中new这个类对象,会调用一

个参数的构造函数,

三个参数的构造函数,通常是有自定义属性,在这里去解析自定义的布局属性,如果在这里解析自定义属性,记得解析完,要把TypedArray实例释放掉.这里 import androidx.viewpager.widget.ViewPager;这个viewpager中没有是三个参数的构造函数,所以没有调用三个参数的super.

在onMeasure中,可以看到传进来的高度通常是整个屏幕的高度,减去的statusbar高度,减去title的高度.

这里是算出子view的最大高度,然后把这个最大高度设置为viewpager的高度.替换成自定义Viewpager后,效果是:

实际ViewPager的高度是真实内容的高度,这里真实内容的高度是LInearlayout去measure的,因为linearLayout中只有一个TextView,并且只有一行文字,所以实际高度只有38,可以去看LienearLayout的onMeasure的过程,是把其中的子view的高度累加后,作为自己的真实高度,当然这个真实的高度,一定是不大于 int parentHeight = MeasureSpec.getSize(heightMeasureSpec); 也即是不会大于父view给定的高度的最大值.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章