Android 對話方塊 Dialog 深度剖析

fengsehng發表於2016-12-06

對話方塊

對話方塊是提示使用者作出決定或輸入額外資訊的小視窗。 對話方塊不會填充螢幕,通常用於需要使用者採取行動才能繼續執行的模式事件。

對話方塊設計

這裡寫圖片描述

Dialog 類是對話方塊的基類,但您應該避免直接例項化 Dialog,而是使用下列子類之一:

AlertDialog

此對話方塊可顯示標題、最多三個按鈕、可選擇項列表或自定義佈局。

DatePickerDialog 或 TimePickerDialog

此對話方塊帶有允許使用者選擇日期或時間的預定義 UI。

避免使用 ProgressDialog

Android 包括另一種名為 ProgressDialog 的對話方塊類,可顯示具有進度條的對話方塊。不過,如需指示載入進度或不確定的進度,則應改為遵循進度和 Activity 的設計指南,並在您的佈局中使用 ProgressBar。

這些類定義您的對話方塊的樣式和結構,但您應該將 DialogFragment 用作對話方塊的容器。DialogFragment 類提供您建立對話方塊和管理其外觀所需的所有控制元件,而不是呼叫 Dialog 物件上的方法。

使用 DialogFragment 管理對話方塊可確保它能正確處理生命週期事件,如使用者按“返回”按鈕或旋轉螢幕時。 此外,DialogFragment 類還允許您將對話方塊的 UI 作為嵌入式元件在較大 UI 中重複使用,就像傳統 Fragment 一樣(例如,當您想讓對話方塊 UI 在大螢幕和小螢幕上具有不同外觀時)。

注:由於 DialogFragment 類最初是通過 Android 3.0(API 級別 11)新增的,因此本文描述的是如何使用支援庫附帶的 DialogFragment 類。 通過將該庫新增到您的應用,您可以在執行 Android 1.6 或更高版本的裝置上使用 DialogFragment 以及各種其他 API。如果您的應用支援的最低版本是 API 級別 11 或更高版本,則可使用 DialogFragment 的框架版本,但請注意,本文中的連結適用於支援庫 API。 使用支援庫時,請確保您匯入的是 android.support.v4.app.DialogFragment 類,而不是 android.app.DialogFragment。

建立對話方塊片段

您可以完成各種對話方塊設計—包括自定義佈局以及對話方塊設計指南中描述的佈局—通過擴充套件 DialogFragment 並在 onCreateDialog() 回撥方法中建立 AlertDialog。

例如,以下是一個在 DialogFragment 內管理的基礎 AlertDialog:

public class FireMissilesDialogFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Use the Builder class for convenient dialog construction
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(R.string.dialog_fire_missiles)
               .setPositiveButton(R.string.fire, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // FIRE ZE MISSILES!
                   }
               })
               .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // User cancelled the dialog
                   }
               });
        // Create the AlertDialog object and return it
        return builder.create();
    }
}

這裡寫圖片描述
圖 1. 一個包含訊息和兩個操作按鈕的對話方塊。

現在,當您建立此類的例項並呼叫該物件上的 show() 時,對話方塊將如圖 1 所示。

下文將詳細描述如何使用 AlertDialog.Builder API 建立對話方塊。

根據對話方塊的複雜程度,您可以在 DialogFragment 中實現各種其他回撥方法,包括所有基礎 片段生命週期方法。

構建提醒對話方塊

您可以通過 AlertDialog 類構建各種對話方塊設計,並且該類通常是您需要的唯一對話方塊類。如圖 2 所示,提醒對話方塊有三個區域:

這裡寫圖片描述

圖 2. 對話方塊的佈局。

標題

這是可選項,只應在內容區域被詳細訊息、列表或自定義佈局佔據時使用。 如需陳述的是一條簡單訊息或問題(如圖 1 中的對話方塊),則不需要標題。

內容區域

它可以顯示訊息、列表或其他自定義佈局。

操作按鈕

對話方塊中的操作按鈕不應超過三個。

AlertDialog.Builder 類提供的 API 允許您建立具有這幾種內容(包括自定義佈局)的 AlertDialog。

要想構建 AlertDialog,請執行以下操作:

// 1. Instantiate an AlertDialog.Builder with its constructor
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

// 2. Chain together various setter methods to set the dialog characteristics
builder.setMessage(R.string.dialog_message)
       .setTitle(R.string.dialog_title);

// 3. Get the AlertDialog from create()
AlertDialog dialog = builder.create();

以下主題介紹如何使用 AlertDialog.Builder 類定義各種對話方塊屬性。

新增按鈕

要想新增如圖 2 所示的操作按鈕,請呼叫 setPositiveButton() 和 setNegativeButton() 方法:

AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
// Add the buttons
builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
               // User clicked OK button
           }
       });
builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialog, int id) {
               // User cancelled the dialog
           }
       });
// Set other dialog properties
...

// Create the AlertDialog
AlertDialog dialog = builder.create();

set…Button() 方法需要一個按鈕標題(由字串資源提供)和一個 DialogInterface.OnClickListener,後者用於定義使用者按下該按鈕時執行的操作。

您可以新增三種不同的操作按鈕:

肯定
您應該使用此按鈕來接受並繼續執行操作(“確定”操作)。

否定
您應該使用此按鈕來取消操作。

中性
您應該在使用者可能不想繼續執行操作,但也不一定想要取消操作時使用此按鈕。 它出現在肯定按鈕和否定按鈕之間。 例如,實際操作可能是“稍後提醒我”。

對於每種按鈕型別,您只能為 AlertDialog 新增一個該型別的按鈕。也就是說,您不能新增多個“肯定”按鈕。

這裡寫圖片描述

圖 3. 一個包含標題和列表的對話方塊。

新增列表

可通過 AlertDialog API 提供三種列表:

  • 傳統單選列表
  • 永久性單選列表(單選按鈕)
  • 永久性多選列表(核取方塊)

要想建立如圖 3 所示的單選列表,請使用 setItems() 方法:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle(R.string.pick_color)
           .setItems(R.array.colors_array, new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int which) {
               // The 'which' argument contains the index position
               // of the selected item
           }
    });
    return builder.create();
}

由於列表出現在對話方塊的內容區域,因此對話方塊無法同時顯示訊息和列表,您應該通過 setTitle() 為對話方塊設定標題。要想指定列表項,請呼叫setItems() 來傳遞一個陣列。或者,您也可以使用 setAdapter() 指定一個列表。 這樣一來,您就可以使用 ListAdapter 以動態資料(如來自資料庫的資料)支援列表。

如果您選擇通過 ListAdapter 支援列表,請務必使用 Loader,以便內容以非同步方式載入。使用介面卡構建佈局和載入程式指南中對此做了進一步描述。

注:預設情況下,觸控列表項會清除對話方塊,除非您使用的是下列其中一種永久性選擇列表。

這裡寫圖片描述

圖 4. 多選項列表。

新增永久性多選列表或單選列表

要想新增多選項(核取方塊)或單選項(單選按鈕)列表,請分別使用 setMultiChoiceItems() 或 setSingleChoiceItems() 方法。

例如,以下示例展示瞭如何建立如圖 4 所示的多選列表,將選定項儲存在一個 ArrayList 中:

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    mSelectedItems = new ArrayList();  // Where we track the selected items
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // Set the dialog title
    builder.setTitle(R.string.pick_toppings)
    // Specify the list array, the items to be selected by default (null for none),
    // and the listener through which to receive callbacks when items are selected
           .setMultiChoiceItems(R.array.toppings, null,
                      new DialogInterface.OnMultiChoiceClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int which,
                       boolean isChecked) {
                   if (isChecked) {
                       // If the user checked the item, add it to the selected items
                       mSelectedItems.add(which);
                   } else if (mSelectedItems.contains(which)) {
                       // Else, if the item is already in the array, remove it
                       mSelectedItems.remove(Integer.valueOf(which));
                   }
               }
           })
    // Set the action buttons
           .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int id) {
                   // User clicked OK, so save the mSelectedItems results somewhere
                   // or return them to the component that opened the dialog
                   ...
               }
           })
           .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int id) {
                   ...
               }
           });

    return builder.create();
}

儘管傳統列表和具有單選按鈕的列表都能提供“單選”操作,但如果您想持久儲存使用者的選擇,則應使用 {@linkandroid.app.AlertDialog.Builder#setSingleChoiceItems(int,int,DialogInterface.OnClickListener) setSingleChoiceItems()}。也就是說,如果稍後再次開啟對話方塊時系統應指示使用者的當前選擇,那麼您就需要建立一個具有單選按鈕的列表。

建立自定義佈局

這裡寫圖片描述

圖 5. 自定義對話方塊佈局。

如果您想讓對話方塊具有自定義佈局,請建立一個佈局,然後通過呼叫 AlertDialog.Builder 物件上的 setView() 將其新增到 AlertDialog。

預設情況下,自定義佈局會填充對話方塊視窗,但您仍然可以使用 AlertDialog.Builder 方法來新增按鈕和標題。

例如,以下是圖 5 中對話方塊的佈局檔案:

res/layout/dialog_signin.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <ImageView
        android:src="@drawable/header_logo"
        android:layout_width="match_parent"
        android:layout_height="64dp"
        android:scaleType="center"
        android:background="#FFFFBB33"
        android:contentDescription="@string/app_name" />
    <EditText
        android:id="@+id/username"
        android:inputType="textEmailAddress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_marginBottom="4dp"
        android:hint="@string/username" />
    <EditText
        android:id="@+id/password"
        android:inputType="textPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginLeft="4dp"
        android:layout_marginRight="4dp"
        android:layout_marginBottom="16dp"
        android:fontFamily="sans-serif"
        android:hint="@string/password"/>
</LinearLayout>

提示:預設情況下,當您將 EditText 元素設定為使用 “textPassword” 輸入型別時,字型系列將設定為固定寬度。因此,您應該將其字型系列更改為 “sans-serif”,以便兩個文字欄位都使用匹配的字型樣式。

要擴充套件 DialogFragment 中的佈局,請通過 getLayoutInflater() 獲取一個 LayoutInflater 並呼叫 inflate(),其中第一個引數是佈局資源 ID,第二個引數是佈局的父檢視。然後,您可以呼叫 setView() 將佈局放入對話方塊。

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    // Get the layout inflater
    LayoutInflater inflater = getActivity().getLayoutInflater();

    // Inflate and set the layout for the dialog
    // Pass null as the parent view because its going in the dialog layout
    builder.setView(inflater.inflate(R.layout.dialog_signin, null))
    // Add action buttons
           .setPositiveButton(R.string.signin, new DialogInterface.OnClickListener() {
               @Override
               public void onClick(DialogInterface dialog, int id) {
                   // sign in the user ...
               }
           })
           .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
               public void onClick(DialogInterface dialog, int id) {
                   LoginDialogFragment.this.getDialog().cancel();
               }
           });
    return builder.create();
}

提示:如果您想要自定義對話方塊,可以改用對話方塊的形式顯示 Activity,而不是使用 Dialog API。 只需建立一個 Activity,並在 清單檔案元素中將其主題設定為 Theme.Holo.Dialog:

<activity android:theme="@android:style/Theme.Holo.Dialog" >

就這麼簡單。Activity 現在會顯示在一個對話方塊視窗中,而非全屏顯示。

將事件傳遞迴對話方塊的宿主

當使用者觸控對話方塊的某個操作按鈕或從列表中選擇某一項時,您的 DialogFragment 可能會自行執行必要的操作,但通常您想將事件傳遞給開啟該對話方塊的 Activity 或片段。 為此,請定義一個介面,為每種點選事件定義一種方法。然後在從該對話方塊接收操作事件的宿主元件中實現該介面。

例如,以下 DialogFragment 定義了一個介面,通過該介面將事件傳回給宿主 Activity:

public class NoticeDialogFragment extends DialogFragment {

    /* The activity that creates an instance of this dialog fragment must
     * implement this interface in order to receive event callbacks.
     * Each method passes the DialogFragment in case the host needs to query it. */
    public interface NoticeDialogListener {
        public void onDialogPositiveClick(DialogFragment dialog);
        public void onDialogNegativeClick(DialogFragment dialog);
    }

    // Use this instance of the interface to deliver action events
    NoticeDialogListener mListener;

    // Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        // Verify that the host activity implements the callback interface
        try {
            // Instantiate the NoticeDialogListener so we can send events to the host
            mListener = (NoticeDialogListener) activity;
        } catch (ClassCastException e) {
            // The activity doesn't implement the interface, throw exception
            throw new ClassCastException(activity.toString()
                    + " must implement NoticeDialogListener");
        }
    }
    ...
}

對話方塊的宿主 Activity 會通過對話方塊片段的建構函式建立一個對話方塊例項,並通過實現的 NoticeDialogListener 介面接收對話方塊的事件:

public class MainActivity extends FragmentActivity
                          implements NoticeDialogFragment.NoticeDialogListener{
    ...

    public void showNoticeDialog() {
        // Create an instance of the dialog fragment and show it
        DialogFragment dialog = new NoticeDialogFragment();
        dialog.show(getSupportFragmentManager(), "NoticeDialogFragment");
    }

    // The dialog fragment receives a reference to this Activity through the
    // Fragment.onAttach() callback, which it uses to call the following methods
    // defined by the NoticeDialogFragment.NoticeDialogListener interface
    @Override
    public void onDialogPositiveClick(DialogFragment dialog) {
        // User touched the dialog's positive button
        ...
    }

    @Override
    public void onDialogNegativeClick(DialogFragment dialog) {
        // User touched the dialog's negative button
        ...
    }
}
由於宿主 Activity 會實現 NoticeDialogListener—由以上顯示的 onAttach() 回撥方法強制執行 — 因此對話方塊片段可以使用介面回撥方法向 Activity 傳遞點選事件:

public class NoticeDialogFragment extends DialogFragment {
    ...

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Build the dialog and set up the button click handlers
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(R.string.dialog_fire_missiles)
               .setPositiveButton(R.string.fire, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // Send the positive button event back to the host activity
                       mListener.onDialogPositiveClick(NoticeDialogFragment.this);
                   }
               })
               .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       // Send the negative button event back to the host activity
                       mListener.onDialogNegativeClick(NoticeDialogFragment.this);
                   }
               });
        return builder.create();
    }
}

顯示對話方塊

如果您想顯示對話方塊,請建立一個 DialogFragment 例項並呼叫 show(),以傳遞對話方塊片段的 FragmentManager 和標記名稱。

您可以通過從 FragmentActivity 呼叫 getSupportFragmentManager() 或從 Fragment 呼叫 getFragmentManager() 來獲取 FragmentManager。例如:

public void confirmFireMissiles() {
    DialogFragment newFragment = new FireMissilesDialogFragment();
    newFragment.show(getSupportFragmentManager(), "missiles");
}

第二個引數 “missiles” 是系統用於儲存片段狀態並在必要時進行恢復的唯一標記名稱。 該標記還允許您通過呼叫 findFragmentByTag() 獲取片段的控制程式碼。

全屏顯示對話方塊或將其顯示為嵌入式片段

您可能採用以下 UI 設計:您想讓一部分 UI 在某些情況下顯示為對話方塊,但在其他情況下全屏顯示或顯示為嵌入式片段(也許取決於裝置使用大螢幕還是小螢幕)。DialogFragment 類便具有這種靈活性,因為它仍然可以充當嵌入式 Fragment。

但在這種情況下,您不能使用 AlertDialog.Builder 或其他 Dialog 物件來構建對話方塊。如果您想讓 DialogFragment 具有嵌入能力,則必須在佈局中定義對話方塊的 UI,然後在 onCreateView() 回撥中載入佈局。

以下示例 DialogFragment 可以顯示為對話方塊或嵌入式片段(使用名為 purchase_items.xml 的佈局):

public class CustomDialogFragment extends DialogFragment {
    /** The system calls this to get the DialogFragment's layout, regardless
        of whether it's being displayed as a dialog or an embedded fragment. */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // Inflate the layout to use as dialog or embedded fragment
        return inflater.inflate(R.layout.purchase_items, container, false);
    }

    /** The system calls this only when creating the layout in a dialog. */
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // The only reason you might override this method when using onCreateView() is
        // to modify any dialog characteristics. For example, the dialog includes a
        // title by default, but your custom layout might not need it. So here you can
        // remove the dialog title, but you must call the superclass to get the Dialog.
        Dialog dialog = super.onCreateDialog(savedInstanceState);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        return dialog;
    }
}

以下程式碼可根據螢幕尺寸決定將片段顯示為對話方塊還是全屏 UI:

public void showDialog() {
    FragmentManager fragmentManager = getSupportFragmentManager();
    CustomDialogFragment newFragment = new CustomDialogFragment();

    if (mIsLargeLayout) {
        // The device is using a large layout, so show the fragment as a dialog
        newFragment.show(fragmentManager, "dialog");
    } else {
        // The device is smaller, so show the fragment fullscreen
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        // For a little polish, specify a transition animation
        transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        // To make it fullscreen, use the 'content' root view as the container
        // for the fragment, which is always the root view for the activity
        transaction.add(android.R.id.content, newFragment)
                   .addToBackStack(null).commit();
    }
}

如需瞭解有關執行片段事務的詳細資訊,請參閱片段指南。

在本示例中,mIsLargeLayout 布林值指定當前裝置是否應該使用應用的大布局設計(進而將此片段顯示為對話方塊,而不是全屏顯示)。 設定這種布林值的最佳方法是宣告一個布林資源值,其中包含適用於不同螢幕尺寸的備用資源值。 例如,以下兩個版本的布林資源適用於不同的螢幕尺寸:

res/values/bools.xml
<!-- Default boolean values -->
<resources>
    <bool name="large_layout">false</bool>
</resources>
res/values-large/bools.xml
<!-- Large screen boolean values -->
<resources>
    <bool name="large_layout">true</bool>
</resources>

然後,您可以在 Activity 的 onCreate() 方法執行期間初始化 mIsLargeLayout 值:

boolean mIsLargeLayout;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mIsLargeLayout = getResources().getBoolean(R.bool.large_layout);
}

將 Activity 顯示為大螢幕上的對話方塊

相對於在小螢幕上將對話方塊顯示為全屏 UI,您可以通過在大螢幕上將 Activity 顯示為對話方塊來達到相同的效果。您選擇的方法取決於應用設計,但當應用已經針對小螢幕進行設計,而您想要通過將短生存期 Activity 顯示為對話方塊來改善平板電腦體驗時,將 Activity 顯示為對話方塊往往很有幫助。

要想僅在大螢幕上將 Activity 顯示為對話方塊,請將 Theme.Holo.DialogWhenLarge 主題應用於 activity 清單檔案元素:

<activity android:theme="@android:style/Theme.Holo.DialogWhenLarge" >

如需瞭解有關通過主題設定 Activity 樣式的詳細資訊,請參閱樣式和主題指南。

清除對話方塊

當使用者觸控使用 AlertDialog.Builder 建立的任何操作按鈕時,系統會為您清除對話方塊。

系統還會在使用者觸控某個對話方塊列表項時清除對話方塊,但列表使用單選按鈕或核取方塊時除外。 否則,您可以通過在 DialogFragment 上呼叫dismiss() 來手動清除對話方塊。

如需在對話方塊消失時執行特定操作,則可以在您的 DialogFragment 中實現 onDismiss() 方法。

您還可以取消對話方塊。這是一個特殊事件,它表示使用者顯式離開對話方塊,而不完成任務。 如果使用者按“返回”按鈕,觸控對話方塊區域外部的螢幕,或者您在 Dialog 上顯式呼叫 cancel()(例如,為了響應對話方塊中的“取消”按鈕),就會發生這種情況。

如上例所示,您可以通過在您的 DialogFragment 類中實現onCancel() 來響應取消事件。

注:系統會在每個呼叫 onCancel() 回撥的事件發生時立即呼叫 onDismiss()。不過,如果您呼叫 Dialog.dismiss() 或 DialogFragment.dismiss(),系統會呼叫 onDismiss(),而不會呼叫 onCancel()。因此,當使用者在對話方塊中按“肯定”按鈕,從檢視中移除對話方塊時,您通常應該呼叫 dismiss()。

相關文章