手把手帶你玩轉 DialogFragment Android View 的繪製流程之 Measure 過程詳解 (一)

1. 概述

最近項目中用到了 DialogFragment,在使用 DialogFragment 的時候,遇到了很多問題,比如如何在顯示的時候保證狀態欄的顏色也不發生變化,如何設置自己的背景,不用系統背景等等。

那麼 DialogFragment 是什麼樣的呢?DialogFragment 在 android 3.0 時被引入,是一種特殊的 Fragment,用於在 Activity 的內容之上展示一個模態的對話框。典型的用於:展示警告框,輸入框,確認框等等。

在 DialogFragment 產生之前,我們創建對話框:一般採用 AlertDialog 和 Dialog。注:官方不推薦直接使用Dialog創建對話框。使用 DialogFragment 來管理對話框,當旋轉屏幕和按下後退鍵時可以更好的管理其聲明週期,它和 Fragment 有着基本一致的聲明週期。且 DialogFragment 也允許開發者把 Dialog 作爲內嵌的組件進行重用,類似 Fragment(可以在大屏幕和小屏幕顯示出不同的效果)

使用 DialogFragment 至少需要實現 onCreateView 或者 onCreateDIalog方法。onCreateView即使用定義的 xml 佈局文件展示 Dialog。onCreateDialog即利用 AlertDialog 或者 Dialog 創建出Dialog。 

2. 應用

2.1 採用靜態方法來構造 DialogFragment

下面我們開始自己寫一個 DialogFragment,但是,當我按照習慣慣性寫下如下代碼的時候,給了我一個刺眼的紅色提示:

錯誤提示: Avoid non-default constructors in fragments: use a default constructor plus Fragment#setArguments(Bundle) instead

其原因是你重載了fragment的構造方法,但是在一些情況下,如屏幕翻轉時,fragment被重新創建,就可能會造成數據丟失。

解決方案一(不推薦):

@SuppressLint({"NewApi", "ValidFragment"})  在構造方法上加上這個註解,就可以不檢察,但是這是google不推薦的做法

解決方案二(也不推薦):

禁止報錯

lintOptions {
    abortOnError false
}

這段加到項目的 gradle 文件中就可以不報這個錯誤了,不過這只是讓他不報而已,實際上還是存在問題的,所以也不推薦這樣做。 

解決方案三(推薦):

創建 newInstance 方法來,這種方式避免了使用構造來傳參數。

創建 newInstance 方法

    public static final SplashViewPagerFragment newInstance(int pid, String message) {
        SplashViewPagerFragment fragment = new SplashViewPagerFragment();
        Bundle bundle = new Bundle();
        bundle.putInt("pid",pid);
        bundle.putString("message", message);
        fragment.setArguments(bundle);
        return fragment ;
    }

重寫onCreate

@Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mMessage=getArguments().getString("message");
        mPid=getArguments().getInt("pid");
    }

創建fragment實例

SplashViewPagerFragment splashViewPagerFragment=SplashViewPagerFragment.newInstance(1,"測試");

通過上面的這種方式就可以解決問題。

不過要說的是,在 Android X 上,並沒有這個提示。

2.2 重寫 onCreateView 創建 Dialog

這裏爲了方便,直接使用 mainActivity 的佈局,具體代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

重寫 onCreateView 方法:

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_main, container, false);
        return view;
    }

最後調用:

    private void openDialog() {
        MyDialogFragment fragment = new MyDialogFragment();
        fragment.show(getSupportFragmentManager(),"Dialog");
    }

最後的顯示效果如下:

 可以發現是默認居中的,這個佈局屬性,應該是內部就定義好的。

這裏做一個小變動,如果把 xml 佈局的 TextView 鋪滿全屏會變成什麼效果呢?

然而,令人喪氣的是,結果效果還是一樣的....

那如果我在外面在嵌套一層呢?更改佈局如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="3000dp"
        android:background="@color/dark_blue">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="Hello World!" />
    </FrameLayout>

</FrameLayout>

具體效果如下:

 

可以看到的是,DialogFragment 是自帶 padding 和居中效果的,因此,如果你定義的 view 需要居中,就不需要再去定義居中等屬性了。

2.3 重寫 onCreateDialog創建 Dialog

在 onCreateDialog 中一般可以使用 AlertDialog 或者 Dialog 創建對話框,不過 google 不推薦直接使用 Dialog,我們就使用 AlertDialog 來創建一個登錄的對話框。

具體代碼如下:

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        LayoutInflater inflater = getActivity().getLayoutInflater();
        View view = inflater.inflate(R.layout.activity_main, null);
        // 設置按鈕名稱和點擊回調
        builder.setView(view)
                .setPositiveButton("test",
                        (dialog, id) -> {
                            // 此處可以添加自己的處理邏輯
                        })
                .setNegativeButton("quit", null);
        return builder.create();

    }

此處爲了方便,我們還是用了原來的 xml 佈局。

可以看到的是,最終效果是按照我們設定的一樣鋪滿屏幕的。這個跟 onCreateView 效果是不一樣的。此外還將我們的小寫字母變成了大寫字母。

3. 屏幕適配

3.1 如何處理屏幕翻轉

如果使用傳統的 Dialog ,需要我們手動處理屏幕翻轉的情況,但使用 DialogFragment 的話,則不需要我們進行任何處理,FragmentManager 會自動管理 DialogFragment 的生命週期。

3.2 如何隱藏標題欄

在基本用法裏代碼註釋有設置主題的地方,下面詳細說下兩種方法下設置無標題欄的方式:

方法一:

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_main, container, false);
        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        return view;
    }

 方法二:

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setStyle(DialogFragment.STYLE_NO_TITLE, 0);
} 

3.3 如何實現全屏

具體代碼如下:

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.activity_main, container, false);
        if (getDialog() != null && getDialog().getWindow() != null) {
            Window window = getDialog().getWindow();
            // 鋪滿全屏
            window.getDecorView().setPadding(0, 0, 0, 0);
            WindowManager.LayoutParams lp = window.getAttributes();
            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            window.setAttributes(lp);
        }
        return view;
    }

最終效果如下:

可以發現有個問題是沒有鋪滿全屏,並且 hello world 也看不見了。那麼怎麼解決呢?只要加上下面這句代碼就好。

    if (getDialog() != null && getDialog().getWindow() != null) {
            Window window = getDialog().getWindow();
            // 透明背景
            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
            // 鋪滿全屏
            window.getDecorView().setPadding(0, 0, 0, 0);
            WindowManager.LayoutParams lp = window.getAttributes();
            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            window.setAttributes(lp);
        }

 這裏我個人猜測是不是跟 setWillNotDraw 方法有關係,但是具體原因還是不明確。後來我試了下,把代碼改成下面這個也是可以 work 的。

 window.getDecorView().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

3.4 如何與 Activity 進行交互?

由於我們是在 activity 實例上創建的 dialogFragment, 因此,acitivty 是持有 dialogFragment 實例的,只要加一些接口回調就可以了。

3.5 設置自己的想要的背景顏色

有時候因爲某些特殊原因,視覺需要換一個背景顏色,由於默認是黑色透明的,因此,需要將該顏色去掉。

只要在前面的代碼加上下面這行即可:

            // 設置蒙層爲完全透明
            window.setDimAmount(0);

同時,我們也對 xml 佈局進行了調整;

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="300dp"
        android:layout_height="500dp"
        android:layout_gravity="center"
        android:background="@color/dark_blue">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="Hello World!" />
    </FrameLayout>

</FrameLayout>

最終效果如下:

在 xml 裏面給父 view 設置半透明的紅色背景,最終顯示效果如下所示:

這和想象的優點區別,那要怎麼鋪滿全屏呢?機智如我的我馬上寫下下面的代碼:

            window.setBackgroundDrawable(new ColorDrawable(getActivity().getResources().getColor(R.color.trans_red)));
            window.getDecorView().setBackgroundDrawable(new ColorDrawable(getActivity().getResources().getColor(R.color.trans_red)));

 結果,還是一樣的。看來是哪裏不對。後來我終於知道是哪裏的問題,是因爲前面給 window 設置的 height 是 WRAP_CONTENT 類型的,於是導致一直不能屏幕。

具體原因可以參見我很早之前寫的博客:Android View 的繪製流程之 Measure 過程詳解 (一)

簡單來說,就是 window 高度是 at_most 類型,雖然子 view 是 match_parent,但是由於 window 的類型,導致 子 view 的 match_parent 失效變成了 at_most 類型,最下面的子 view 有設定高度,於是所有是 at_most 類型的最終都會變成子 view 設定的高度。

因此,解決問題的代碼如下:

            WindowManager.LayoutParams lp = window.getAttributes();
            lp.width = WindowManager.LayoutParams.MATCH_PARENT;
            lp.height = WindowManager.LayoutParams.MATCH_PARENT;
            window.setAttributes(lp);

最終效果如下所示:

但是,加了這行代碼帶來的問題是就會發現退出和進入的時候狀態欄顏色會發生變化,這個對於敏感的人羣來說,可能是一種不好的體驗。

關於狀態欄顏色,目前我沒發現有比較好的解決辦法,這個如果你們有解決了,可以評論區告訴我。要想不變色就是採用系統默認背景顏色,不要自己設置顏色,同時不要設置高度爲 match_parent 。

3.6 禁止動畫效果:

   // 禁止動畫
   window.setWindowAnimations(0);

加了該行代碼,在退出和進入的時候都不會有動畫效果。

相信到這裏,你對 DialogFragment 已經有了比較深的認識了。

 

參考文章

Android 官方推薦 : DialogFragment 創建對話框

手把手帶你玩轉 DialogFragment

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章