本文會不按期更新,推薦watch下項目。css
若是喜歡請star,若是以爲有紕漏請提交issue,若是你有更好的點子能夠提交pull request。html
本文的示例代碼主要是基於EasyDialog這個庫編寫的,若你有其餘的技巧和方法能夠參與進來一塊兒完善這篇文章。java
本文固定鏈接:github.com/tianzhijiex…react
有一個統一的dialog樣式對於app是極其重要的,這種統一規範越早作越好。業務開發者應該去調用統一封裝好的java api,不要隨意定義本身順手的類。固然對於特殊的業務,咱們能夠在可控的狀況下擴展基礎的dialog api,而擴展性也是基礎api所需具有的能力之一。android
實際中的不少項目都會封裝原生dialog以知足自定義的需求,但其實原生dialog的設計已經將style和java邏輯徹底分離了,是一個標準的html+css的作法,並且還提供了詳細的配置方案,能夠說是十分完整了。本章將從源碼入手,幫助你們從新認識dialog,但願能夠幫助讀者找到最簡單、最便捷的自定義dialog寫法。ios
題外話:git
當業務開發者開始拋棄項目的基礎api而定義自定義類的時候,通常是由於基礎api的易用度、穩定性和擴展性出了問題。程序員
不管是大型項目仍是小型項目,設計給出的對話框樣式必然是變幻無窮的,甚至一個項目裏有三種以上的樣式都不足爲奇。經過長期的工做發現,下列問題廣泛存在於各個項目中:github
既然寫代碼的原則是能少些就小寫,能用穩定的android代碼則用,那麼咱們天然但願能夠利用原生的api來實現高擴展性的自定義的dialog,這也是咱們須要瞭解源碼的重要緣由。後端
知道如何造輪子才能更好的用輪子,因此咱們先來看看android中古老的dialog類。不管是support包中的alertDialog仍是android sdk自帶的datePickerDialog,他們都是繼承自Dialog這個類:
Dialog的顯示就是下面三行代碼:
Dialog dialog=new Dialog(MainActivity.this); // 建立
dialog.setContentView(R.layout.dialog); // 設置view
dialog.show(); // 展現
複製代碼
由於dialog是動態建立的,因此咱們能夠猜測是view被動態掛載到了window上,下面來簡單分析下初始化的過程。
Dialog(Context context, int themeResId, boolean createContextThemeWrapper) {
// 1. 設置context,必須是activity
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
// 去當前activity的theme中檢索dialogTheme,用來渲染view的樣式
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
// 2. 設置window,本質上是一個phoneWindow對象
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
// 創建事件的統一處理器,方便處理全部的事件
mListenersHandler = new ListenersHandler(this);
}
複製代碼
第一步是context的初始化,從上面的代碼可知dialog展現的時候須要主題資源,也就是contextThemeWrapper,也就是說這個context對象也只能是activity了。Dialog會根據當前activity的theme來獲得dialog本身的theme,這裏的樣式id就是dialogTheme
。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="dialogTheme">@style/Theme.Dialog</item>
</style>
複製代碼
爲了幫助你們理解context在不一樣場景下的具體對象,故給出下表:
關於NO上標的解釋:
注:contentProvider、broadcastReceiver之因此在上述表格中,是由於在其內部有一個context對象。
圖片來源:Android Context 上下文 你必須知道的一切
第二步是對window的操做,這裏再次貼一下關鍵代碼:
// context是activity,因此這裏須要activity的windowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext); // 創建phoneWindow
w.setOnWindowDismissedCallback(this); // 設置監聽
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel(); // 設置監聽
}
});
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this); // 設置監聽
複製代碼
這裏的回調監有關於window的,也有關於dialog的listenersHandler,這個handler會被用來處理dialog的三個重要事件:顯示、取消和關閉。
private static final class ListenersHandler extends Handler {
// 弱引用
private final WeakReference<DialogInterface> mDialog;
public ListenersHandler(Dialog dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DISMISS:
((OnDismissListener) msg.obj).onDismiss(mDialog.get());
break;
case CANCEL:
((OnCancelListener) msg.obj).onCancel(mDialog.get());
break;
case SHOW:
((OnShowListener) msg.obj).onShow(mDialog.get());
break;
}
}
}
複製代碼
接着咱們來看設置view的代碼,既然在初始化的時候獲得了當前phoneWindow這個window對象,那麼掛載view的重任天然就交給它了:
public void setContentView(@LayoutRes int layoutResID) {
mWindow.setContentView(layoutResID);
}
public void setContentView(@NonNull View view) {
mWindow.setContentView(view);
}
public void setContentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
mWindow.setContentView(view, params);
}
public void setTitle(@Nullable CharSequence title) {
mWindow.setTitle(title);
mWindow.getAttributes().setTitle(title);
}
public @Nullable View getCurrentFocus() {
return mWindow != null ? mWindow.getCurrentFocus() : null;
}
複製代碼
能夠這麼說,正如intent是bundle的封裝同樣,dialog是window的封裝。Dialog的不少public方法都是對內部的window的操做,好比dialog.setTitle()、dialog.getCurrentFocus()等。
顯示一個dialog過程其實就是將上一步設置的view掛載到window的過程,在方法執行完畢後dialog會發送一個已經show的信號,用來標記當前dialog的狀態和觸發監聽事件。
public void show() {
dispatchOnCreate(null); // 執行onCreate()
onStart(); // 調用onStart方法
//獲取DecorView對象實例
mDecor = mWindow.getDecorView();
WindowManager.LayoutParams l = mWindow.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
mWindowManager.addView(mDecor, l); // 將decorView添加到window上(關鍵方法)
mShowing = true;
sendShowMessage(); // 發送「顯示」的信號,調用相關的listener
}
複製代碼
相對的還有dismiss()方法,關閉後會發送一個dismiss的信號:
@Override
public void dismiss() {
if (Looper.myLooper() == mHandler.getLooper()) {
dismissDialog();
} else {
mHandler.post(mDismissAction);
}
}
void dismissDialog() {
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop(); // 執行onStop()
mShowing = false;
sendDismissMessage(); // 發送dismiss信號
}
}
複製代碼
這裏順便提一下,android中只要涉及到view的展現,那必然會有view數據保存的問題。在dialog中也提供了相似於activity的數據保存方法,下面這兩個方法會自動保存和恢復dialog中view的各類狀態。
public Bundle onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putBoolean(DIALOG_SHOWING_TAG, mShowing);
if (mCreated) {
bundle.putBundle(DIALOG_HIERARCHY_TAG, mWindow.saveHierarchyState());
}
return bundle;
}
複製代碼
恢復狀態:
public void onRestoreInstanceState(Bundle savedInstanceState) {
Bundle dialogHierarchyState = savedInstanceState.getBundle(DIALOG_HIERARCHY_TAG);
if (dialogHierarchyState == null) {
// dialog has never been shown, or onCreated, nothing to restore.
return;
}
dispatchOnCreate(savedInstanceState);
mWindow.restoreHierarchyState(dialogHierarchyState);
if (savedInstanceState.getBoolean(DIALOG_SHOWING_TAG)) {
show();
}
}
複製代碼
這兩個方法最終會被外部進行調用,而調用的地方一般是activity或dialogFragment,從以前的知識可知fragment的數據保存是經過activity來作的,因此dialog保存的數據從本質上是存在activity的bundle中的。
Activity中的performSaveInstanceState():
final void performSaveInstanceState(Bundle outState) {
onSaveInstanceState(outState); // 保存activity自身的數據
saveManagedDialogs(outState); // 保存dialog
mActivityTransitionState.saveState(outState);
}
複製代碼
通常狀況下咱們不用過多的考慮數據保存的問題,由於系統提供的view都已經幫咱們處理好了。但若是你的dialog中有自定義view,若自定義view中你並無處理view的onSaveInstanceState(),那麼旋轉後的dialog中的數據頗有可能不會如你想象的同樣保留下來。關於如何處理自定義view的狀態,能夠參考《Android中正確保存view的狀態》。
(圖示代表dialog類僅僅提供的是一塊白板)
由於google官方建議不要直接使用progressDialog和dialog類,因此咱們一般用的是alertDialog。能夠說dialog提供了一個基礎的空白畫板,而alertDialog則會用一些title、message等對其進行填充,實現了一個基本的樣式。
AlertDialog是dialog的子類,經過其構造方法可知全部的重要邏輯都是交給alertController進行代理的:
protected AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, resolveDialogTheme(context, themeResId));
mAlert = new AlertController(getContext(), this, getWindow());
}
複製代碼
AlertDialog和appCompatActivity同樣使用了代理模式,下面咱們就來看看這個alertController到底作了些什麼事情。
從android官網可知alertDialog提供了以下樣式:
關於ui方面,相信你們都十分熟悉了,這裏就不貼圖說明了。咱們在alertController的構造中能夠看到上述樣式的屬性id,也就是說這些佈局最終會被填充到dialog提供的白板中,而佈局文件中的view則是最終數據展現的載體。
public AlertController(Context context, AppCompatDialog di, Window window) {
final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
R.attr.alertDialogStyle, 0);
mAlertDialogLayout = a.getResourceId(R.styleable.AlertDialog_android_layout, 0);
mButtonPanelSideLayout = a.getResourceId(
R.styleable.AlertDialog_buttonPanelSideLayout, 0);
mListLayout = a.getResourceId(R.styleable.AlertDialog_listLayout, 0);
mMultiChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_multiChoiceItemLayout, 0);
mSingleChoiceItemLayout = a.getResourceId(
R.styleable.AlertDialog_singleChoiceItemLayout, 0);
mListItemLayout = a.getResourceId(R.styleable.AlertDialog_listItemLayout, 0);
a.recycle();
/* We use a custom title so never request a window title */
di.supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
}
複製代碼
上述代碼中初始化了以下viewGroup,具體的佈局和樣式隨着activity的theme的不一樣而不一樣。
佈局對象 | style | 提供的view |
---|---|---|
mAlertDialogLayout | AlertDialog_android_layout | title、message、button和容器 |
mButtonPanelSideLayout | AlertDialog_buttonPanelSideLayout | 三個按鈕在右側的容器 |
mListLayout | AlertDialog_listLayout | AlertController.RecycleListView |
mMultiChoiceItemLayout | AlertDialog_multiChoiceItemLayout | CheckedTextView |
mSingleChoiceItemLayout | AlertDialog_singleChoiceItemLayout | CheckedTextView |
mListItemLayout | AlertDialog_listItemLayout | TextView |
詳細的佈局能夠參考源碼中theme的定義:
<style name="Base.AlertDialog.AppCompat" parent="android:Widget">
<item name="android:layout">@layout/abc_alert_dialog_material</item>
<item name="listLayout">@layout/abc_select_dialog_material</item>
<item name="listItemLayout">@layout/select_dialog_item_material</item>
<item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
<item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
<item name="buttonIconDimen">@dimen/abc_alert_dialog_button_dimen</item>
</style>
<style name="AlertDialog.Leanback" parent="AlertDialog.Material">
<item name="buttonPanelSideLayout">@android:layout/alert_dialog_leanback_button_panel_side</item>
</style>
複製代碼
從代碼來看,下圖中框起來的是一個叫作customPanel的frameLayout,是自定義佈局的容器:
主佈局文件,即android:layout定義的佈局文件,裏面提供了icon、title、message、button來給dialog這個空白的畫板增長了最基本的元素。不管是自定義佈局仍是android提供的單選或多選列表,他們都是將本身的佈局文件add到了主佈局文件的customPanel中。
下圖爲單選對話框的佈局結構,能夠看見listView被add到了id爲customPanel的frameLayout中:
builder = new AlertDialog.Builder(this);
builder.setIcon(R.mipmap.ic_launcher);
builder.setTitle(R.string.simple_list_dialog);
String[] Items = {"one","two","three"};
builder.setItems(Items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// ...
}
});
builder.setCancelable(true);
AlertDialog dialog = builder.create();
dialog.show();
複製代碼
咱們如今知道了全部的佈局都是經過alertController來設置的,那麼給這些佈局中的view填充的數據則是alertDialog.Builder的工做了。
由於dialog中的每一個數據都是能夠獨立存在的,對於構建這種有大量可選數據的對象,咱們在java中通常會經過builder模式去創建它,而android中的dialog則是一個教科書般的例子。
public static class Builder {
private final AlertController.AlertParams P; // 重要的參數
public Builder(Context context, int themeResId) {
// new出P對象
P = new AlertController.AlertParams(new ContextThemeWrapper(
context, resolveDialogTheme(context, themeResId)));
}
public Builder setTitle(@StringRes int titleId) {
P.mTitle = P.mContext.getText(titleId);
return this;
}
// ...
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
// 將P中的數據塞入alertDialog
P.apply(dialog.mAlert); // dialog.mAlert爲alertController對象
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
return dialog;
}
}
複製代碼
經過上述代碼咱們能夠知道builder中全部的數據最終都會存放在P中,而這個P(alertParams)就是alertDialog的全部數據參數的聚合。在alertDialog.Builder.create()中,P.apply()執行了最終的裝配工做,將數據分別設置到了dialog的各個view中,讓其有了title、icon等信息。
public AlertParams(Context context) {
this.mContext = context;
this.mCancelable = true;
this.mInflater = (LayoutInflater)context.getSystemService("layout_inflater");
}
public void apply(AlertController dialog) {
// 由於alertController管理了各類佈局文件
// 因此經過alertController來將數據設置給各個view
if (this.mTitle != null) {
dialog.setTitle(this.mTitle);
}
if (this.mIcon != null) {
dialog.setIcon(this.mIcon);
}
// ...
}
複製代碼
總結:
AlertController承擔了管理dialog中全部view的工做,alertController中的alertParams承擔了數據的聚合工做。AlertParams經過apply()讓alertController將數據和視圖進行綁定,展現出一個完整的alertDialog。
題外話:
AlertDialog自身的theme是經過
alertDialogTheme
進行設置的,咱們能夠在style中經過設置以下的屬性來定義它的樣式。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar" >
<item name="alertDialogTheme">@style/Theme.Dialog.Alert</item>
</style>
複製代碼
new AlertDialog.Builder(this)
.setTitle("title")
.setIcon(R.drawable.ic_launcher)
.setPositiveButton("好", new positiveListener())
.setNeutralButton("中", new NeutralListener())
.setNegativeButton("差", new NegativeListener())
.creat()
.show();
複製代碼
通常的alertDialog用法如上,但若是咱們想要對傳入的參數作校驗和判空呢,若是想要作一些通用的背景設置呢?
若是咱們更進一步,作一個如上圖所示的自定義dialog。那麼咱們須要綁定自定義view,甚至可能會進行網絡的請求。若是用alertDialog作,上述的代碼都須要在activity中完成,這會讓activity的代碼變得混亂不堪,讓dialog失去內聚性。
一個在activity中寫邏輯的糟糕例子:
public class MainActivity extends AppCompatActivity {
EditText inputTextEt; // dialog中的editText
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("title")
.setView(R.layout.dialog_input_layout)
.setCancelable(false)
.show();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
inputTextEt = ((AlertDialog)dialog).findViewById(R.id.input_et);
inputTextEt.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// 執行邏輯代碼
return false;
}
});
}
});
}
}
複製代碼
若是更加極端一些,屏幕方向發生變化後,activity會重建,以前顯示的對話框就不見了,查看log能夠發現以下異常:
04-1917:30:06.999: E/WindowManager(14495): Activitycom.example.androidtest.MainActivity has leaked windowcom.android.internal.policy.impl.PhoneWindow$DecorView{42ca3c18 V.E.....R....... 0,0-1026,414} that was originally added here
綜上所述,alertDialog已經不能知足現今的複雜需求了,咱們能夠考慮創建一個幫助類來解決一些問題:
public class DialogHelper {
private String title, msg;
/**
* 各類自定義參數,如:title
*/
public void setTitle(String title) {
this.title = title;
}
/**
* 各類自定義參數,如:message
*/
public void setMsg(String msg) {
this.msg = msg;
}
public void show(Context context) {
// 經過配置的參數來創建一個dialog
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(msg)
.create();
// ...
// 通用的設置
Window window = dialog.getWindow();
window.setBackgroundDrawable(new ColorDrawable(0xffffffff)); // 白色背景
dialog.show();
}
}
複製代碼
幫助類的出現解決了重複代碼過多和內聚性差的問題,可是仍舊沒有解決dialog數據保存和生命週期管理等問題。Google在android 3.0的時候引入了一個新的類dialogFragment,如今咱們徹底可使用dialogFragment做一個controller來管理alertDialog,省去了創建幫助類的麻煩。
能夠這麼說,alertDialog被dialogFragment管理,dialogFragment被fragmentManager管理,fragmentManager被fragmentActivity調用。
各自的工做以下:
目前官方推薦使用dialogFragment來管理對話框,因此它可確保能正確的處理生命週期事件。DialogFragment就是一個fragment,當用戶旋轉屏幕時仍舊是fragment的執行流程。
旋轉屏幕時的log:
04-1917:45:41.289: D/==========(16156): MyDialogFragment : onAttach
04-1917:45:41.299: D/==========(16156): MyDialogFragment : onCreate
04-1917:45:41.299: D/==========(16156): MyDialogFragment : onCreateView
04-1917:45:41.309: D/==========(16156): MyDialogFragment : onStart
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onStop
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onDestroyView
04-1917:45:50.619: D/==========(16156): MyDialogFragment : onDetach
04-1917:45:50.639: D/==========(16156): MyDialogFragment : onAttach
04-1917:45:50.639: D/==========(16156): MyDialogFragment : onCreate
04-1917:45:50.659: D/==========(16156): MyDialogFragment : onCreateView
04-1917:45:50.659: D/==========(16156): MyDialogFragment : onStart
複製代碼
從log可知,只要旋轉屏幕就會銷燬當前fragment,並創建一個新的fragment掛載到activity中,這天然能夠保證dialogFragment在旋轉後仍舊保留dialog,不會出現轉屏後dialog自動消失的問題。
既然談到了轉屏,那麼就要記得保存和恢復數據。DialogFragment的onActivityCreated()中會觸發mDialog.onRestoreInstanceState()
,這個就是dialog恢復數據的方法(保存方法是onSaveInstanceState())。
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final Activity activity = getActivity();
mDialog.setOwnerActivity(activity);
if (savedInstanceState != null) {
// 恢復數據
Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
if (dialogState != null) {
mDialog.onRestoreInstanceState(dialogState);
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (mDialog != null) {
Bundle dialogState = mDialog.onSaveInstanceState();
if (dialogState != null) {
// 保存數據
outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
}
}
}
複製代碼
DialogFragment的設計雖然精巧,但要知道dialogFragment和fragment是有差別的。Google官方強烈不推薦在fragment的onCreateView()中直接inflate一個佈局,推薦的作法是在onCreateDialog()中創建dialog對象。
強烈禁止的寫法:
public class MyDialogFragment extends DialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// 不要複寫dialogFragment的onCreateView(),若是非要複寫,請直接返回null
return inflater.inflate(R.layout.dialog, null);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 應該創建dialog的方法
Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("用戶申明")
.setMessage(getResources().getString(R.string.hello_world))
.setPositiveButton("我贊成", this)
.setNegativeButton("不一樣意", this)
.setCancelable(false);
//.show(); // 注意不要調用show()
return builder.create();
}
}
複製代碼
DialogFragment用了fragment的機制,簡單完成了數據的保存和恢復工做,同時又經過onCreateDialog()來創建alertDialog對象,將fragment和alertDialog結合的至關巧妙。
Dialog重要的方法是show()和dismiss(),在dialogFragment中,這兩個方法都是會被fragment間接調用的,下面咱們就來看下這兩個過程。
show()
DialogFragment的show()實際上是創建了一個fragment對象,而後執行了無容器的add()操做。Fragment啓動後會調用內部的onCreateDialog()創建真正的dialog對象,最後在onStart()中觸發dialog.show()。
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
public int show(FragmentTransaction transaction, String tag) {
mDismissed = false;
mShownByMe = true;
transaction.add(this, tag);
mViewDestroyed = false;
mBackStackId = transaction.commit();
return mBackStackId;
}
@Override
public void onStart() {
super.onStart();
if (mDialog != null) {
mViewDestroyed = false;
mDialog.show(); // 在fragment的onStart()中調用了dialog的show()
}
}
複製代碼
必須注意的是,咱們必須在onStart()以後再去執行dialog.findView()的操做,不然會出現NPE。
dismiss()
DialogFragment提供了兩個關閉的方法,分別是dismiss()和dismissAllowingStateLoss(),前者對應的是fragmentTransaction.commit(),後者對應的是fragmentTransaction.commitAllowingStateLoss()。用dismissAllowingStateLoss()的好處是可讓咱們忽略異步關閉dialog時的狀態問題,讓咱們不用考慮當前activity的狀態,這會減小不少線上的崩潰。
public void dismiss() {
dismissInternal(false);
}
public void dismissAllowingStateLoss() {
dismissInternal(true);
}
void dismissInternal(boolean allowStateLoss) {
mDismissed = true;
mShownByMe = false;
if (mDialog != null) {
mDialog.dismiss();
mDialog = null;
}
mViewDestroyed = true;
// 處理多個dialogFragment的問題
if (mBackStackId >= 0) {
getFragmentManager().popBackStack(mBackStackId,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mBackStackId = -1;
} else {
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(this); // 移除當前的fragment
if (allowStateLoss) {
ft.commitAllowingStateLoss();
} else {
ft.commit();
}
}
}
複製代碼
上述代碼也說明了dialogFragment是支持回退棧的,若是棧中有就pop出來,若是沒有就直接remove和commit。
dismiss()和cancel()的區別:
由於fragment自己就是一個複雜的管理器,不少開發者對於dialogFragment中的各類回調方法會產生理解上的誤差,經過下面的圖示能夠幫助你們更好的理解這點:
public class DemoDialog extends android.support.v4.app.DialogFragment {
private static final String TAG = "DemoDialog";
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 獲得各類外部參數
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
// 這裏返回null,讓fragment做爲一個controller
return null;
}
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 根據參數創建dialog
return new AlertDialog.Builder(getActivity())
.setTitle("title")
.setMessage("message")
.create();
}
public void setupDialog(Dialog dialog, int style) {
super.setupDialog(dialog, style);
// 上面創建好dialog後,這裏能夠進行進一步的配置操做(不推薦)
}
public void onStart() {
super.onStart();
// 這裏的view來自於onCreateView,因此是null,不要使用
View view = getView();
// 能夠在這裏進行dialog的findViewById操做
Window window = getDialog().getWindow();
view = window.getDecorView();
}
}
複製代碼
圖片來源:mmazzarolo/react-native-dialog
如圖所示,當咱們自定義的dialog中有一個editText時,咱們天然但願呼出dialog後能自動彈出輸入法,只惋惜原生並不支持這種操做。一個簡單的解決方案是在dialogFragment中的onStart()後調用以下代碼,強制彈出輸入法:
public void showInputMethod(final EditText editText) {
editText.post(new Runnable() {
@Override
public void run() {
editText.setFocusable(true);
editText.setFocusableInTouchMode(true);
editText.requestFocus();
InputMethodManager imm = (InputMethodManager)
getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
}
});
}
複製代碼
有的需求是須要從dialogA來彈出dialogB,對於這樣的需求咱們須要利用fragment的回退棧來完成。
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(DialogFragmentA.this); // 移除A,防止屏幕上顯示兩個dialog
ft.addToBackStack(null); // 讓A入棧
// ...
// 利用fragmentTransaction來展現B
dialogFragmentB.show(ft,"my_tag");
複製代碼
按照如上代碼配置後,顯示B以前會隱藏A,點擊返回鍵後B會消失,A會再次顯示出來。這裏須要注意的必須用fragmentTransaction來顯示dialog,並且兩個dialogFragment的tag不要用同一個值。
在《一個內存泄漏引起的血案》一文中,做者提到局部變量的生命週期在Dalvik VM跟ART/JVM中是有區別的。在DVM中,假如線程死循環或者阻塞,那麼線程棧幀中的局部變量若沒有被置null,那麼就不會被回收。這個設計會致使在lollipop以前使用alertDialog的時候,引發內存泄漏。
當你看到本文的時候android5.0已經成爲了主流,能夠不用考慮這個問題,但咱們仍舊須要注意下非靜態內部類持有外部類的問題。
new AlertDialog.Builder(this)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
}
}).show();
複製代碼
在activity中咱們一般會這麼寫一個dialog,這裏的onDismissListener就是一個非靜態的匿名內部類。在設置後,alertDialog會將其保存在alertParams中,最終把它設置到dialog對象上。
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
P.mOnDismissListener = onDismissListener;
return this;
}
複製代碼
創建dialog對象:
public AlertDialog create() {
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
dialog.setOnCancelListener(P.mOnCancelListener);
// 設置監聽器
dialog.setOnDismissListener(P.mOnDismissListener);
return dialog;
}
複製代碼
咱們着重關注一下dialog的setOnDismissListener(),這個方法中會將listener做爲obj設置給handler來創建一個message,而這個mssage對象會被dialog持有。
public void setOnDismissListener(@Nullable OnDismissListener listener) {
if (listener != null) {
// 設置給handler
mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
} else {
mDismissMessage = null;
}
}
複製代碼
下面是兩種可能會出現內存泄漏的狀況:
若是你的項目在線上遇到了這種問題,能夠在dialogFragment的onDestroyView()中置空監聽器,或者在dialog被移出window的時候作置空操做。
方法一:
由於dialogFragment自己就fragment,因此這裏能夠利用fragment的生命週期來作置空操做:
@Override
public void onDestroyView() {
super.onDestroyView();
positiveListener = null;
negativeListener = null;
neutralListener = null;
clickListener = null;
multiChoiceClickListener = null;
}
複製代碼
方法二:
爲了避免破壞原有的監聽器,下面用《Dialog引起的內存泄漏 - 鮑陽的博客》中提到的包裝類來解決這個問題。
public final class DetachableClickListener implements DialogInterface.OnClickListener {
public static DetachableClickListener wrap(DialogInterface.OnClickListener delegate) {
return new DetachableClickListener(delegate);
}
private DialogInterface.OnClickListener delegateOrNull;
private DetachableClickListener(DialogInterface.OnClickListener delegate) {
this.delegateOrNull = delegate;
}
public void onClick(DialogInterface dialog, int which) {
if (delegateOrNull != null) {
delegateOrNull.onClick(dialog, which);
}
}
public void clearOnDetach(Dialog dialog) {
dialog.getWindow()
.getDecorView()
.getViewTreeObserver()
.addOnWindowAttachListener(new OnWindowAttachListener() {
public void onWindowAttached() { }
public void onWindowDetached() {
delegateOrNull = null;
}
});
}
}
複製代碼
將包裝類作真正的監聽器對象:
DetachableClickListener clickListener = wrap(new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
MyActivity.this.makeCroissants();
}
});
AlertDialog dialog = new AlertDialog.Builder(this)
.setPositiveButton("Baguette", clickListener)
.create();
clickListener.clearOnDetach(dialog);//監聽窗口解除事件,手動釋放引用
dialog.show();
複製代碼
方法三:
既然咱們發現不少狀況是持有message的問題,那麼咱們爲什麼不在handlerThread空閒的時候給隊列中發送一個null的message呢,這樣就可讓其永遠不持有dialog中的任何監聽了:
static void flushStackLocalLeaks(Looper looper) {
final Handler handler = new Handler(looper);
handler.post(new Runnable() {
@Override
public void run() {
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
handler.sendMessageDelayed(handler.obtainMessage(), 1000);
return true;
}
});
}
});
}
複製代碼
默認的dialog是有一個固定的寬的,爲了和ui稿保持一致,咱們須要進行一些。咱們能夠直接在onStart()中修改window的屬性,最終完成自定義的效果。
private void setStyle() {
Window window = getDialog().getWindow();
// 無標題
getDialog().requestWindowFeature(STYLE_NO_TITLE);
// 透明背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
// 設置寬高
window.getDecorView().setPadding(0, 0, 0, 0);
WindowManager.LayoutParams wlp = window.getAttributes();
wlp.width = mWidth;
wlp.height = mHeight;
// 設置dialog出現的位置
wlp.gravity = Gravity.CENTER;
// 設置x、y軸的偏移距離
wlp.x = DensityUtil.dip2px(getDialog().getContext(), mOffsetX);
wlp.y = DensityUtil.dip2px(getDialog().getContext(), mOffsetY);
// 設置顯示和關閉時的動畫
window.setWindowAnimations(mAnimation);
window.setAttributes(wlp);
}
複製代碼
關於背景也是同理,都是對於window的操做:
private void setBackground() {
// 去除dialog的背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable());
// 白色背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(0xffffffff));
// 設置主體背景
getDialog().getWindow().setBackgroundDrawableResource(R.drawable.dialog_bg_custom);
}
複製代碼
咱們既能夠用window.setWindowAnimations(mAnimation)
直接給window設置動畫,也能夠用style文件來設置動畫。
dialog_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromYDelta="100%p"
android:toYDelta="0%p"
android:duration="200"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
複製代碼
dialog_out.xml
<?xml version="1.0" encoding="utf-8"?>
<translate
android:fromYDelta="0%p"
android:toYDelta="100%p"
android:duration="200"
xmlns:android="http://schemas.android.com/apk/res/android">
</translate>
複製代碼
定義好上述動畫文件後,只須要創建一個動畫樣式,設置給windowAnimationStyle
:
<style name="AlertDialogAnimation">
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<item name="android:windowExitAnimation">@anim/dialog_out</item>
</style>
<!-- 動畫 -->
<item name="android:windowAnimationStyle">@style/AlertDialogAnimation</item>
複製代碼
題外話:
若是是作自定義的dialog,咱們一般都會將主體背景設置爲透明,這樣方便作增長圓角和陰影等操做。
Dialog的默認邏輯是點擊任何按鈕後都會自動關閉,這個默認邏輯對於要作輸入校驗的場景就不太友好了。咱們能夠模擬一個場景:
用戶輸入文字後點擊「ok」,若是輸入的文字不符合預期,則彈出toast要求從新輸入,不然關閉
在這個場景中,要求「ok」這個button被點擊後,不會觸發默認的dismiss事件。
要修改默認的邏輯,就要先看看源碼中是怎麼處理的。AlertController中將底部的三個button都設置了一個叫作mButtonHandler
的clickListener,而mButtonHandler在響應任何事件後都會觸發dismiss操做。
private void setupButtons(ViewGroup buttonPanel) {
mButtonPositive = (Button) buttonPanel.findViewById(android.R.id.button1);
mButtonPositive.setOnClickListener(mButtonHandler);
mButtonNegative = buttonPanel.findViewById(android.R.id.button2);
mButtonNegative.setOnClickListener(mButtonHandler);
mButtonNeutral = (Button) buttonPanel.findViewById(android.R.id.button3);
mButtonNeutral.setOnClickListener(mButtonHandler);
}
複製代碼
監聽器的內部邏輯:
View.OnClickListener mButtonHandler = new View.OnClickListener() {
@Override
public void onClick(View v) {
final Message m;
if (v == mButtonPositive && mButtonPositiveMessage != null) {
// 發送positive事件
m = Message.obtain(mButtonPositiveMessage);
} else if (v == mButtonNegative && mButtonNegativeMessage != null) {
// 發送negative事件
m = Message.obtain(mButtonNegativeMessage);
} else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
// 發送neutral事件
m = Message.obtain(mButtonNeutralMessage);
}
// 任何事件最終都會觸發dismiss
mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialog)
.sendToTarget();
}
};
複製代碼
知曉了原理後,如今的思路就是替換這個listener,將其換成本身的。好比咱們要在positiveButton點擊後作一些事情,那麼就在dialogFragment的onStart()後拿到當前的dialog對象,經過dialog對象獲得這個positiveButton,爲其設置本身的監聽器。
DialogInterface有三個常量,這三個常量對應三個button對象,而咱們的「ok」就是BUTTON_POSITIVE。
public interface DialogInterface {
/** The identifier for the positive button. */
int BUTTON_POSITIVE = -1;
/** The identifier for the negative button. */
int BUTTON_NEGATIVE = -2;
/** The identifier for the neutral button. */
int BUTTON_NEUTRAL = -3;
}
複製代碼
在onStart()中經過getButton(AlertDialog.BUTTON_POSITIVE)就能夠獲得「ok按鈕」:
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
複製代碼
最終,從新設置「肯定」按鈕的監聽器,作自定義的一些邏輯:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (TextUtils.isEmpty(mInputTextEt.getText())) {
Toast.makeText(getActivity(), "請輸入內容,不然不能關閉!", Toast.LENGTH_SHORT).show();
} else {
getPositiveListener().onClick(null, AlertDialog.BUTTON_POSITIVE);
dismiss();
}
}
});
複製代碼
在有這樣需求的場景中,咱們通常都會自定義一個對話框,在這裏完成邏輯的封裝,最終調用dialogFragment的dismiss()來手動關閉對話框,將控制權緊緊把握在本身的手中。
在線上的崩潰統計中常常會看到dialogFragment的日誌:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
衆所周知,dialog是動態出現和消失的,而fragment的commit()對於activity狀態有着嚴格的校驗,一旦activity在dialog出現時已經走向了finish,那麼則必然會崩潰。具體的緣由咱們已經在fragment一章中詳細講解了,也給出了防護的策略,這裏就再也不贅述了。
一個很簡單的作法是在show()和dismiss()前進行狀態判斷,也能夠順便包一個try-cache:
public void show(FragmentManager manager, String tag) {
if (manager == null || manager.isDestroyed() || manager.isStateSaved()) {
// 防護:state異常
return;
}
try {
super.show(manager, tag);
} catch (IllegalStateException e) {
// 防護:Can not perform this action after onSaveInstanceState
e.printStackTrace();
}
}
複製代碼
換個思路來看,既然這是狀態不匹配的問題,那麼咱們爲什麼不直接忽略狀態呢,直接自定義一個dialogFragment.commitAllowingStateLoss(),雖然這不是最優的策略。
定義一個showAllowingStateLoss()方法:
public void showAllowingStateLoss(FragmentManager manager) {
manager.beginTransaction().add(this, "kale-dialog").commitAllowingStateLoss();
}
複製代碼
其實在android的源碼中,google的程序員已經準備好了一個容許忽略狀態的show方法了,只不過目前還未暴露,處於hide的階段。
android.app.DialogFragment:
DialogFragment雖然好處多多,可是其缺失了builder模式,使用起來不是很方便。咱們封裝dialogFragment的核心目的就是爲其增長一個builder,讓使用者能夠像用alertDialog那樣進行多參數的靈活配置。
封裝類應該具有的功能:
既然咱們要仿照alertDialog寫一個builder,那麼爲什麼不直接用它的builder呢,畢竟alertDIalog.Builder支持的傳參已經徹底夠用了。
AlertDialog.Builder的部分設置功能:
alertDialogBuilder.setTitle(); // 設置標題
alertDialogBuilder.setIcon(); // 設置icon
// 設置底部三個操做按鈕
alertDialogBuilder.setPositiveButton();
alertDialogBuilder.setNegativeButton();
alertDialogBuilder.setNeutralButton();
setMessage(); // 設置顯示的文案
setItems(); // 設置對話框內容爲簡單列表項
setSingleChoiceItems(); // 設置對話框內容爲單選列表項
setMultiChoiceItems(); // 設置對話框內容爲多選列表項
setAdapter(); // 設置對話框內容爲自定義列表項
setView(); // 設置對話框內容爲自定義View
// 設置對話框是否可取消
setCancelable(boolean cancelable);
setCancelListener(onCancelListener);
複製代碼
前文說到這個builder會將全部的參數放在一個叫作alertParams
的對象中,那麼咱們直接從alertParams中取參數後塞給dialogFragment就好。惋惜的是alertParams自己是public的,但它的外部類alertController倒是私有的,咱們沒法訪問。
爲了解決這個問題,咱們不得不用反射的方式來獲得這個public的alertParams(十分少見的反射場景)。爲了讓反射的代碼寫起來更加簡單,咱們須要作一個public的alertController,讓AlertController.AlertParams
寫起來不會由於找不到AlertController
這個外部類而報錯。
首先,咱們創建一個叫作provided的module,在裏面模仿support包寫一個本身的public類:
public class AlertController { // 用public修飾
public static class AlertParams {
public Context mContext;
public LayoutInflater mInflater;
public int mIconId = 0;
public Drawable mIcon;
// ...
}
}
複製代碼
而後,讓工程compileOnly依賴這個provided的module,避免類衝突:
dependencies {
compileOnly project(':provided')
}
複製代碼
最後,經過反射來獲得系統中的alertParams,也就是P對象:
AlertParams getParams() {
AlertParams P = null;
Field field = AlertDialog.Builder.class.getDeclaredField("P");
field.setAccessible(true);
P = (AlertParams) field.get(this);
return P;
}
複製代碼
這樣咱們在編寫下面代碼的時候就不會有任何報錯了:
// 由於咱們騙IDE說AlertController是public的,因此它不會出現錯誤提示
AlertController.AlertParams p = getParams();
// 獲得P中具體的值
int iconId = p.mIconId;
Sting title = p.mTitle;
String message = p.mMessage;
複製代碼
完成了主體框架後,如今來補充一些傳參的細節。咱們但願dialogFragment能夠獲得builder的全部參數,但直接傳遞alertParams對象會有訪問權限的問題,因此須要定義一個基礎模型類,假設就叫作dialogParams
:
public class DialogParams implements Serializable {
public int mIconId = 0;
public int themeResId;
public CharSequence title;
public CharSequence message;
public CharSequence positiveText;
public CharSequence neutralText;
public CharSequence negativeText;
public CharSequence[] items;
public boolean[] checkedItems;
public boolean isMultiChoice;
public boolean isSingleChoice;
public int checkedItem;
}
複製代碼
這個類其實就是alertParams對象的複製,關鍵是要讓其支持序列化,這樣才能放入bundle中。其緣由是由於dialogFragment是一個fragment,因此傳遞的參數必須是可序列化的對象。
若是咱們更進一步,想要更好的支持自定義dialog,讓自定義的dialog也能用父類builder中的參數,咱們確定要自定義一個繼承自alertDialog.Builder的builder對象,而最好方案就是寫一個「支持泛型的builder」。
自定義的builder基類:
public abstract static class Builder<T extends Builder> extends AlertDialog.Builder {
public Builder(@NonNull Context context) {
this(context);
}
@Override
public T setTitle(CharSequence title) {
return (T) super.setTitle(title);
}
@Override
public T setTitle(@StringRes int titleId) {
return (T) super.setTitle(titleId);
}
// ...
@NonNull
protected abstract EasyDialog createDialog(); // 創建子類的具體dialog
}
複製代碼
經過泛型,咱們能夠簡單的實現任意一個dialog的builder,其既可讓其擁有本身的新方法,又能夠擁有父類的基礎方法,想要最大限度利用了繼承的能力。惋惜的是alertDialog.Builder自己不支持繼承,crate()方法中已經寫死了具體的類。
/**
* Calling this method does not display the dialog. If no additional
* processing is needed, {@link #show()} may be called instead to both
* create and display the dialog.
*/
public AlertDialog create() {
// 注意不要用三參數的構造方法來構造aslertDialog
// 這裏new出了具體的alertDialog對象,不容許使用者替換實現類
final AlertDialog dialog = new AlertDialog(P.mContext, mTheme);
P.apply(dialog.mAlert);
dialog.setCancelable(P.mCancelable);
if (P.mCancelable) {
dialog.setCanceledOnTouchOutside(true);
}
dialog.setOnCancelListener(P.mOnCancelListener);
dialog.setOnDismissListener(P.mOnDismissListener);
if (P.mOnKeyListener != null) {
dialog.setOnKeyListener(P.mOnKeyListener);
}
return dialog;
}
複製代碼
這裏咱們姑且拋棄不可修改的create()方法,新增一個createDialog()
方法,在這個方法中返回子類dialog的對象,真正能實現一個可繼承的builder類。
一個自定義的builder的代碼,增長了本身的setInputText()方法:
/**
* 自定義builder來增長一些參數,記得要繼承自父類(BaseDialog)的Builder
*/
public static class Builder extends BaseDialog.Builder<Builder> {
private Bundle bundle = new Bundle();
public Builder setInputText(CharSequence text, CharSequence hint) {
bundle.putCharSequence(KEY_INPUT_TEXT, text);
bundle.putCharSequence(KEY_INPUT_HINT, hint);
return this;
}
@Override
protected InputDialog createDialog() {
// 關鍵方法!!!
InputDialog dialog = new InputDialog();
dialog.setArguments(bundle);
return dialog;
}
}
複製代碼
如今咱們擁有了以下對象:
因而能夠完成總體的流程圖:
重要的代碼邏輯:
BaseDialog dialog = createDialog(); // 1. 創建一個dialogFragment
AlertParams p = getParams(); // 2. 獲得alertParams
DialogParams params = createDialogParamsByAlertParams(p); // 3. 獲得dialogParams
Bundle bundle = new Bundle();
bundle.putSerializable(KEY_DIALOG_PARAMS, params);
dialog.setArguments(bundle); // 4. 將dialogParams傳入dialogFragment
複製代碼
如今咱們的dialogFragment中已經能夠獲得構建dialog的全部參數了,剩下的就是解析參數和真正創建alertDialog的步驟了:
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getArguments();
if (bundle != null) {
// 1. 獲得dialogParams
dialogParams = (DialogParams) bundle.getSerializable(KEY_DIALOG_PARAMS);
}
}
private Dialog onCreateDialog(@NonNull Activity activity) {
DialogParams p = dialogParams;
// 2. 將參數設置到alertDialog的builder中
AlertDialog.Builder builder = new AlertDialog.Builder(activity, p.themeResId)
.setTitle(p.title)
.setIcon(p.mIconId)
.setMessage(p.message)
.setPositiveButton(p.positiveText, positiveListener)
.setNeutralButton(p.neutralText, neutralListener)
.setNegativeButton(p.negativeText, negativeListener);
if (p.items != null) {
if (p.isMultiChoice) {
builder.setMultiChoiceItems(p.items,p.checkedItems,onMultiChoiceClickListener);
} else if (p.isSingleChoice) {
builder.setSingleChoiceItems(p.items, p.checkedItem, onClickListener);
} else {
builder.setItems(p.items, onClickListener);
}
}
return builder.create(); // 3. 創建最終的alertDialog
}
/**
* 4. 這時dialog已經建立完畢,能夠調用{@link Dialog#findViewById(int)}了
*/
public void onStart() {
super.onStart();
Window window = getDialog().getWindow();
bindAndSetViews(window != null ? window.getDecorView() : null);
}
複製代碼
至此,咱們的封裝工做已經基本完畢,這也是tianzhijiexian/EasyDialog的核心思路,下面咱們就來着重看下這個叫作easyDialog庫。
簡單來講,tianzhijiexian/EasyDialog僅僅是dialogFragment的簡單封裝庫,它提供了極其原生的api,幾乎沒有學習成本,而且將自定義dialog的步驟模板化了。在上文中咱們已經瞭解了它的核心思想,下面就來看看該怎麼使用它,而且順便講一下你們都會忽略的dialog樣式的問題。
EasyDialog充分利用了原生alertDialog.Builder的api,因此使用方式和alertDialog無異,它提供了以下四種基本的dialog。
默認對話框
EasyDialog.Builder builder = EasyDialog.builder(this); // 創建builder對象
builder.setTitle("Title")
.setIcon(R.drawable.saber)
.setMessage(R.string.hello_world)
.setOnCancelListener(dialog -> Log.d(TAG, "onCancel"))
.setOnDismissListener(dialog -> Log.d(TAG, "onDismiss"))
// 設置下方的三個按鈕
.setPositiveButton("ok", (dialog, which) -> {})
.setNegativeButton("cancel", (dialog, which) -> dialog.dismiss())
.setNeutralButton("ignore", null)
.setCancelable(true); // 點擊空白處能夠關閉
DialogFragment easyDialog = builder.build();
// 用showAllowingStateLoss()彈出
easyDialog.showAllowingStateLoss(getSupportFragmentManager());
複製代碼
簡單列表框
<string-array name="country">
<item>阿爾及利亞</item>
<item>安哥拉</item>
<item>貝寧</item>
<item>緬甸</item>
</string-array>
複製代碼
Java代碼:
EasyDialog.builder(this)
// R.array.country爲xml中定義的string數組
.setItems(R.array.country, (dialog, which) -> showToast("click " + which))
.setPositiveButton("yes", null)
.setNegativeButton("no", null)
.build()
.show(getSupportFragmentManager());
複製代碼
單選列表框
EasyDialog dialog = EasyDialog.builder(this)
.setTitle("Single Choice Dialog")
// 這裏傳入的「1」表示默認選擇第二個選項
.setSingleChoiceItems(new String[]{"Android", "ios", "wp"}, 1,
(d, position) -> {d.dismiss();})
.setPositiveButton("ok", null)
.build();
dialog.show(getSupportFragmentManager(), TAG);
複製代碼
多選列表框
EasyDialog.builder(this)
// 設置數據和默認選中的選項
.setMultiChoiceItems(
new String[]{"Android", "ios", "wp"}, new boolean[]{true, false, true},
(dialog, which, isChecked) -> showToast("onClick pos = " + which))
.build()
.show(getSupportFragmentManager());
複製代碼
在不少場景中咱們都須要自定義本身的dialog,方便使用自定義的佈局文件。在easyDialog中,它提供了baseCustomDialog類來讓咱們繼承,繼承後能夠看到一個明晰的代碼模板:
public class DemoDialog extends BaseCustomDialog {
@Override
protected int getLayoutResId() {
return 0; // 返回自定義的layout文件
}
@Override
protected void bindViews(View root) {
// 進行findViewById操做
}
@Override
protected void setViews() {
// 對view或者dialog的window作各類設置
}
}
複製代碼
順便一提,在這個類中咱們能夠複寫保存、恢復狀態的方法,作特殊狀況下的數據管理:
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
複製代碼
在瞭解了自定義dialog的基本寫法後,下面咱們來看下兩種實現方案。
假設ui設計了一個如上圖所示的dialog,那麼咱們天然要創建以下佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="@drawable/kale"
/>
<TextView
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Yosemite National Park"
/>
</LinearLayout>
複製代碼
分析可知,這裏須要的是一個「圖片資源」和「按鈕文案」,對應到alertDialog中就是icon和positiveText。從上文可知,咱們的dialogFragment中能夠拿到dialogParams,那麼直接從這裏取出須要的數據就好,無需自定義一個builder對象。
public class ImageDialog extends BaseCustomDialog {
@Override
protected int getLayoutResId() {
return R.layout.custom_dialog_image_layout; // 引入自定義佈局
}
@Override
protected void modifyAlertDialogBuilder(AlertDialog.Builder builder) {
super.modifyAlertDialogBuilder(builder);
builder.setPositiveButton(null, null); // 去掉了alerdialog的按鈕
}
@Override
protected void bindViews(View root) {
button = root.findViewById(R.id.button);
}
@Override
protected void setViews() {
// 經過getDialogParams()獲得外部傳入的數據,拿到按鈕的文案
button.setText(getDialogParams().positiveText);
button.setOnClickListener(v -> {
// 手動調用外層回調
getPositiveListener().onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
// 關閉對話框
dismiss();
});
}
}
複製代碼
展現的方式和上文的dialog並無什麼區別,不用修改外部的api:
EasyDialog.builder(this, ImageDialog.class)
.setPositiveButton("彈出動態設置樣式的Dialog", (dialog, which) -> {})
.build()
.show(getSupportFragmentManager());
複製代碼
在這個case中咱們須要着重看下modifyAlertDialogBuilder()
這個方法,modifyAlertDialogBuilder()容許easyDialog的子類修改創建alerDialog的builder對象,咱們能夠複寫它來作任何的事情。
@Override
void modifyAlertDialogBuilder(android.support.v7.app.AlertDialog.Builder builder) {
// 它會傳入android.support.v7.app.AlertDialog.Builder
}
複製代碼
Dialog作的事情必須是簡單的展現邏輯,儘可能不要在裏面作網絡請求等異步操做。當alertDialog.Builder不支持某些數據的時候,咱們就要用到自定義builder了。
首先,定義一個自定義的dialog,好比叫作MyBuilerDialog:
public class MyBuilderDialog extends BaseCustomDialog {
public static final String KEY_AGE = "KEY_AGE", KEY_NAME="KEY_NAME";
@Override
protected int getLayoutResId() {
return 0;
}
@Override
protected void bindViews(View root) {
}
@Override
protected void setViews() {
// 拿到參數,進行展現
int age = getArguments().getInt(KEY_AGE);
Toast.makeText(getContext(), "age: " + age, Toast.LENGTH_SHORT).show();
}
}
複製代碼
而後,實現自定義的Builder,創建新的set方法來把數據放入bundle中:
/**
* 繼承自{@link EasyDialog.Builder}以擴展builder
*/
public static class Builder extends BaseEasyDialog.Builder<Builder> {
private Bundle bundle = new Bundle();
public Builder(@NonNull Context context) {
super(context);
}
public Builder setAge(int age) {
bundle.putInt(KEY_AGE, age); // 設置年齡
return this;
}
public Builder setName(String name) {
bundle.putString(KEY_NAME, name); // 設置姓名
return this;
}
@Override
protected EasyDialog createDialog() {
// 這裏務必記得要new出本身自定義的dialog對象
MyBuilderDialog dialog = new MyBuilderDialog();
// 記得設置本身的bundle數據
dialog.setArguments(bundle);
return dialog;
}
}
複製代碼
最後,用傳入的數據來作自定義的操做,好比這裏替換了alertDialog的message:
@Override
protected void modifyAlertDialogBuilder(AlertDialog.Builder builder) {
super.modifyAlertDialogBuilder(builder);
Bundle arguments = getArguments();
String name = arguments.getString(KEY_NAME); // name
int age = arguments.getInt(KEY_AGE); // age
String str = "name: " + name + ", age: " + age;
// 修改builder對象
builder.setMessage("修改後的message是:\n\n" + str);
}
複製代碼
調用方式:
new MyBuilderDialog.Builder(this)
.setTitle("Custom Builder Dialog")
.setMessage("message")
.setName("kale")
.setAge(31)
.build()
.show(getSupportFragmentManager());
複製代碼
Design Support Library新加了一個bottomSheets控件,bottomSheets顧名思義就是底部操做控件,用於在屏幕底部建立一個可滑動關閉的視圖。BottomSheets必需要配合coordinatorLayout控件使用,也就是說底部對話框佈局的父容器必須是coordinatorLayout。
首先,定義一個佈局文件,用來承載主體的內容:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/ll_sheet_root"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="40dp"
app:layout_behavior="@string/bottom_sheet_behavior"
>
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text="內容區域"
android:textSize="30dp"
/>
</LinearLayout>
複製代碼
這裏有三個屬性:
app:behavior_peekHeight="40dp"
app:behavior_hideable="true"
app:layout_behavior="@string/bottom_sheet_behavior"
複製代碼
具體的java代碼咱們就不展開講述了,由於easyDialog已經幫咱們處理好了,這裏只須要知道要在用design包中的底部對話框的時候必需要有一個coordinatorLayout作容器。
EasyDialog支持了底部對話框的樣式,而實現的方式和自定義dialog並沒有區別,仍舊是自定義diaog三部曲:
public class BottomDialog extends BaseCustomDialog {
@Override
protected int getLayoutResId() {
return R.layout.custom_dialog_layout;
}
@Override
protected void bindViews(View root) {
// ...
}
@Override
protected void setViews() {
TextView textView = findView(R.id.message_tv);
textView.setOnClickListener(view -> {
dismiss();
});
// 獲得外部傳入的message信息
textView.setText(getDialogParams().message);
}
}
複製代碼
爲了代表其要從底部彈出,咱們須要在構建的時候增長一個標誌位,即setIsBottomDialog(true):
BottomDialog.Builder builder = EasyDialog.builder(this, BottomDialog.class);
builder.setMessage("click me");
builder.setIsBottomDialog(true); // 設置後則會變成從底部彈出,不然爲正常模式
builder.build().show(getSupportFragmentManager(), "dialog");
複製代碼
這裏的setIsBottomDialog()
是關鍵,baseCustomDialog中的onCreateDialog()會根據標誌位進行判斷,返回不一樣的dialog對象:
public abstract class BaseCustomDialog extends EasyDialog {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (!isBottomDialog()) {
return super.onCreateDialog(savedInstanceState);
} else {
return new BottomSheetDialog(getContext(), getTheme());
}
}
}
複製代碼
若是你不喜歡用這種方式,你能夠經過設置window的展現位置來作一個底部對話框:
@Override
protected void setViews() {
// 獲得屏幕寬度
final DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
// 創建layoutParams
final WindowManager.LayoutParams layoutParams = getDialog().getWindow().getAttributes();
int padding = 10;
layoutParams.width = dm.widthPixels - (padding * 2);
layoutParams.gravity = Gravity.BOTTOM; // 位置在底部
getDialog().getWindow().setAttributes(layoutParams);
}
複製代碼
說完了用法,下面咱們來研究下原理。在閱讀源碼後發現,easyDialog用了support包中提供的bottomSheetDialog
,它就是底部對話框的載體。BottomSheetDialog中已經配置好了bottomSheetBehavior,它還自定義了一個frameLayout容器。
咱們的layoutId會繼續被傳入wrapInBottomSheet()
,將咱們的佈局文件包裹一層父控件,即coordinatorLayout。
@Override
public void setContentView(@LayoutRes int layoutResId) {
super.setContentView(wrapInBottomSheet(layoutResId, null, null));
}
複製代碼
wrapInBottomSheet()方法會將咱們的佈局文件放入一個frameLayout中,而這個frameLayout則是由coordinatorLayout組成的,也就天然完成了bottomSheetDialog展現的前提。
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
FrameLayout container = (FrameLayout) inflate(getContext(),
R.layout.design_bottom_sheet_dialog, null);
CoordinatorLayout coordinator = container.findViewById(R.id.coordinator);
FrameLayout bottomSheet = coordinator.findViewById(R.id.design_bottom_sheet);
mBehavior = BottomSheetBehavior.from(bottomSheet); // behavior
}
複製代碼
design_bottom_sheet_dialog:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/touch_outside"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:soundEffectsEnabled="false"/>
<-- 自定義的佈局最終會被add到這個frameLayout中 -->
<FrameLayout
android:id="@+id/design_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
app:layout_behavior="@string/bottom_sheet_behavior"
style="?attr/bottomSheetStyle"/>
</android.support.design.widget.CoordinatorLayout>
複製代碼
須要注意的是,最下方的frameLayout中已經寫死了layout_behavior
屬性,google的開發者巧妙的將style
放在了最後一個,即style="?attr/bottomSheetStyle"
,因此咱們能夠經過定義style來完成相關的設置,好比:
app:behavior_hideable="true"
app:behavior_peekHeight="40dp"
app:layout_behavior="@string/bottom_sheet_behavior"
複製代碼
圖片來源:javiersantos/MaterialStyledDialogs
任何一個app都應該在早期定義一套對話框規範,這是極其必要的。不管ui設計的樣式多麼變幻無窮,咱們都應該用ui和data分離的思路來看問題,上圖所示的materialStyledDialogs就是一個很好的效果。
數據方面若是不知足須要,能夠經過自定義builder的方式來擴展,ui方面咱們能夠創建一個全局的樣式,這樣全部的對話框文件都會自動套用此樣式,不用修改任何邏輯代碼,並且還能夠利用style的繼承作擴展。
假設咱們定義了一個叫作Theme.Dialog
的樣式,若是你的項目像materialStyledDialogs那樣很符合官方的alerDialog,那麼修改以下屬性便可:
<style name="Theme.Dialog" parent="Theme.AppCompat.Light.Dialog"> <item name="windowActionBar">false</item> <!-- 有無標題欄 --> <item name="windowNoTitle">true</item> <!-- 對話框的邊框,通常不進行設置 --> <item name="android:windowFrame">@null</item> <!-- 是否浮如今activity之上 --> <item name="android:windowIsFloating">true</item> <!-- 是否半透明 --> <item name="android:windowIsTranslucent">true</item> <!-- 決定背景透明度 --> <item name="android:backgroundDimAmount">0.3</item> <!-- 除去title --> <item name="android:windowNoTitle">true</item> <!-- 對話框是否有遮蓋 --> <item name="android:windowContentOverlay">@null</item> <!-- 對話框出現時背景是否變暗 --> <item name="android:backgroundDimEnabled">true</item> <!-- 背景顏色,由於windowBackground中的背景已經寫死了,因此這裏的設置無效 --> <item name="android:colorBackground">#ffffff</item> <!-- 着色緩存(通常不用)--> <item name="android:colorBackgroundCacheHint">@null</item> <!-- 標題的字體樣式 --> <item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat </item> <item name="android:windowTitleBackgroundStyle"> @style/Base.DialogWindowTitleBackground.AppCompat </item> <!--對話框背景(重要),默認是@drawable/abc_dialog_material_background --> <item name="android:windowBackground">@drawable/abc_dialog_material_background</item> <!-- 動畫 --> <item name="android:windowAnimationStyle">@style/Animation.AppCompat.Dialog</item> <!-- 輸入法彈出時自適應 --> <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item> <item name="windowActionModeOverlay">true</item> <!-- 列表部分的內邊距,做用於單選、多選列表 --> <item name="listPreferredItemPaddingLeft">20dip</item> <item name="listPreferredItemPaddingRight">24dip</item> <item name="android:listDivider">@null</item> <!-- 單選、多選對話框列表區域文字的顏色 默認是@color/abc_primary_text_material_light --> <item name="textColorAlertDialogListItem">#00ff00</item> <!-- 單選、多選對話框的分割線 --> <!-- dialog中listView的divider 默認是@null --> <item name="listDividerAlertDialog">@drawable/divider</item> <!-- 單選對話框的按鈕圖標 --> <item name="android:listChoiceIndicatorSingle">@android:drawable/btn_radio</item> <!-- 對話框總體的內邊距,不做用於列表部分 默認:@dimen/abc_dialog_padding_material --> <item name="dialogPreferredPadding">20dp</item> <item name="alertDialogCenterButtons">true</item> <!-- 對話框內各個佈局的佈局文件,默認是@style/Base.AlertDialog.AppCompat --> <item name="alertDialogStyle">@style/Base.AlertDialog.AppCompat</item> </style>
<!-- parent="@style/Theme.AppCompat.Light.Dialog.Alert" -->
<style name="Theme.Dialog.Alert"> <item name="windowMinWidthMajor">@dimen/abc_dialog_min_width_major</item> <item name="windowMinWidthMinor">@dimen/abc_dialog_min_width_minor</item> </style>
複製代碼
最後記得在activity的theme中替換本來的alertDialogTheme
屬性:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="alertDialogTheme">@style/Theme.Dialog.Alert</item>
</style>
複製代碼
不幸的是,大多數ui人員設計的對話框都和系統的不符,因此咱們仍是須要創建自定義的樣式佈局,即替換alertDialogStyle
屬性中的佈局文件:
<style name="Theme.Dialog" parent="Theme.AppCompat.Light.Dialog">
// ...
<!-- 對話框內各個佈局的佈局文件,默認是@style/Base.AlertDialog.AppCompat -->
<item name="alertDialogStyle">@style/AlertDialogStyle</item>
</style>
<!-- 這裏是自定義佈局 -->
<style name="AlertDialogStyle" parent="Base.AlertDialog.AppCompat">
<!-- dialog的主體佈局文件,裏面包含了title,message等控件 -->
<item name="android:layout">@layout/abc_alert_dialog_material</item>
<!-- dialog中的列表佈局文件,其實就是listView -->
<item name="listLayout">@layout/abc_select_dialog_material</item>
<!-- dialog中列表的item的佈局 -->
<item name="listItemLayout">@layout/select_dialog_item_material</item>
<!-- 多選的item的佈局 -->
<item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>
<!-- 單選的item的佈局 -->
<item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>
</style>
複製代碼
題外話:
由於android文檔對於樣式的說明十分有限,並且還存在加「android:」前綴和不加的區別,因此這裏必需要貼出整段的配置文件,防止你們用錯屬性。
修改默認的佈局前要了解默認佈局是怎麼寫的,這裏建議直接copy系統的佈局到項目中,在上面再進行二次修改,這樣最不易出錯。
關於如何管理自定義佈局和自定義樣式,有一個小技巧。咱們能夠把自定義的全局樣式放在一個xml文件中編寫,並以common_dialog_或dialog_爲前綴,也能夠新建一個資源目錄來專門存放,只不過千萬不要在app的styles.xml文件中直接編寫,會讓styles.xml文件十分的混亂。
簡單列表的Item
select_dialog_item_material.xml:
<TextView
android:id="@android:id/text1"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:textAppearance="?attr/textAppearanceListItemSmall"
android:textColor="?attr/textColorAlertDialogListItem"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingRight="?attr/listPreferredItemPaddingRight"
/>
複製代碼
簡單列表的item就是一個textView,複製後記得要保留id,即android:id="@android:id/text1"
,其他的則能夠任意修改。
單選、多選列表的Item
單選和多選列表的item就是一個checkedTextView,一樣是須要保留android:id="@android:id/text1"
。
select_dialog_multichoice_material.xml: select_dialog_singlechoice_material.xml:
<CheckedTextView
android:id="@android:id/text1"
android:textColor="?attr/textColorAlertDialogListItem"
android:paddingRight="?attr/dialogPreferredPadding"
android:drawableLeft="?android:attr/listChoiceIndicatorSingle"
/>
複製代碼
做爲列表框架的ListView
不管是基礎的數組列表,仍是單選、多選列表,其容器都是一個viewGroup,系統默認是經過RecycleListView來實現的。這個類其實就是一個listView,它支持經過屬性設置padding,沒有任何複雜的邏輯。
public static class RecycleListView extends ListView {
private int mPaddingTopNoTitle, mPaddingBottomNoButtons;
public RecycleListView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecycleListView);
mPaddingBottomNoButtons = ta.getDimensionPixelOffset(
R.styleable.RecycleListView_paddingBottomNoButtons, -1);
mPaddingTopNoTitle = ta.getDimensionPixelOffset(
R.styleable.RecycleListView_paddingTopNoTitle, -1);
}
public void setHasDecor(boolean hasTitle, boolean hasButtons) {
if (!hasButtons || !hasTitle) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = hasTitle ? getPaddingTop() : mPaddingTopNoTitle;
final int paddingRight = getPaddingRight();
final int paddingBottom = hasButtons ? getPaddingBottom()
: mPaddingBottomNoButtons;
setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
}
}
複製代碼
系統默認的佈局文件是abc_select_dialog_material.xml,咱們複製後須要保留的是select_dialog_listview
這個id,其他的屬性可根據須要來修改。
abc_select_dialog_material.xml:
<!--
This layout file is used by the AlertDialog when displaying a list of items.
This layout file is inflated and used as the ListView to display the items.
Assign an ID so its state will be saved/restored.
-->
<view android:id="@+id/select_dialog_listview"
style="@style/Widget.AppCompat.ListView"
class="android.support.v7.app.AlertController$RecycleListView"
android:divider="?attr/listDividerAlertDialog"
app:paddingBottomNoButtons="@dimen/abc_dialog_list_padding_bottom_no_buttons"
app:paddingTopNoTitle="@dimen/abc_dialog_list_padding_top_no_title"/>
複製代碼
Dialog總體的佈局框架
abc_alert_dialog_material.xml:
AlertDialog中最重要的就是容器佈局了,它決定了dialog的總體外殼,而內部的自定義佈局僅僅是它其中的frameLayout區域,該文件涉及到以下三個xml:
由於這裏涉及的代碼量大,實際又沒有難點,你們能夠去搜索上述的xml文件來閱讀,這裏僅僅放出一個自定義後的效果:
能夠看到咱們並無修改任何java代碼,僅僅經過自定義layout就能夠完成符合業務需求的dialog樣式,這也是本章的核心思想。
Dialog的背景圖片是須要編寫的,絕對不是一張簡單的png,這個背景圖片決定了dialog的外邊距,也就是說dialog的外邊距不該該經過設置window的寬度來作,而是應該在背景圖中靜態的定義好。
InsetDrawable是android中一個頗有用的類,它一般會做爲dialog的背景圖。它能夠指定內容的內邊距,在dialog中控制的是dialog內容和屏幕四邊的距離。
設置邊距的屬性:
設置上下左右邊距的例子:
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetBottom="16dp"
android:insetLeft="26dp"
android:insetRight="26dp"
android:insetTop="16dp"
>
<bitmap android:src="@drawable/bg"/>
</inset>
複製代碼
下圖是用「蒙拉麗莎」作背景的展現效果,左右黑邊的寬度就是咱們所設置的26dp:
有不少項目喜歡模仿ios作圓角的dialog,經過insetDrawable和shape的結合,咱們能夠很簡單的在android上實現ios的效果。
dialog_bg_custom:
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetBottom="16dp"
android:insetLeft="26dp"
android:insetRight="26dp"
android:insetTop="16dp"
>
<shape android:shape="rectangle">
<corners android:radius="15dp" />
<solid android:color="@color/dialog_bg_color" />
</shape>
</inset>
複製代碼
上方的代碼定義了上下邊距爲16dp,左右邊距爲26dp的一個圓角背景(用shape畫了圓角)。定義好這個背景後,直接將其做用於windowBackground上即可獲得ios風格的圓角背景:
<style name="Theme.Dialog.Alert.IOS">
<item name="android:windowBackground">@drawable/dialog_bg_custom</item>
</style>
複製代碼
若是你但願更加靈活的實現圓角佈局,用cardView做爲容器也是一個很好的思路。又由於cardView自己就是frameLayout,又支持層級的顯示,因此它是一個完美的圓角父容器。
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="100dp"
app:cardBackgroundColor="@color/colorPrimary"
app:cardCornerRadius="15dp" // 定義圓角角度
app:cardElevation="0dp" // 去掉陰影
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/bg_mlls"
/>
</android.support.v7.widget.CardView>
複製代碼
題外話:
若是你用了一個圓角的shape做爲背景,那麼dialog中的內容也會被切割成圓角的樣式,等於說這個背景就是一個mask,這也是爲何源碼中用insetDrawable作背景的緣由之一。
若是你的dialog是像上圖同樣是上部透明,下部規整的樣式,你能夠考慮用「layer-list + inset」來實現:
上述的代碼給背景中增長了一個透明的item,關鍵是要標記非透明部分的top、bottom等屬性,這樣才能出效果。
上文講述的是如何靜態的定義全局dialog樣式,可是實際項目裏面常常會有多種dialog的樣式。推薦的作法是將全部的樣式都定義出來,在activity的theme中配置主要的樣式,將不經常使用的樣式用動態的方案設置給dialog。
假設Theme.Dialog.Alert是咱們定義的系統全局樣式,Theme.Dialog.Alert.Kale是部分dialog纔會用到的樣式,咱們把他們都定義好:
<style name="Theme.Dialog.Alert">
<item name="windowMinWidthMajor">@dimen/abc_dialog_min_width_major</item>
<item name="windowMinWidthMinor">@dimen/abc_dialog_min_width_minor</item>
</style>
<style name="Theme.Dialog.Alert.Kale">
<item name="android:windowBackground">@drawable/dialog_bg_custom</item>
</style>
複製代碼
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="alertDialogTheme">@style/Theme.Dialog.Alert</item>
</style>
複製代碼
系統默認的樣式天然須要定義在AppTheme中了,而不經常使用的樣式則是經過java代碼來引入的。EasyDialog的builder()中容許咱們傳遞一個樣式的id,也就是說這時構建出的dialog會直接用咱們傳入的樣式,而非使用靜態定義的。
private void showDialog(){
EasyDialog.Builder builder = EasyDialog.builder(getActivity(),
R.style.Theme_Dialog_Alert_Kale); // 自定義樣式
builder.setTitle("Dynamic Style Dialog")
.setIcon(R.drawable.kale)
.setMessage("上半部分是透明背景的樣式")
.build()
.show(getFragmentManager());
}
複製代碼
爲了簡單起見,咱們通常會用匿名內部類作dialog的監聽事件:
// ...
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
}
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
}
})
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
複製代碼
這樣作好處是實現簡單,百分之九十以上的狀況不會出問題,而壞處是轉屏後dialog的各類listener都會變成null。若是你想要保證轉屏後dialog的事件不丟,那麼必須採用activity來作監聽器對象,而且要給easyDialog的builder設置setRetainInstance(true)
。
EasyDialog.builder(this).setRetainInstance(true);
複製代碼
設置了這個標誌位後,在旋轉屏幕時fragment不會被執行onDestroy(),僅僅會執行到onDestroyView(),即不會銷燬當前的對象。EasyDialog利用這一特性,在回調函數中作了監聽器的保存和恢復操做,保證恢復的時候能讓listener和新的activity產生綁定,避免丟失事件。
/**
* 保存參數
*/
@CallSuper
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 若是其中有監聽器是activity的對象,那麼則保存它
EasyDialogListeners.saveListenersIfActivity(this);
}
/**
* 恢復參數
*/
@CallSuper
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
// 若是發現某個監聽器以前是activity對象,那麼則用當前新的activity爲其賦值
EasyDialogListeners.restoreListenersIfActivity(this, getActivity());
}
/**
* 清理參數
*/
@Override
public void onDestroyView() {
super.onDestroyView();
// 清理監聽器的引用,防止持有activity對象,避免引發內存泄漏
EasyDialogListeners.destroyListeners(this);
}
複製代碼
這裏須要特別注意的是:
onDestroy()
,恢復後的dialogFragment和以前的並不相同若是咱們的app支持帳戶踢出的功能,那麼在接到後端push的「須要踢出當前用戶」的消息後就須要彈出一個dialog。一種方式就是作一個系統層面的dialog,就像ANR時出現的系統dialog同樣,讓其永遠保持在屏幕的上方:
Dialog dialog = new Dialog(this);
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
dialog.show();
複製代碼
但這種寫法有兩個問題,一個是TYPE_SYSTEM_ALERT
已被廢棄,其二是須要申請彈窗的權限。
/** @deprecated */
@Deprecated
public static final int TYPE_SYSTEM_ALERT = 2003;
複製代碼
申請權限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
複製代碼
咱們能夠換個思路來考慮這個需求,要知道dialog的構建和activity是強相關的,那麼直接在application中保存當前的activity對象就好,這樣就能夠隨時使用activity了,也就能夠在任什麼時候候進行彈窗了。但要記得在鎖屏和app退出到後臺時,清空保存的activity對象。
首先,讓application中持有當前activity的引用:
public class App extends Application {
private AppCompatActivity curActivity;
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(Activity activity) {
curActivity = (AppCompatActivity) activity;
}
@Override
public void onActivityPaused(Activity activity) {
curActivity = null;
}
});
}
}
複製代碼
而後,定義彈出dialog的方法,好比叫做showDialog():
public class App extends Application {
private AppCompatActivity curActivity;
public void showDialog(String title, String message) {
if (curActivity == null) {
return; // 不要忘了判空操做
}
EasyDialog.builder(curActivity)
.setTitle(title)
.setMessage(message)
.setPositiveButton("ok", null)
.build()
.show(curActivity.getSupportFragmentManager());
}
}
複製代碼
最後,在須要的時候調用application.showDialog()來完成彈窗:
((App) getApplication()).showDialog("全局彈窗", "可在任意時機彈出一個dialog")
複製代碼
這裏的代碼在onResume()
和onPause()
中作了activity對象的獲取和清理,能夠保證獲取的是當前最上層的activity。此外記得要在彈出時作個activity的判空或isDestroyed()之類的判斷,避免使用了即將銷燬的activity對象。
題外話:
當你的應用支持了分屏功能,也就是多窗口後,那麼則須要在onStart()中獲得activity,在onStop()中清空activity,,更多詳細的內容請參考《多窗口支持 | Android Developers》。
@Override
public void onActivityStarted(Activity activity) {
curActivity = (AppCompatActivity) activity;
}
@Override
public void onActivityStopped(Activity activity) {
curActivity = null;
}
複製代碼
在讀完本章後,相信你們能利用原生或者現成的方案來知足ui的需求,不用再雜亂無章的定義各類對話框了。Dialog是一個咱們很經常使用的控件,但它的知識點其實並很多。若是咱們從頭思考它,你會發現它涉及fragment、activity的生命週期、windowManager掛載、fragment與activity通訊等等知識點,因此咱們須要更加深刻的瞭解它,學會它。
其實dialog和dialogFragment的設計算是android源碼中的典範了,正由於有如此優秀的設計,咱們才能不斷的擴展dialog,產生一個能隨着需求複雜度增長而進化的模型,這就是「遇強則強、遇弱則惹」的設計思路。