Dialog最佳實踐

本文會不定期更新,推薦watch下項目麻削。

如果喜歡請star掏熬,如果覺得有紕漏請?zhí)峤籭ssue王悍,如果你有更好的點子可以提交pull request。

本文的示例代碼主要是基于EasyDialog這個庫編寫的,若你有其他的技巧和方法可以參與進(jìn)來一起完善這篇文章。

本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices


背景

image_1bk93uogp1qhvr2911u1p3f10ms6c.png-124kB
image_1bk93uogp1qhvr2911u1p3f10ms6c.png-124kB

無論是大型項目還是小型項目,設(shè)計給出的對話框樣式都是千變?nèi)f化的疆液,很難形成統(tǒng)一的模塊化風(fēng)格。經(jīng)過長期的分析發(fā)現(xiàn)下列問題普遍存在在各個項目中:

  • 不用android原生的dialog樣式陕贮,全部自定義
  • dialog沒有統(tǒng)一的風(fēng)格堕油,至少有三種以上的風(fēng)格
  • 自定義dialog眾多,沒有統(tǒng)一設(shè)計飘蚯,難以擴展和關(guān)聯(lián)
  • 多數(shù)dialog和業(yè)務(wù)強綁定馍迄,獨立性極差

我們希望可以利用原生的api來實現(xiàn)高擴展性的自定義的dialog。經(jīng)過長期的探索局骤,我找到了一個更加輕量的集成方案攀圈。

需求

  • 模塊化的封裝dialog,由dialogfragment來做管理者
  • 利用原生的api來配置dialog峦甩,降低學(xué)習(xí)成本
  • 讓dialog的builder支持繼承赘来,實現(xiàn)組合+繼承的形式
  • 一個配置項可將原本的自定義dialog變成從底部彈出的樣式
  • 允許設(shè)置dialog的背景现喳,支持透明背景
  • 可通過直接修改style的方式,將原生dialog變成自定義的樣式
  • 屏幕旋轉(zhuǎn)后dialog中的數(shù)據(jù)不應(yīng)丟失
  • 能監(jiān)聽到dialog的消失犬辰、點擊空白處關(guān)閉等事件
  • dialog可以和activity之間進(jìn)行事件聯(lián)動
  • 實現(xiàn)從底部拉出的dialog樣式

實現(xiàn)

模塊化的封裝Dialog

我們最早就有了dialog這個類嗦篱,我們一般都會用它的子類——alertDialog,在v7中的AlertDialog還提供了theme和各種能力(單選幌缝、多選)灸促,一般在activity中的用法如下:

new AlertDialog.Builder(this)
        .setTitle("title")
        .setIcon(R.drawable.ic_launcher)
        .setPositiveButton("好", new positiveListener())
        .setNeutralButton("中", new NeutralListener())
        .setNegativeButton("差", new NegativeListener())
        .creat()
        .show();

但這里有個很明顯的問題——dialog的獨立性太差!

因為alertDialog是通過builder的形式new出來的涵卵,所以它讓dialog喪失了可繼承的特性浴栽。如果一個項目里面的dialog有一些通用的代碼,我們肯定要進(jìn)行整理轿偎。如果你還希望dialog能被統(tǒng)一管理典鸡,那么肯定要建立一個封裝類

public class DialogHelper {

    private String title, msg;

    /**
     * 各種自定義參數(shù),如:title
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * 各種自定義參數(shù)坏晦,如:message
     */
    public void setMsg(String msg) {
        this.msg = msg;
    }

    public void show(Context context) {
        // 通過配置的參數(shù)來建立一個dialog
        AlertDialog dialog = new AlertDialog.Builder(context)
                .setTitle(title)
                .setMessage(msg)
                .create();
        // ...
        // 通用的設(shè)置
        Window window = dialog.getWindow();
        window.setBackgroundDrawable(new ColorDrawable(0xffffffff)); // 白色背景
        dialog.show();
    }
}

包裝類解決了重復(fù)代碼多的問題萝玷,但是仍舊沒有解決dialog數(shù)據(jù)保存和生命周期管理等問題。后來google在android3.0的時候引入了一個新的類:dialogFragment±バ觯現(xiàn)在球碉,我們完全可以使用dialogFragment作一個control來管理alertDialog。

public class MyDialogFragment extends DialogFragment{

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // 不推薦的寫法
        return inflater.inflate(R.layout.dialog, null);
    }
}

注意仓蛆,注意汁尺!
如果你在onCreateView中做了dialog布局,那么我們之前的所有工作都可能沒有意義了多律,而且會破壞模塊化。我強烈建議通過onCreateDialog來建立dialog搂蜓!

正確的做法是AlertDialog被DialogFragment管理狼荞,DialogFragment被FragmentManager管理,這樣才是真正的面向?qū)ο蟮姆庋b方式帮碰,代碼自然也會干凈很多相味。

@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
    Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle("我是標(biāo)題")
        .setMessage(getResources().getString(R.string.hello_world))
        .setPositiveButton("我同意", this)
        .setNegativeButton("不同意", this)
        .setCancelable(false);
        //.show(); // show cann't be use here
    
    return builder.create();
}

如果你要做自定義的dialog,那么直接通過setView就能做到:

builder.setView(view)  // 設(shè)置自定義view

這樣的話他們的職責(zé)就很明確了:

  1. fragmentManager管理fragment的生命周期和activity的綁定關(guān)系
  2. dialogFragment來處理各種事件(onDismiss等)和接收外部傳參(bundle)
  3. alertDialog負(fù)責(zé)dialog的內(nèi)容和樣式的展示
public class MyDialog extends DialogFragment{

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle bundle = getArguments();
        // ...
        // 得到各種配置參數(shù)
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 根據(jù)得到的參數(shù)殉挽,建立一個dialog
        return new AlertDialog.Builder(getActivity())
                .setMessage("message")
                .create();
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        // 處理響應(yīng)事件
    }
}

至此丰涉,dialog三部曲就已經(jīng)完成:

  1. 在onCreate中拿到外部傳入的參數(shù)
  2. 在onCreateDialog中構(gòu)建一個alertDialog對象
  3. 通過DialogFragment的show()來顯示對話框

理解DialogFragment的方法調(diào)用

因為fragment本身就是一個復(fù)雜的管理器,而且很多開發(fā)者對于dialogFragment中的各種回調(diào)方法會產(chǎn)生理解上的偏差斯碌,所以我做了下面的圖示:

image_1bk895qo21fb71qkn14bb1vk3175p9.png-36.3kB
image_1bk895qo21fb71qkn14bb1vk3175p9.png-36.3kB
public class MyDialog extends android.support.v4.app.DialogFragment {

    private static final String TAG = "MyDialog";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 得到各種外部參數(shù)
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        // 這里返回null一死,讓fragment作為一個control
        return null;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 根據(jù)參數(shù)建立dialog
        return new AlertDialog.Builder(getActivity())
                .setMessage("msg")
                .setTitle("title")
                .create();
    }

    @Override
    public void setupDialog(Dialog dialog, int style) {
        super.setupDialog(dialog, style);
        // 上面建立好dialog后,這里可以進(jìn)行進(jìn)一步的配置操作
    }


    @Override
    public void onStart() {
        super.onStart();
        // 這里的view來自于onCreateView傻唾,所以是null
        View view = getView(); 
        // ...
        // 可以進(jìn)行dialog的findViewById操作
    }
}

調(diào)用流程為:

image_1bk8b5uh6ikp3the4k1iv8ote13.png-49.9kB
image_1bk8b5uh6ikp3the4k1iv8ote13.png-49.9kB

這里需要注意的是:只有在onStart中再去執(zhí)行findView操作投慈,因為在此之前window還沒有配置完成承耿,WindowManager還沒有把整個window掛載上去,會報錯伪煤!

利用原生的Builder進(jìn)行傳參

上面說到了我們可以通過intent給fragment進(jìn)行傳參加袋,但實際中的參數(shù)數(shù)目會很多。一般的項目中我們都會建立一個簡單的Serializable對象來做一次性裝配抱既,最后將其再塞給fragment职烧。

public class BuildParams 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;

}

有了數(shù)據(jù)對象后,我們自然要想到要通過build模式將其進(jìn)行封裝:

public class BuildParamsBuilder {

    private int mIconId;

    private int mThemeResId;

    // ... 省略部分參數(shù)
 
    public BuildParamsBuilder setIconId(int iconId) {
        mIconId = iconId;
        return this;
    }

    public BuildParamsBuilder setThemeResId(int themeResId) {
        mThemeResId = themeResId;
        return this;
    }

    // ... 省略部分代碼

    public BuildParams build() {
        return new BuildParams(mIconId, mThemeResId, mTitle, mMessage, mPositiveText, mNeutralText, mNegativeText, mItems, mCheckedItems,
                mIsMultiChoice, mIsSingleChoice, mCheckedItem);
    }
}

這時防泵,我們可以明顯的發(fā)現(xiàn)這里的builder和alert的builder是極其類似的蚀之。那么我們能否直接拿來用呢?
通過閱讀源碼我們發(fā)現(xiàn)AlertController.AlertParams是原生api提供的存放各種參數(shù)的對象择克,我們可以將其和自定義的BuildParams進(jìn)行映射恬总,這樣就可以省去了自造builder的工作了。

映射過程:

public BuildParams getBuildParams(AlertController.AlertParams p) {
    BuildParams data = new BuildParamsBuilder().createBuildParams();
    data.themeResId = themeResId;

    data.mIconId = p.mIconId;
    data.title = p.mTitle;
    data.message = p.mMessage;
    data.positiveText = p.mPositiveButtonText;
    data.neutralText = p.mNeutralButtonText;
    data.negativeText = p.mNegativeButtonText;
    data.items = p.mItems;
    data.isMultiChoice = p.mIsMultiChoice;
    data.checkedItems = p.mCheckedItems;
    data.isSingleChoice = p.mIsSingleChoice;
    data.checkedItem = p.mCheckedItem;

    return data;
}

build過程:

public <D extends EasyDialog> D build() {
    EasyDialog dialog = createDialog();
    AlertController.AlertParams p = getParams();

    Bundle bundle = new Bundle();
    bundle.putSerializable(KEY_BUILD_PARAMS, getBuildParams(p));
    bundle.putBoolean(KEY_IS_BOTTOM_DIALOG, isBottomDialog);
    dialog.setArguments(bundle);

    dialog.setOnCancelListener(p.mOnCancelListener);
    dialog.setOnDismissListener(p.mOnDismissListener);

    dialog.setPositiveListener(p.mPositiveButtonListener);
    dialog.setNeutralListener(p.mNeutralButtonListener);
    dialog.setNegativeListener(p.mNegativeButtonListener);
    dialog.setOnClickListener(p.mOnClickListener);
    dialog.setOnMultiChoiceClickListener(p.mOnCheckboxClickListener);
    
    dialog.setCancelable(p.mCancelable);
    return (D) dialog;
}

這樣我們可以直接將裝配好的各種參數(shù)扔給fragment了肚邢。

讓原生builder支持繼承

通常情況下壹堰,我們的builder都是不支持繼承的,但是對于dialog這種形式骡湖,我們希望可以存在父子類的關(guān)系贱纠。

對話框一號:

image_1bk8e07oi1rjqmg01rcs1me94ha1g.png-23.9kB
image_1bk8e07oi1rjqmg01rcs1me94ha1g.png-23.9kB

對話框二號:

image_1bk8e7psmimlrac6dcbfq93s1t.png-25kB
image_1bk8e7psmimlrac6dcbfq93s1t.png-25kB

這兩個對話框很像,我們想要做點有趣的事情响蕴。我第二個對話框沒有icon谆焊,如果外面?zhèn)魅氲膖itle字段的值是“Title”,我就將其變?yōu)樾碌闹灯忠模础癗ew Title”辖试。

public class MyEasyDialog extends EasyDialog{

    /**
     * 繼承自父類的Builder
     */
    public static class Builder extends EasyDialog.Builder {

        public Builder(@NonNull Context context) {
            super(context);
        }

        protected EasyDialog createDialog() {
            return new MyEasyDialog();
        }
    }

    @Override
    protected void modifyOriginBuilder(EasyDialog.Builder builder) {
        super.modifyOriginBuilder(builder);
        
        builder.setIcon(0); // 去掉icon
        if (TextUtils.equals(getBuildParams().title, "Title")) {
            builder.setTitle("New Title");
        }
    }
}

這里有兩個重要的方法:

  • modifyOriginBuilder():用來修改原本父類的builder對象
  • getBuildParams():得到原本父類中builder中設(shè)置的各個參數(shù)

我們現(xiàn)在只需要繼承自父類的builder,然后復(fù)寫createDialog方法就好劈狐,其余的工作都在modifyOriginBuilder中罐孝。

試想,如果我們不用繼承的話肥缔。要完成這個工作莲兢,就必須在原本的dialogFragment加一些條件判斷,實在不夠靈活续膳。

利用原生builder的示例代碼:

EasyDialog.Builder builder = new EasyDialog.Builder();
builder.setTitle("Title")
        .setMessage(R.string.hello_world)
        .setOnCancelListener(new OnCancelListener() {
            public void onCancel(DialogInterface dialog) {
                // onCancel - > onDismiss
            }
        })
        .setOnDismissListener(new OnDismissListener() {
            public void onDismiss(DialogInterface dialog) {

            }
        })
        .setNeutralButton("no", null)
        .setPositiveButton("ok", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                
            }
        })
        .setNegativeButton("cancel", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
               
            }
        });

EasyDialog dialog = builder.build();
dialog.setCancelable(true); // 點擊空白是否可以取消
dialog.show(getSupportFragmentManager(), TAG);

模板化的制作自定義dialog

1. 自定義一個builder

自定義一個dialog肯定要先定義一個繼承自BaseEasyDialog.Builder的builder改艇,這個builder當(dāng)然也支持bundle類型的傳參。

public static class Builder extends BaseEasyDialog.Builder<Builder> {

    private Bundle bundle = new Bundle(); // 通過bundle來支持參數(shù)

    public Builder setImageBitmap(Bitmap bitmap) {
        bundle.putByteArray(KEY_IMAGE_BITMAP, bitmap2ByteArr(bitmap));
        return this;
    }

    public Builder setInputText(CharSequence text, CharSequence hint) {
        bundle.putCharSequence(KEY_INPUT_TEXT, text);
        bundle.putCharSequence(KEY_INPUT_HINT, hint);
        return this;
    }

    protected DemoSimpleDialog createDialog() {
        DemoSimpleDialog dialog = new DemoSimpleDialog();
        dialog.setArguments(bundle);
        return dialog;
    }

}

說明:上面的泛型傳入的參數(shù)是當(dāng)前的Builder類

2. 建立一個繼承自BaseCustomDialog的Dialog

編寫dialog的方式也是有流程可循的:

  1. 拿到數(shù)據(jù)
  2. 設(shè)置布局文件
  3. 綁定view
  4. 設(shè)置view和其相關(guān)事件
  5. 銷毀view
public class DemoSimpleDialog extends BaseCustomDialog {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 拿到參數(shù)
        Bundle arguments = getArguments();
        if (arguments != null) {
            mInputText = arguments.getCharSequence(KEY_INPUT_TEXT);
        }
    }

    @Override
    protected int getLayoutResId() {
        // 設(shè)置布局文件
        return R.layout.demo_dialog_layout;
    }

    @Override
    protected void bindViews(View root) {
        // 綁定view
        mInputTextEt = findView(R.id.input_et);
    }

    @Override
    public void setViews() {
        // 設(shè)置view
        if (mInputText != null) {
            mInputTextEt.setVisibility(View.VISIBLE);
            if (!isRestored()) {
                // 如果是從旋轉(zhuǎn)屏幕或其他狀態(tài)恢復(fù)的fragment
                mInputTextEt.setText(mInputText);
            }
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // 銷毀相關(guān)的view
        mInputTextEt = null;
    }

}

自定義從底部彈出的dialog

效果

image_1bk8m111gth5p41ijr1rt4jkq2a.png-47.4kB
image_1bk8m111gth5p41ijr1rt4jkq2a.png-47.4kB

這種從底部彈出的dialog我們并不少見坟岔,可android原生并不提供這樣的樣式谒兄,那么就來自定義吧。自定義的方式也很簡單炮车,也是繼承自BaseCustomDialog

public class CustomBottomSheetDialog extends BaseCustomDialog {

    public static class Builder extends BaseEasyDialog.Builder<Builder> {

        public Builder(@NonNull Context context) {
            super(context);
        }
        
        protected EasyDialog createDialog() {
            return new CustomBottomSheetDialog();
        }
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.custom_dialog_layout;
    }

    @Override
    protected void bindViews(View root) {
        // findView...
    }

    @Override
    protected void setViews() {
        ((TextView) findView(R.id.message_tv)).setText(getBuildParams().message);
    }
}

唯一的區(qū)別是需要在構(gòu)建的時候加一個標(biāo)志位:

CustomBottomSheetDialog.Builder builder = new CustomBottomSheetDialog.Builder(this);
        builder.setIsBottomDialog(true); // 表明這是從底部彈出的
        CustomBottomSheetDialog dialog = builder.build();
        dialog.show(getSupportFragmentManager(), "dialog");

原理

這里的原理是用了support包中提供的BottomSheetDialog舵变。BottomSheetDialog里面已經(jīng)配置好了BottomSheetBehavior酣溃,它還自定義了一個容器:

<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2015 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
-->
<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"/>

    <FrameLayout  // 你自定義的布局最終會被add到這里
            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>

我們都知道BottomSheetBehavior是會通過app:layout_behavior="@string/bottom_sheet_behavior"這個標(biāo)識來找“底部布局”的,而我們的自定義布局又是在design_bottom_sheet中纪隙,所以自然就有了底部彈出的效果了赊豌。

順便說一下,因為這個容器的布局在源碼里已經(jīng)寫死了绵咱,你自定義的布局又在容器內(nèi)碘饼,所以你自定義布局中寫

app:behavior_hideable="true"
app:behavior_peekHeight="40dp"
app:layout_behavior="@string/bottom_sheet_behavior"

是完全沒有任何作用的,如果想要起作用悲伶,那么請使用style="?attr/bottomSheetStyle"艾恼。

除了這種方式外,你當(dāng)然也可以自己在setViews中實現(xiàn)此效果:

@Override
protected void setViews() {
    // 得到屏幕寬度
    final DisplayMetrics dm = new DisplayMetrics();
    getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);

    // 建立layoutParams
    final WindowManager.LayoutParams layoutParams = getDialog().getWindow().getAttributes();
    
    int padding = getResources().getDimensionPixelOffset(R.dimen.kale_dialog_padding);
    layoutParams.width = dm.widthPixels - (padding * 2);

    layoutParams.gravity = Gravity.BOTTOM; // 位置在底部
    getDialog().getWindow().setAttributes(layoutParams); // 通過attr設(shè)置

    // 也可通過setLayout來設(shè)置
    // getDialog().getWindow().setLayout(dm.widthPixels, getDialog().getWindow().getAttributes().height);
}

以上就是實現(xiàn)底部彈出dialog的標(biāo)準(zhǔn)方式了麸锉。

從底部拉出的dialog樣式

652417-6c205a491048768c.gif-164.8kB
652417-6c205a491048768c.gif-164.8kB

android給出了一個完善好用的BottomSheet來實現(xiàn)底部彈窗效果钠绍。

<android.support.design.widget.CoordinatorLayout android:id="@+id/coordinatorlayout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    >

    <include layout="@layout/content_bottom_sheet" />

</android.support.design.widget.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
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="bottom|center"
        android:layout_marginTop="20dp"
        android:orientation="horizontal"
        >
    
    // ... 藏起來的部分

    </LinearLayout>

</LinearLayout>
// 得到 Bottom Sheet 的視圖對象所對應(yīng)的 BottomSheetBehavior 對象
behavior = BottomSheetBehavior.from(findViewById(R.id.ll_sheet_root));
if (behavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
    behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}

其實它看起來是dialog,但其本質(zhì)就是一個布局文件花沉,和dialog的關(guān)系不大柳爽。

正確的設(shè)置dialog的背景

設(shè)置dialog背景的方法有兩種:

1、給window設(shè)置setBackgroundDrawable

在dialogFragment#onStart的時候:

getDialog().getWindow().setBackgroundDrawable(new ColorDrawable()); // 去除dialog的背景
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(0xffffffff)); // 白色背景
getDialog().getWindow().setBackgroundDrawableResource(R.drawable.dialog_bg_custom_red); // 資源文件

2碱屁、在style中設(shè)置

<!--對話框背景(重要)  , default = abc_dialog_material_background-->
<item name="android:windowBackground">@drawable/dialog_bg_custom</item>

實際中我們的設(shè)計一般都會給我們一個圓角+邊距的樣式:

image_1bk8ntkeanu61ic2f1l151l10u42n.png-123.4kB
image_1bk8ntkeanu61ic2f1l151l10u42n.png-123.4kB

我們的目標(biāo)是做圓角和外邊距磷脯,那么很自然就想到了shapeinset兩個標(biāo)簽:

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<inset xmlns:android="http://schemas.android.com/apk/res/android"
       android:insetLeft="16dp"
       android:insetTop="16dp"
       android:insetRight="16dp"
       android:insetBottom="16dp">
    <shape android:shape="rectangle">
        <corners android:radius="2dp" />
        <solid android:color="@android:color/white" />
    </shape>
</inset>
image_1bk8o2fkg1nhf1j2c51m1ar9olc34.png-66.7kB
image_1bk8o2fkg1nhf1j2c51m1ar9olc34.png-66.7kB

A Drawable that insets another Drawable by a specified distance or fraction of the content bounds. This is used when a View needs a background that is smaller than the View's actual bounds.

inset標(biāo)簽可能有同學(xué)沒有用過。你可以理解為view設(shè)置這個資源為background后娩脾,它會和view的外邊距保留一定的距離赵誓,成為一個比view小的背景圖片。

image_1bk8okdprcea1eg34ra11lpvml3h.png-305.7kB
image_1bk8okdprcea1eg34ra11lpvml3h.png-305.7kB

如果你的dialog是像上圖一樣上部透明柿赊,下部規(guī)整的樣式俩功,你可以考慮用layer-listinset來實現(xiàn):

<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:insetBottom="16dp"
    android:insetLeft="26dp"
    android:insetRight="26dp"
    android:insetTop="16dp"
    >

    <layer-list>
        <item>
            <color android:color="#00E4007F" />  // 透明區(qū)域
        </item>

        <item android:top="80dp">
            <shape android:shape="rectangle">
                <corners android:radius="10dp" />
                <solid android:color="#d19a70" />
            </shape>
        </item>
    </layer-list>
</inset>
image_1bk8oonia8431su6cjiojr5r3u.png-88.6kB
image_1bk8oonia8431su6cjiojr5r3u.png-88.6kB

通過修改style來改變樣式

如果你項目中的dialog很簡單,僅僅是想要對原生的樣式做輕微的定制碰声,你可以考慮修改一下dialog的style绑雄。修改的方式是在項目的theme中設(shè)置alertDialogTheme屬性。

<style name="AppTheme.CustomDialogStyle">
    <!-- 如果要看自定義樣式的例子奥邮,可以加載此布局文件 -->
    <item name="alertDialogTheme">@style/Theme.Dialog.Alert</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>

關(guān)鍵在于Theme.Dialog中的各種屬性:

<style name="Theme.Dialog" parent="Theme.AppCompat.Light.Dialog">
    <item name="windowActionBar">false</item>
    <!-- 沒有標(biāo)題欄 -->
    <item name="windowNoTitle">true</item>

    <!--邊框-->
    <item name="android:windowFrame">@null</item>

    <!--是否浮現(xiàn)在activity之上-->
    <item name="android:windowIsFloating">true</item>

    <!-- 是否透明 -->
    <item name="android:windowIsTranslucent">true</item>

    <!--除去title-->
    <item name="android:windowNoTitle">true</item>

    <!-- 對話框是否有遮蓋 -->
    <item name="android:windowContentOverlay">@null</item>

    <!-- 對話框出現(xiàn)時背景是否變暗 -->
    <item name="android:backgroundDimEnabled">true</item>

    <!-- 背景顏色,因為windowBackground中的背景已經(jīng)寫死了罗珍,所以這里的設(shè)置無效 -->
    <item name="android:colorBackground">@color/background_floating_material_light</item>

    <!-- 著色緩存(一般不用)-->
    <item name="android:colorBackgroundCacheHint">@null</item>

    <!-- 標(biāo)題的字體樣式 -->
    <item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat</item>
    <item name="android:windowTitleBackgroundStyle">@style/Base.DialogWindowTitleBackground.AppCompat</item>

    <!--對話框背景(重要)  , default = abc_dialog_material_background-->
    <item name="android:windowBackground">@drawable/dialog_bg_custom</item>

    <!-- 動畫 -->
    <item name="android:windowAnimationStyle">@style/Animation.AppCompat.Dialog</item>

    <!-- 輸入法彈出時自適應(yīng) -->
    <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>

    <item name="windowActionModeOverlay">true</item>

    <!-- 列表部分的內(nèi)邊距洽腺,作用于單選、多選列表 -->
    <item name="listPreferredItemPaddingLeft">20dip</item>
    <item name="listPreferredItemPaddingRight">24dip</item>

    <item name="android:listDivider">@null</item>

    <!-- 單選覆旱、多選對話框列表文字的顏色 默認(rèn):@color/abc_primary_text_material_light -->
    <item name="textColorAlertDialogListItem">#00ff00</item>

    <!-- 單選蘸朋、多選對話框的分割線 -->
    <!-- dialog中l(wèi)istView的divider 默認(rèn):@null-->
    <item name="listDividerAlertDialog">@drawable/divider</item>

    <!-- 單選對話框的按鈕圖標(biāo) (默認(rèn)不為null)-->
    <item name="android:listChoiceIndicatorSingle">@android:drawable/btn_radio</item>

    <!-- 對話框整體的內(nèi)邊距,但不作用于列表部分 默認(rèn):@dimen/abc_dialog_padding_material-->
    <item name="dialogPreferredPadding">20dp</item>

    <item name="alertDialogCenterButtons">true</item>

    <!-- 對話框內(nèi)各個布局的布局文件-->
    <item name="alertDialogStyle">@style/AlertDialogStyle</item>
</style>

這里的屬性我已經(jīng)做了詳細(xì)的解釋扣唱,就不多做說明了藕坯,里面關(guān)鍵的是:

<!-- 對話框內(nèi)各個布局的布局文件-->
<item name="alertDialogStyle">@style/AlertDialogStyle</item>
<!-- 這里全是自定義的屬性团南,修改后就會改變dialog的顏色等樣式 -->
<style name="AlertDialogStyle" parent="Base.AlertDialog.AppCompat">
    
    <!-- AlertController.class - line:168 -->

    <!-- dialog的主體布局文件,里面包含了title炼彪,message等控件 -->
    <item name="android:layout">@layout/custom_dialog_alert_material</item>
    <!-- dialog中的列表布局文件吐根,其實就是listView -->
    <item name="listLayout">@layout/custom_dialog_list_material</item>
    <!-- dialog中列表的item的布局 -->
    <item name="listItemLayout">@layout/custom_dialog_select_item_material</item>
    <!-- 多選的item的布局 -->
    <item name="multiChoiceItemLayout">@layout/custom_dialog_select_multichoice_material</item>
    <!-- 單選的item的布局 -->
    <item name="singleChoiceItemLayout">@layout/custom_dialog_select_singlechoice_material</item>
</style>

如果你想要稍微修改原生樣式,你可以直接copy原生的layout辐马,修改后將新的layout放到這里就行了拷橘。

修改布局前:

image_1bk8pj19b1qdsa9431k10dadj74b.png-23.8kB
image_1bk8pj19b1qdsa9431k10dadj74b.png-23.8kB

修改布局后:

image_1bk8pjjr45kj1slr1ens1ibs1t964o.png-40.7kB
image_1bk8pjjr45kj1slr1ens1ibs1t964o.png-40.7kB

樣式完全變了,但代碼一行沒動喜爷,效果還是很神奇的冗疮。

注意:原生的layout代碼會隨著support版本的不同而發(fā)生改變,所以每次更新support包的時候需要檢查這里檩帐,防止出現(xiàn)不可知的崩潰术幔。

屏幕旋轉(zhuǎn)后保持dialog中的數(shù)據(jù)

1.保存view的狀態(tài)

我們知道,當(dāng)Activity調(diào)用了onSaveInstanceState()后湃密,便會對它的View Tree進(jìn)行保存诅挑,而進(jìn)一步對每一個子View調(diào)用其onSaveInstanceState()來保存狀態(tài)。
如果你的dialog沒有什么異步和特別的數(shù)據(jù)勾缭,僅僅是一個editText揍障,那么android自己view的自動保存機制就已經(jīng)幫你實現(xiàn)了自動保存數(shù)據(jù)了。

橫屏:

image_1bk8r00as1ng01rok1dgolk3tmg55.png-31.3kB
image_1bk8r00as1ng01rok1dgolk3tmg55.png-31.3kB

豎屏:

image_1bk8r0eig1ka81u66128k1qbm6o5i.png-26kB
image_1bk8r0eig1ka81u66128k1qbm6o5i.png-26kB

如果你的dialog中有自定義的view俩由,自定義view中你并沒有處理view的onSaveInstanceState()毒嫡,那么旋轉(zhuǎn)后dialog中的數(shù)據(jù)很有可能不會如你想象的一樣保留下來。

關(guān)于如何處理自定義view的狀態(tài)幻梯,可以參考《android中正確保存view的狀態(tài)》一文兜畸。

2.保存intent中的數(shù)據(jù)

每次旋轉(zhuǎn)屏幕后onCreate都會重新觸發(fā),onCreate中拿到的bundle中的數(shù)據(jù)仍舊會和之前一樣碘梢,所以不用擔(dān)心是否要手動保存通過getArgument()拿到的bundle咬摇。

只不過你可以用isRestored()來判斷當(dāng)前dialog是否是重建的,這樣來避免新設(shè)置一個title反而會沖掉eidtText自動保存的輸入值的問題煞躬。

@Override
protected void setViews() {
    // ...
    if (!isRestored()) {
        editText.setText("default value");
    } 
}

3.保存邏輯數(shù)據(jù)

image_1bk8t7mb71trfd07128k1cql1e595v.png-175.3kB
image_1bk8t7mb71trfd07128k1cql1e595v.png-175.3kB

利用fragment管理dialog的一大好處就是可以用它本身的數(shù)據(jù)保存方案:

public class MyEasyDialog extends EasyDialog {

    private static final String TAG = "MyEasyDialog";

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
    
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }
    
    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    }
}

當(dāng)我們的dialog中的操作有異步網(wǎng)絡(luò)操作的時候肛鹏,簡單的view保存方案已經(jīng)不能滿足我們了《髋妫可以考慮將網(wǎng)絡(luò)請求的狀態(tài)和結(jié)果通過onSaveInstanceState進(jìn)行保存在扰,在onRestoreInstanceState中來恢復(fù)。

Dialog相關(guān)的事件處理

為了簡單起見雷客,我仍舊采用了builder模式來設(shè)置dialog的監(jiān)聽事件:

EasyDialog.Builder builder = new MyEasyDialog.Builder(this);
builder.setTitle("Title")
.setIcon(R.mipmap.ic_launcher)
.setMessage(R.string.hello_world)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
    @Override
    public void onCancel(DialogInterface dialog) {
        // 點空白處消失時才會觸發(fā)C⒅椤!=寥埂皱卓!
    }
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
    @Override
    public void onDismiss(DialogInterface dialog) {
        // 對話框消失的時候觸發(fā)
    }
})
.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.dismiss(); // cancel -> dismiss
    }
})
// ...

這樣做好處是簡單裹芝,壞處是轉(zhuǎn)屏后,dialog的各種listener都成了null娜汁。所以嫂易,如果你要保證轉(zhuǎn)屏后dialog的事件不丟,那么你還是得采用activity實現(xiàn)接口的方式來做存炮。

需要特別注意的是:

  1. dialog的出現(xiàn)和消失并不會觸發(fā)activity的onPause()和onResume()
  2. onCancelListener僅僅是監(jiān)聽點擊空白處dialog消失的事件

總結(jié)

dialog是一個我們很常用的控件炬搭,但它的知識點其實并不少。如果我們從頭思考它穆桂,你會發(fā)現(xiàn)它涉及封裝技巧宫盔、生命周期、windowManager掛載享完、fragment&activity通信等方面灼芭。
我相信如果大家可以通過最簡單的api,簡化現(xiàn)有的dialog設(shè)計般又,利用原生或者現(xiàn)成的方案來滿足自己項目的需求彼绷,不用再雜亂無章的四處定義對話框。

developer-kale@foxmail.com
developer-kale@foxmail.com
微博:@天之界線2010

參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市堕义,隨后出現(xiàn)的幾起案子猜旬,更是在濱河造成了極大的恐慌,老刑警劉巖倦卖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洒擦,死亡現(xiàn)場離奇詭異,居然都是意外死亡怕膛,警方通過查閱死者的電腦和手機熟嫩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褐捻,“玉大人掸茅,你說我怎么就攤上這事∧眩” “怎么了倦蚪?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長边苹。 經(jīng)常有香客問我,道長裁僧,這世上最難降的妖魔是什么个束? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任慕购,我火速辦了婚禮,結(jié)果婚禮上茬底,老公的妹妹穿的比我還像新娘沪悲。我一直安慰自己,他們只是感情好阱表,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布殿如。 她就那樣靜靜地躺著,像睡著了一般最爬。 火紅的嫁衣襯著肌膚如雪涉馁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天爱致,我揣著相機與錄音烤送,去河邊找鬼。 笑死糠悯,一個胖子當(dāng)著我的面吹牛帮坚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播互艾,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼试和,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纫普?” 一聲冷哼從身側(cè)響起阅悍,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎局嘁,沒想到半個月后溉箕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡悦昵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年肴茄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片但指。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡寡痰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出棋凳,到底是詐尸還是另有隱情拦坠,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布剩岳,位于F島的核電站贞滨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晓铆,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一勺良、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骄噪,春花似錦尚困、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滔韵,卻和暖如春逻谦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奏属。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工跨跨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人囱皿。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓勇婴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嘱腥。 傳聞我的和親對象是個殘疾皇子耕渴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容