自定义Preference

说在前面

前面的一篇博文中,我们讲到了Preference的一些坑,虽然不多,但是我会保持更新的。

这几天写Hitokoto的过程中也熟悉了甚至可以说基本掌握了Preference的基本使用,数据的存储以及默认值的设置,都已经没有问题了,但是再写“显示当前”的时候,需要弹出一个Dialog,有了之前EditTextPreference的坑之后,我没有尝试,我想应该也会报一个主题的问题,毕竟当时的错误是报在AlertDialog上的,所以我有直接写。

我查了一下Android的文档,发现官方中似乎有一个叫做DialogPreference的控件,但是这个类是一个抽象类,不能实例化,而之前提到的常用的Preference中,EditTextPreference就是一个继承了DialogPreference重写的Preference,所以说,如果我们需要一个单纯的Dialog的话,我们需要自定义并且继承DialogPreference

提示

对于自定义控件,我想我一直都写的不怎么好,虽然说在我自己的工具箱中写了三个自定义控件,但是功能都比较单一。所以这次自定义Preference也遇到了不少问题,当然,截至现在(2017年3月16日21:35:59),我依旧没有解决,虽然这个控件大部分完成了。

自定义Preference

由于之前路由器的坑的原因,所以这次我直接选择官方的文档。
Android 框架包括各种 Preference 子类,您可以使用它们为各种不同类型的设置构建 UI。不过,您可能会发现自己需要的设置没有内置解决方案,例如,数字选取器或日期选取器。 在这种情况下,您将需要通过扩展 Preference 类或其他子类之一来创建自定义首选项。

扩展 Preference 类时,您需要执行以下几项重要操作:
指定在用户选择设置时显示的用户界面。
适时保存设置的值。
使用显示的当前(默认)值初始化 Preference。
在系统请求时提供默认值。
如果 Preference 提供自己的 UI(例如对话框),请保存并恢复状态以处理生命周期变更(例如,用户旋转屏幕)。

上面的是官方的说明,也正好给我们一个提示。

指定用户界面


自定义控件的第一步都是指定布局文件,当然,如果是基于原有控件进行修改的不需要,我也没写过不指定布局的,对于绘图那一块我一直没有去详细的研究……
在这里有四个默认构造函数,其中调用第一个和第四个的super方法需要sdk>=21,我也不知道为什么,但是为了低版本能用,所以我调用的是第三个。
在构造函数中我们需要指定我们的布局文件。
通过以下代码:

1
2
3
4
5
6
7
8
public TextDialogPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
setDialogLayoutResource(R.layout.mystery0_text_preference);//指定布局
setPositiveButtonText(R.string.mystery0_preference_content);//指定确认键文本
setNegativeButtonText(R.string.mystery0_preference_source);//指定取消键文本
setDialogIcon(null);//设置dialog图标
}

对于布局中控件的绑定,我们需要重写View onCreateDialogView()以及void onBindDialogView(View view)方法。
如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected View onCreateDialogView()
{
super.onCreateDialogView();
if (view != null) return view;
view = LayoutInflater.from(getContext()).inflate(
R.layout.mystery0_text_preference, null);
return view;
}

@Override
protected void onBindDialogView(View view)
{
super.onBindDialogView(view);

textView = (TextView) view.findViewById(R.id.mystery0_text_preference_view_content);
sourceView = (TextView) view.findViewById(R.id.mystery0_text_preference_view_source);
}

保存设置的值

在这里有几个方法,分别用于保存对应类型的值:

persist*()

其中的*对应类型,如persistInt()
对于这个方法的调用,一般是在Dialog关闭的时候保存数据。
对于我们的这个例子,我想写的只是将数据在Dialog中显示,所以并不需要存储数据,所以在这里我没写。
以下是Android的原文:

当 DialogPreference 关闭时,系统会调用 onDialogClosed() 方法。该方法包括一个布尔参数,用于指定用户结果是否为“肯定”;如果值为 true,则表示用户选择的是肯定按钮且您应该保存新值。 例如:
@Override
protected void onDialogClosed(boolean positiveResult) {
    // When the user selects "OK", persist the new value
    if (positiveResult) {
        persistInt(mNewValue);
    }
}
在此示例中,mNewValue 是一个类成员,可存放设置的当前值。调用 persistInt() 会将该值保存到 SharedPreferences 文件(自动使用在此 Preference 的 XML 文件中指定的键)。

初始化当前值

系统将 Preference 添加到屏幕时,会调用 onSetInitialValue() 来通知您设置是否具有保留值。如果没有保留值,则此调用将为您提供默认值。
onSetInitialValue()方法传递一个布尔值 (restorePersistedValue),以指示是否已为该设置保留值。 如果值为 true,则应通过调用 Preference 类的一个 getPersisted\*() 方法(如整型值对应的 getPersistedInt())来检索保留值。 通常,您会需要检索保留值,以便能够正确更新 UI 来反映之前保存的值。
如果 restorePersistedValuefalse,则应使用在第二个参数中传递的默认值。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue)
{
if (restorePersistedValue)
{
text = getPersistedString("test");
} else
{
text = (String) defaultValue;
}
}

注意:您不能使用 defaultValue 作为 getPersisted*()方法中的默认值,因为当 restorePersistedValuetrue 时,其值始终为 null

提供默认值

如果 Preference 类的实例指定一个默认值(使用 android:defaultValue 属性),则在实例化对象以检索该值时,系统会调用 onGetDefaultValue()。您必须实现此方法,系统才能将默认值保存在 SharedPreferences 中。

1
2
3
4
5
@Override
protected Object onGetDefaultValue(TypedArray a, int index)
{
return a.getString(index);
}

方法参数可提供您所需的一切:属性的数组和 android:defaultValue(必须检索的值)的索引位置。 之所以必须实现此方法以从该属性中提取默认值,是因为您必须为此属性指定在未定义属性值时所要使用的局部默认值。

显示

官方文档中还有保存首选项设置的代码重写,但是由于我这里用不到,所以没有去研究。
所以我直接重写了显示方法,也就是void showDialog(Bundle state),在之前,无论我实在那个函数中调用settext方法都会得到一个空指针的Exception,不得已,我只能将settext方法写在显示的方法中。如下:

1
2
3
4
5
6
7
@Override
protected void showDialog(Bundle state)
{
super.showDialog(state);
textView.setText(text);
sourceView.setText(source);
}

保存,调试,成功。
效果图:

后话

但是很快我便发现了一个异常,虽然显示的结果完全正常,但是在第二次点击的时候就报错。
以下是错误原文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:4417)
at android.view.ViewGroup.addView(ViewGroup.java:4258)
at android.view.ViewGroup.addView(ViewGroup.java:4230)
at com.android.internal.app.AlertController.setupCustomContent(AlertController.java:601)
at com.android.internal.app.AlertController.setupView(AlertController.java:495)
at com.android.internal.app.AlertController.installContent(AlertController.java:253)
at android.app.AlertDialog.onCreate(AlertDialog.java:423)
at android.app.Dialog.dispatchOnCreate(Dialog.java:395)
at android.app.Dialog.show(Dialog.java:294)
at android.preference.DialogPreference.showDialog(DialogPreference.java:297)
at com.mystery0.tools.TextDialogPreference.TextDialogPreference.showDialog(TextDialogPreference.java:93)
at android.preference.DialogPreference.onClick(DialogPreference.java:277)
at android.preference.Preference.performClick(Preference.java:994)
at android.preference.PreferenceScreen.onItemClick(PreferenceScreen.java:249)
at android.widget.AdapterView.performItemClick(AdapterView.java:310)
at android.widget.AbsListView.performItemClick(AbsListView.java:1156)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3128)
at android.widget.AbsListView$3.run(AbsListView.java:4043)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6126)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

我想我应该遇到这个错误很多次了,当时在学习Fragment的时候并没有认真去学,导致后来写fragment的时候非常依赖ide,同时fragment的相关错误我都不知道如何解决,而这个就是错误中的一种。
我想我会解决这个问题的。

坚持原创技术分享,您的支持将鼓励我继续创作!