support包中preference下自定义原生alertdialog解决方案

场景

最近在开发UI控件库中需要对perference自定义样式,而EditTextPreference以及ListPreference等均使用到alertdialog,则需要对alertdialog的样式进行自定义修改

首先从demo出发,Android  Support包源码中sample并没有对perference作demo,网罗的一些demo,其中不乏两种方式使用support-preference

  1. preferences-demo
  2. platform_packages_apps_settings

preferences-demo采用的是AppCompatActivity – PreferenceFragmentCompat的搭配,这个时候你要使用appcompat的theme

platform_packages_apps_settings(android8.0 下settings app源码)采用的是Activity-PreferenceFragment的搭配(这个需要仔细去阅读android8.0 settings的源码,在这里不做详述),可以在activity下使用

使用这两种方式去展示时,会发现Activity-PreferenceFragment的搭配是不能通过在theme下重写alertdialogstyle的样式布局去改变perference控件(例如android.support.v7.preference.EditTextPreference、android.support.v7.preference.ListPreference)弹出的alertdialog。而AppCompatActivity – PreferenceFragmentCompat是可以的

那么为什么会这样?我们从两者的源码出发去探索

android.support.v7.preference. PreferenceFragmentCompat 源码:

@Override
public void onDisplayPreferenceDialog(Preference preference) {
    .....
    final DialogFragment f;
    if (preference instanceof EditTextPreference) {
        f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());
    } else if (preference instanceof ListPreference) {
        f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
    } else if (preference instanceof AbstractMultiSelectListPreference) {
        f = MultiSelectListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
    } else {
        throw new IllegalArgumentException("Tried to display dialog for unknown " +
            "preference type. Did you forget to override onDisplayPreferenceDialog()?");
    }
    .....
}

android.support.v14.preference.PreferenceFragment源码:

@Override
public void onDisplayPreferenceDialog(Preference preference) {
    ....
    final DialogFragment f;
    if (preference instanceof EditTextPreference) {
   	f = EditTextPreferenceDialogFragment.newInstance(preference.getKey());
    } else if (preference instanceof ListPreference) {
   	f = ListPreferenceDialogFragment.newInstance(preference.getKey());
    } else if (preference instanceof MultiSelectListPreference) {
    	f = MultiSelectListPreferenceDialogFragment.newInstance(preference.getKey());
    } else {
    	throw new IllegalArgumentException("Tried to display dialog for unknown " +
            "preference type. Did you forget to override onDisplayPreferenceDialog()?");
    }
    ....
}

即v14包中在PreferenceFragment下使用了EditTextPreferenceDialogFragment、ListPreferenceDialogFragment、MultiSelectListPreferenceDialogFragment,而他们继承了PreferenceDialogFragment

同样的 v7包中在PreferenceFragmentCompat下使用了EditTextPreferenceDialogFragmentCompat、ListPreferenceDialogFragmentCompat、MultiSelectListPreferenceDialogFragmentCompat,而他们继承了PreferenceDialogFragmentCompat。

那么我们接下来看一下 PreferenceDialogFragment 和 PreferenceDialogFragmentCompat 的源码:

android.support.v14.preference. PreferenceDialogFragment 源码:

@Override
public @NonNull Dialog onCreateDialog(Bundle savedInstanceState) {
    final Context context = getActivity();
    mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;

    final android.app.AlertDialog.Builder builder = new AlertDialog.Builder(context)
            .setTitle(mDialogTitle)
            .setIcon(mDialogIcon)
            .setPositiveButton(mPositiveButtonText, this)
            .setNegativeButton(mNegativeButtonText, this);
	//...省略
    return dialog;
}

 

android.support.v7.preference.PreferenceDialogFragmentCompat源码:

@Override
public @NonNull Dialog onCreateDialog(Bundle savedInstanceState) {
    final Context context = getActivity();
    mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;

    final android.support.v7.app.AlertDialog.Builder builder = new AlertDialog.Builder(context)
            .setTitle(mDialogTitle)
            .setIcon(mDialogIcon)
            .setPositiveButton(mPositiveButtonText, this)
            .setNegativeButton(mNegativeButtonText, this);
    //...省略 
    return dialog;
 }

v14下的PreferenceDialogFragment 使用的是android.app.AlertDialog,而v7下的PreferenceDialogFragmentCompat使用的是android.support.v7.app.AlertDialog

那么,接下来问题便转化成:

原生AlertDialog和v7下的AlertDialog有什么不同

我们知道,AlertDialog的源码使用了建造者模式,用到了AlertController去进行控制

com.android.internal.app. AlertController源码:

protected AlertController(Context context, DialogInterface di, Window window) {
    ...
    final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
                com.android.internal.R.attr.alertDialogStyle, 0);
    ...
}

 

android.support.v7.app. AlertController源码:

public AlertController(Context context, AppCompatDialog di, Window window) {
	...
    final TypedArray a = context.obtainStyledAttributes(null, R.styleable.AlertDialog,
                android.support.v7.appcompat.R.attr.alertDialogStyle, 0);
	...
}

于是乎比较原生和v7下的AlertController会发现原生使用的是com.android.internal.R.styleable.AlertDialog,我们是无法通过更改alertdialogstyle去修改原生的样式的,虽然官方在官方文档中有提供如下api

AlertDialog.Builder(Context context, int themeResId)

Creates a builder for an alert dialog that uses an explicit theme resource.

但是AlertDialog在v14中是以方法的局部变量使用的,这就导致了v14 PreferenceDialogFragment下使用的AlertDialog是无法通过在theme下重写alertdialogstyle的样式布局去改变perference控件样式布局。而v7使用的AlertDialog是可以通过在theme下重写alertdialogstyle这个style改变其样式以及布局的。

 

梳理一下

也即是说,在android.support.v14.preference.PreferenceFragment使用preference控件,弹出的AlertDialog会使用原生的样式以及布局,这是无法通过调用api改变的,而PreferenceFragment继承了android.app.Fragment,可以在Activity下使用,所以如果你在Activity-PreferenceFragment这套方案下使用时,无法改变AlertDialog的样式。

在v7的PreferenceFragmentCompat使用preference控件,弹出的AlertDialog是可以通过在theme下重写alertdialogstyle的样式布局去改变的

解决方案

那么接下来,为了适配更多的方式,我需要在android.support.v14.preference.PreferenceFragment弹出的AlertDialog去改变其样式和布局,想到了两种策略:

  1. hook注入,偷梁换柱
  2. 重新写一套v14下的PreferenceFragment,包括android.app.AlertDialog,改为自己使用的样式(其实在UI库中已经将android.app.AlertDialog重写了,所以这套方案那没有想象之中的困难)

api-hook这套方案需要用反射进去对android.app.AlertDialog进行修改

hook点如果是android.app.AlertDialog那么对整个R文件需要修改,工程量很大。

退一步,假如在android.support.v14.preference.PreferenceDialogFragment 的onCreateDialog(Bundle savedInstanceState)进行修改,那么其中AlertDialog为方法的局部变量,也没有办法反射,需要直接拿到onCreateDialog下的所有变量,对整个方法进行偷梁换柱,这种方法可行,但工程量较大。

接下来想到了还有aop进行hook注入,同样工作量不小。

而且如果使用hook的话,那么假如app接入了UI库运行在各种Android手机上,各种手机会对系统源码做定制,假如修改了android.app.AlertDialog的代码,而又对他进行hook时,这样就不安全了。

于是最后的方案选择了第二个方案:重新写一套v14下的PreferenceFragment

 

这次的思路应该一路下来看源码和做修改没有大差错,好处是自己熟悉系统以及兼容包源码中preference和alertdialog下配合使用的部分,也了解hook的一些局限性和导致的后果

附,参考文章:

android preference:

Android Preference 设置偏好全攻略

hook:

Android插件化原理解析——Hook机制之动态代理

理解 Android Hook 技术以及简单实战

 


6 条评论

昵称
  1. 匿名

    优秀

  2. 1

    特特牛逼

  3. 匿名

    问题不大

  4. 匿名

    写的很nice

  5. 匿名

    写的不错

  6. 爸爸

    fb(fucking boy)(face book)(face brother)