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