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給定的高度的最大值.

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