巧用android:layout_weight,把WebView和Button一起裝進AlertDialog

ZxLee發表於2019-02-23

背景

要求:把WebView裝進AlertDialog,並且實現Dialog的高度根據WebView的ContentHeight高度來變化,該怎麼做?

當然是直接通過AlertDialog的setView方法,把WebView設定進去就好了,並不需要其他的特殊技巧,而且當WebView的ContentHeight超過螢幕高度的時候,WebView還可以自動變成滾動模式,非常nice。

好的,現在產品需求變了,要在WebView下面增加一個自定義的“確定”按鈕。
也很簡單,我們用一個LinearLayout來裝上WebView和Button,同樣通過setView來把這個Layout設定為Dialog檢視,執行,大功告……不對,有問題。
當WebView的ContentHeight超過螢幕高度的時候,下方的自定義“確定”按鈕就被擠得不見了,而正常的需求都會是優先保證Button的顯示,WebView變成滾動才對。
那麼,問題來了,怎麼把WebView和Button一起裝進AlertDialog,還要實現Dialo的高度適應WebView的高度?

RelativeLayout大法(失敗)

對付這種部分固定部分彈性的佈局,很容易讓人想到用RelativeLayout來解決。
在RelativeLayout佈局內,將Button設定為alignParentBottom=true,即靠著父佈局底部放置,將WebView置於Button的上方,再將根佈局的高度設定為wrap_content。這樣應該就可以保證,Button是固定顯示在彈窗底部的,WebView顯示於Button的上方,使得WebView內容超高時也不會把button給擠掉。當內容超高時,可以看到效果如下:


可以看到“HELLO”這個button沒有被WebView擠掉,符合我的預期。再來測試一下WebView不超高的情況。

額,這上面多出來的空白是什麼,是網頁裡的嗎?用佈局分析工具來檢視,從而知道,這塊空白不屬於WebView,而是屬於父佈局RelativeLayout。這麼看來,我們之前設定了RelativeLayout的高度為wrap_content,但是並沒有使得RelativeLayout和WebView的上邊緣重合。從現象來看,RelativeLayout的高度像是被設定成了match_parent。

是什麼導致的這種現象呢?一個一個屬性的查,很快就確認了,Button的alignParentBottom屬性設定為true的時候就會導致這種現象發生。
我本來想看看RelativeLayout的原始碼來找找原因,結果剛看到這個類的註釋,原因就找到了。且看註釋:

 * <p>
 * Note that you cannot have a circular dependency between the size of the RelativeLayout and the
 * position of its children. For example, you cannot have a RelativeLayout whose height is set to
 * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT} and a child set to
 * {@link #ALIGN_PARENT_BOTTOM}.
 * </p>複製程式碼

在RelativeLayout的size設定和子檢視的定位之中,兩者不允許產生迴圈依賴,比如,將RelativeLayout高設定為wrap_content,又將子檢視設定為align_parent_bottom。
這個例子簡直就是針對我來舉的啊!註釋中說的迴圈依賴在哪裡呢?仔細想想看,wrap_content導致RelativeLayout需要等待子檢視確定好高度,它才能確定好自己的高度;而align_parent_bottom導致子檢視需要等待父檢視RelativeLayout確定了底部位置才能確定自己的位置,可是父檢視現在都不知道自己多高,怎麼知道自己的底部在哪裡呢?所以父檢視乾脆宣佈wrap_content無效,用match_parent來做高度了。

LinearLayout大法(成功)

用RelativeLayout是做不到了,其他的佈局似乎都不善於應對這種“固定+浮動”高度的場景,怎麼辦呢?

其實,原生的Dialog實現就已經給了我答案。
Dialog#setView無論傳入多高的檢視,它總是不可能把Dialog的標題,還有“確定”,“取消”這些按鈕給擠走的。那麼,只要按照這種原生實現方式來做,就可以滿足我的需求了。嘿,又到了“read the fucking code”的時候了。
AlertDialog負責View展示,裡面關聯一個AlertDialogController來操作邏輯,我們要找它關聯的layout檔案,直接在它的建構函式裡就可以看到,檔名為alert_dialog.xml。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentPanel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:paddingTop="9dip"
    android:paddingBottom="3dip"
    android:paddingStart="3dip"
    android:paddingEnd="1dip">

    <LinearLayout android:id="@+id/topPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="54dip"
        android:orientation="vertical">
        <LinearLayout android:id="@+id/title_template"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical"
            android:layout_marginTop="6dip"
            android:layout_marginBottom="9dip"
            android:layout_marginStart="10dip"
            android:layout_marginEnd="10dip">
            <ImageView android:id="@+id/icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:paddingTop="6dip"
                android:paddingEnd="10dip"
                android:src="@drawable/ic_dialog_info" />
            <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle"
                style="?android:attr/textAppearanceLarge"
                android:singleLine="true"
                android:ellipsize="end"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textAlignment="viewStart" />
        </LinearLayout>
        <ImageView android:id="@+id/titleDivider"
            android:layout_width="match_parent"
            android:layout_height="1dip"
            android:visibility="gone"
            android:scaleType="fitXY"
            android:gravity="fill_horizontal"
            android:src="@android:drawable/divider_horizontal_dark" />
        <!-- If the client uses a customTitle, it will be added here. -->
    </LinearLayout>

    <LinearLayout android:id="@+id/contentPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">
        <ScrollView android:id="@+id/scrollView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="2dip"
            android:paddingBottom="12dip"
            android:paddingStart="14dip"
            android:paddingEnd="10dip"
            android:overScrollMode="ifContentScrolls">
            <TextView android:id="@+id/message"
                style="?android:attr/textAppearanceMedium"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="5dip" />
        </ScrollView>
    </LinearLayout>

    <FrameLayout android:id="@+id/customPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1">
        <FrameLayout android:id="@+android:id/custom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="5dip"
            android:paddingBottom="5dip" />
    </FrameLayout>

    <LinearLayout android:id="@+id/buttonPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="54dip"
        android:orientation="vertical" >
        <LinearLayout
            style="?android:attr/buttonBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingTop="4dip"
            android:paddingStart="2dip"
            android:paddingEnd="2dip"
            android:measureWithLargestChild="true">
            <LinearLayout android:id="@+id/leftSpacer"
                android:layout_weight="0.25"
                android:layout_width="0dip"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:visibility="gone" />
            <Button android:id="@+id/button1"
                android:layout_width="0dip"
                android:layout_gravity="start"
                android:layout_weight="1"
                style="?android:attr/buttonBarButtonStyle"
                android:maxLines="2"
                android:layout_height="wrap_content" />
            <Button android:id="@+id/button3"
                android:layout_width="0dip"
                android:layout_gravity="center_horizontal"
                android:layout_weight="1"
                style="?android:attr/buttonBarButtonStyle"
                android:maxLines="2"
                android:layout_height="wrap_content" />
            <Button android:id="@+id/button2"
                android:layout_width="0dip"
                android:layout_gravity="end"
                android:layout_weight="1"
                style="?android:attr/buttonBarButtonStyle"
                android:maxLines="2"
                android:layout_height="wrap_content" />
            <LinearLayout android:id="@+id/rightSpacer"
                android:layout_width="0dip"
                android:layout_weight="0.25"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:visibility="gone" />
        </LinearLayout>
     </LinearLayout>
</LinearLayout>複製程式碼

佈局元素很多,但是思路很清晰。
根元素為LinearLayout,共有四個子檢視。
Title檢視,展示標題icon和文案;
Content檢視,展示Message,可以通過setMessage來展示文案,為應付文案過長問題還包了一層ScrollView;
Custom檢視,看到名字就知道,setView設定的View就塞在這裡;
Button檢視,放positive,neutral,negative三個按鈕用的。

再細看,可以看出Title和Button設定了MinHeight=54dp,沒有設定weight;
而Content和Custom設定了weight=1,沒有設定MinHeight;

這,就是使得Title和Button檢視的高度能固定,而Content和Custom檢視的高度浮動的關鍵屬性。

在LinearLayout檢視裡,如果子檢視都設定了weight,那麼就按照weight權重來分配各自的高度(寬度);如果並不是所有的子檢視都有weight,那麼,LinearLayout會優先把高度(寬度)分配給無weight的子檢視,分配完後,再把剩下的高度(寬度)按weight權重分配給其他子檢視。

所以,最後用LinearLayout來實現我們的需求就很簡單了:將根佈局LinearLayout高度設定為wrap_content,裡面上下放置WebView和Button,將WebView的weight設定為1(其實任意非0值都可以),高度可以設定為任意值(Android Studio會建議設定為0dp),再將Button高度設定為wrap_content,不設定weight值,就可以了。

編譯執行,測試WebView高度越界和高度不越界的情況,表現都符合需求了,完美!

總結

綜上,本輪問題的解決涉及到兩個點:

  1. RelativeLayout中注意,其寬高的設定不要和子檢視的位置設定產生迴圈依賴,否則會產生意料之外的佈局效果;
  2. LinearLayout的子檢視可以設定weight,也可以不設定,父佈局總的高度(寬度)會優先滿足給不設定weight的子檢視,再將剩餘的寬度(高度)按照weight權重分配給設定了weight的子檢視。

11/07更新

評論區的@四月一號 同學有一種利用負值的margin來實現的方式,也可以解決問題,大家可以參考一下。

相關文章