我们都知道要想让一个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后弹出的页面显示效果:
那么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了。
通过断点调试(没有对应调试机源码的话就用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:windowIsFloating为true。
查看一个基础的Dialog的Theme内容,其中关于window的属性有如下这些:
-- SDK/platforms/android-25/data/res/values/themes.xml -->
可以看到,其中除了定义android:windowIsFloating,还指定了其他一些值,那些值具体是控制什么的,在那些类中被使用就不去探查了,待有需要时再查看吧(虽然根据以window开始的名字猜测其最有可能也是在Window或者PhoneWindow)。