Android Dialog显示成Activity(全屏)

前言

我们都知道要想让一个Activity显示成为Dialog的样式可以对Activity指定Dialog的主题(Theme),但今天使用PreferenceScreen时发现内嵌的PreferenceScreen将一个对话框(Dialog)显示出了Activity的效果。这是怎么做到的呢,今天就来分析一下其中的原理。

现象

在preference的配置文件中配置内嵌的PreferenceScreen标签。如下:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <ListPreference
        android:defaultValue="180"
        android:entries="@array/pref_sync_frequency_titles"
        android:entryValues="@array/pref_sync_frequency_values"
        android:key="sync_frequency"
        android:negativeButtonText="@null"
        android:positiveButtonText="@null"
        android:title="@string/pref_title_sync_frequency" />

    <PreferenceScreen android:title="preference screen"
        android:key="pref_screen_key">
        <Preference android:title="@string/pref_title_system_sync_settings">
            <intent android:action="android.settings.SYNC_SETTINGS" />
        Preference>
    PreferenceScreen>


PreferenceScreen>

点击Preference Screen后弹出的页面显示效果:
Android Dialog显示成Activity(全屏)_第1张图片

PreferenceScreen#onClick

那么PreferenceScreen是怎样处理点击事件的呢,源码如下(SDK/sources/android-25/android/preference/PreferenceScreen.java):

    @Override
    protected void onClick() {
        if (getIntent() != null || getFragment() != null || getPreferenceCount() == 0) {
            return;
        }

        showDialog(null);
    }

    private void showDialog(Bundle state) {
        Context context = getContext();
        if (mListView != null) {
            mListView.setAdapter(null);
        }

        LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View childPrefScreen = inflater.inflate(mLayoutResId, null);
        View titleView = childPrefScreen.findViewById(android.R.id.title);
        mListView = (ListView) childPrefScreen.findViewById(android.R.id.list);
        if (mDividerSpecified) {
            mListView.setDivider(mDividerDrawable);
        }

        bind(mListView);

        // Set the title bar if title is available, else no title bar
        final CharSequence title = getTitle();
        Dialog dialog = mDialog = new Dialog(context, context.getThemeResId());
        if (TextUtils.isEmpty(title)) {
            if (titleView != null) {
                titleView.setVisibility(View.GONE);
            }
            dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        } else {
            if (titleView instanceof TextView) {
                ((TextView) titleView).setText(title);
                titleView.setVisibility(View.VISIBLE);
            } else {
                dialog.setTitle(title);
            }
        }
        dialog.setContentView(childPrefScreen);
        dialog.setOnDismissListener(this);
        if (state != null) {
            dialog.onRestoreInstanceState(state);
        }

        // Add the screen to the list of preferences screens opened as dialogs
        getPreferenceManager().addPreferencesScreen(dialog);

        dialog.show();
    }

其中显示Dialog的代码与平常使用Dialog并无太大区别。但为何平时我们使用时就是Dialog样式,而这儿就是Activity的样式呢?

经过仔细debug后,最终发现这儿使用Dialog的方式与平时我自己使用还是不太一样的。最重要的区别就在于Dialog的创建方式了!如下:

Dialog dialog = mDialog = new Dialog(context, context.getThemeResId());

这是该处实例化Dialog的代码,其中使用了Dialog的两个参数的构造方式。有context和一个theme的resource id。
而平时我们使用时一般使用的方式是一个参数的构造方法,如下:

Dialog dialog = new Dialog(context);

那么问题终于找到了,原来是因为构造时传了一个theme导致的。

从代码可以看出传入的theme应该是当前activity对应的theme,那么是否意味着让Dialog显示成Activity效果的方式也与让Activity显示成Dialog的方式一致——使用对应的Activity(Dialog)的Theme就可以了?

答案是肯定的,于是我们也可以使用Activity的Theme将Dialog也显示为Activity了。

Theme中有什么魔法

通过断点调试(没有对应调试机源码的话就用Log输出的方式吧…),在Dialog显示全屏后中断执行,查看了其中一些变量的值。
因为想到最终我们看到的Dialog和Activity效果最大的不同是在其宽高是否填满了手机屏幕,因此着重查看了Dialog的window的相关属性。调试过程中发现了一个重要信息。

对于当前显示成Activity的Dialog,其window的attr中宽高属性为-1(MATCH_PARENT)

getWindow().getAttributes().width = -1
getWindow().getAttributes().height = -1

而对于正常创建的Dialog,其window的attr中宽高属性却不是-1的。因此追查对width变量赋值的代码,追查到PhoneWindow中的一段代码,如下:

//SDK/sources/android-25/com/android/internal/policy/PhoneWindow.java

        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                & (~getForcedWindowFlags());
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        } else {
            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
        }

其中的setLayout(WRAP_CONTENT, WRAP_CONTENT);即为对attr的width和height赋值的方法。可以看到,该处先从属性集中获取了theme中定义的windowIsFloating属性,然后判断若是floating(浮动)的,就设置window的attr参数宽高为WRAP_CONTENT。

setLayout定义在PhoneWindow的父类Window中,定义如下:

    /**
     * Set the width and height layout parameters of the window.  The default
     * for both of these is MATCH_PARENT; you can change them to WRAP_CONTENT
     * or an absolute value to make a window that is not full-screen.
     *
     * @param width The desired layout width of the window.
     * @param height The desired layout height of the window.
     *
     * @see ViewGroup.LayoutParams#height
     * @see ViewGroup.LayoutParams#width
     */
    public void setLayout(int width, int height) {
        final WindowManager.LayoutParams attrs = getAttributes();
        attrs.width = width;
        attrs.height = height;
        dispatchWindowAttributesChanged(attrs);
    }

从注释中可以看到,若未指定宽高,就会使用MATCH_PARENT,即为填满手机窗口。

所以,可以断定,在Dialog的Theme中肯定是指定了android:windowIsFloatingtrue

查看一个基础的Dialog的Theme内容,其中关于window的属性有如下这些:

-- SDK/platforms/android-25/data/res/values/themes.xml -->

可以看到,其中除了定义android:windowIsFloating,还指定了其他一些值,那些值具体是控制什么的,在那些类中被使用就不去探查了,待有需要时再查看吧(虽然根据以window开始的名字猜测其最有可能也是在Window或者PhoneWindow)。

你可能感兴趣的:(技术杂记)