Preference 使用小结
在Symbian实现类似如下配置参数的设置界面
需要复杂的自定义列表来实现,在android中由于SDK封装和提供了一套基于Preference的类,使用Preference通过编辑xml配置文件,只要很少的代码就可以实现了,而且Preference本身已经实现了参数保存,不需要我们再考虑将参数保存文件,下面让我们来认识下Preference。
PreferenceActivity布局文件
Preference需要通过Activity才能显示出来,SDK封装了一个抽象类PreferenceActivity专门提供我们派生自己需要的Activity。和Activtiy需要layout布局一样,这里的PreferenceActivity实例化的时候也是需要XML布局文件,该布局文件可以通过“File”“New”“Android XML File”菜单弹出如下对话框来生成
在资源类型中选择Preference,在root element中保持默认即选择PreferenceScreen,否则在Activity中绑定该资源时,将报“java.lang.RuntimeException: Unable to start activity……”的类似错误。
其实在Preference XML资源文件中,元素标签(element)类型主要有有两类:一类是管理布局的有PreferenceScreen和PreferenceCategory;另一类是具体的设置元素,有CheckBoxPreference、ListPreference、EditTextPreference和RingtonePreference等。假设我们要实现如下图所示的效果
首先,我们需要生成一个Preference资源文件,命名为preferencescategory.xml,具体内容如下
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="Settings">
<PreferenceCategory
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="Emotions"
android:summary="settings about emotions">
<CheckBoxPreference
android:title="Love me?"
android:summaryOn="Yes,I love you!"
android:summaryOff="No,I am sorry."
android:defaultValue="true" android:key="@string/category_loveme_key">
</CheckBoxPreference>
<CheckBoxPreference
android:title="Hate me?"
android:summaryOn="Yes,I hate you!"
android:summaryOff="No,you are a good person."
android:defaultValue="false">
</CheckBoxPreference>
</PreferenceCategory>
<PreferenceCategory
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="Relations"
android:summary="settings about relations">
<CheckBoxPreference
android:title="Family?"
android:summaryOn="Yes,we are family!"
android:summaryOff="No,I am sorry."
android:defaultValue="true">
</CheckBoxPreference>
<CheckBoxPreference
android:title="Friends?"
android:summaryOn="Yes,we are friends!"
android:summaryOff="No,I am sorry."
android:defaultValue="false">
</CheckBoxPreference>
</PreferenceCategory>
</PreferenceScreen>
其次,我们从PreferenceActivity派生一个PreferenceCategoryActivity类,具体代码如下
public class PreferenceCategoryActivity extends PreferenceActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferencescategory);
}
}
再次,将这个Activity添加到AndroidManifest.xml中,假设该Activity被设置为起始Activity,那么程序一运行就呈现上述界面。
Preference配置参数的保存
其实通过上述的列子,我们可以看到当修改参数配置后,退出程序,再重新进入程序,出现的配置参数是更改以后,而并非我们初始化设置的。这个参数配置保存功能是怎么实现的呢?答案是SDK提供了一个SharedPreferences来实现上述功能的。所以很多参考书中将SharedPreferences与文件和SQLite一起被放置在Android数据存储章节,用以获取和修改持久化存储的数据。需要注意的是这种方式主要用来存储比较简单的一些数据,而且是标准的Boolean、Int、Float、Long、String等类型
那么这些参数具体保存在哪里呢?通过模拟器我们可以发现,在data/data/包名/shared_prefs/下面存在着若干xml文件,具体如下图所示
Android就是靠这些文件来实现参数配置的保存的。我们如何通过SharedPreferences来实现对这些文件访问和修改呢。
SDK提供了三个获取SharedPreferences的函数,分别是
public SharedPreferences getPreferences (int mode)
public SharedPreferences getSharedPreferences (String name, int mode)
public static SharedPreferences getDefaultSharedPreferences (Context context)
前两个是非静态类,需要通过具体的Acitvity对象或者在Activity对象内调用,最后一个可以通过静态方法调用。
第一个getPreferences函数,操作的是属于Activity自身的Preference参数配置文件,文件名是Actvity的类名,一个Activity只能有一个该类配置文件,比如上述图示中的PreferenceDemoActivity.xml就是属于PreferenceDemoActivity参数配置文件。
第二个getSharedPreferences函数,操作的是属于整个应用程序的参数配置文件,一个应用程序可以包含有多个该类配置文件,文件名是函数第一参数的name。在上述图示中就是形如loveme.xml这类文件。
第三个getDefaultSharedPreferences函数,操作的也是属于整个应用程序的参数配置文件,不过该类文件一个应用程序只有一个,文件名为“包名_preferences”,这类配置文件就是用来保存Preference布局文件中元素定义的参数配置的。
有了SharedPreferences之后,我们就可以通过其提供的如下接口函数来获取储存的配置参数。
boolean getBoolean(String key, boolean defValue);
float getFloat(String key, float defValue);
long getLong(String key, long defValue);
int getInt(String key, int defValue);
String getString(String key, String defValue);
Map<String, ?> getAll();
也可以通过SharedPreferences.Editor来修改配置参数,具体见如下代码
SharedPreferences vPreferences = getSharedPreferences(checkbox_key, Activity.MODE_PRIVATE);
boolean vLoveme = vPreferences.getBoolean(checkbox_key, true);
SharedPreferences.Editor editor = vPreferences.edit();
editor.putBoolean(checkbox_key, false);
editor.commit();
拦截监听接口
当PreferenceActivity中的内容改变时,Android系统会自动进行保存和持久化维护,我们只需要在要用的设置界面中需要数据的地方进行读取就可以了。同时Android还提供了OnPreferenceClickListener和OnPreferenceChangeListener两个与Preference相关的监听接口,当PreferenceActivity中的某一个Preference进行了点击或者改变的操作时,都会回调接口中的函数,这样可以第一个时间向其他Activity等通知系统设置进行了改变。下面提供一份拦截监听的代码
public class PreferenceDemoActivity extends PreferenceActivity implements OnPreferenceChangeListener,
OnPreferenceClickListener
{
/** Called when the activity is first created. */
CheckBoxPreference vCheckBox;
String checkbox_key;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// setContentView(R.layout.main);
addPreferencesFromResource(R.xml.pref);
checkbox_key = getResources().getString(R.string.love_me);
vCheckBox = (CheckBoxPreference)findPreference(checkbox_key);
//注册修改函数
vCheckBox.setOnPreferenceChangeListener(this);
vCheckBox.setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue)
{
// TODO Auto-generated method stub
//判断是哪个Preference改变了
if(preference.getKey().equals(checkbox_key))
{
}
else
{
//如果返回false表示不允许被改变
return false;
}
//返回true表示允许改变
return true;
}
@Override
public boolean onPreferenceClick(Preference preference) {
// TODO Auto-generated method stub
//判断是哪个Preference被点击了
if(preference.getKey().equals(checkbox_key))
{
}
else
{
return false;
}
return true;
}
}
通过调用发现当点击CheckBox时,先调用onPreferenceChange,之后再调用onPreferenceClick,所以个人感觉,很多情况如果只对参数感兴趣,可以不用拦截点击监听。
以上是对Preference基础使用的小结,系统提供的Preference毕竟太少,为此我们通常需要用到自定义的Preference。下面就介绍自定义Preference的使用。
自定义Preference
系统提供的Preference样式还是少了点,为了呈现丰富的UI,很多时候我们需要自定义Preference,自定义Preference有两种实现方法:第一种实现方法仅通过资源xml的修改来实现自定义Preference的效果;第二种方法是通过派生类的方法来实现自定义Preference的效果。下面分别阐述如下:
修改资源的方法
系统默认的Preference风格是黑底白字的样式,有时候我们需要改变下字体颜色或者字体类型,抑或我们想要修改下CheckBox的图标不是系统自带的勾子,而是自定义的图标,假设入下图的效果
那么该如何实现呢?
上述样式的实现,借助于每个Preference的android:layout和android:widgetLayout属性,给其重新布局,重新布局的时候需要参考frameworks\base\core\res\res下面的原有布局,否则在不清楚其资源格式的情况下进行修改往往达不到效果,通过这里的尝试,我发现假设自定义的资源在在加载过程中出错或者类似解析不符,那么系统会默认加载缺省的资源。
下面针对上图自定义preference中的第一个CheckBoxPreference的实现,给出参考了frameworks\base\core\res\res\layout文件夹下面的preference.xml布局文件,增添文本颜色等参数而重构的custom_preferece_layout.xml文件的内容(其中的红色字体就是原有基础上新增加的属性)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingRight="?android:attr/scrollbarSize">
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dip"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:layout_weight="1">
<TextView android:id="@+android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:textColor="#00FF00" />
<TextView android:id="@+android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignLeft="@android:id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#FF0000"
android:textStyle="bold|italic"
android:maxLines="4" />
</RelativeLayout>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout android:id="@+android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:orientation="vertical" />
</LinearLayout>
如上修改很少,但是效果达到了。至于将默认的打勾图标,切换成自己的图标,也是依样画葫芦,找到源码frameworks\base\core\res\res\layout文件夹下的preference_widget_checkbox.xml,然后重构为custom_check_widget.xml,内容如下(其中红色字体时原有布局基础上新增加的,关于其中button的实现就不贴出来,参考demo程序吧)
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@drawable/selfcheckbtn"
android:layout_marginRight="4dip"
android:layout_gravity="center_vertical"
android:focusable="false"
android:clickable="false" />
在Preference的资源文件中定义这个CheckBoxPreference的代码就如下所示
<CheckBoxPreference
android:defaultValue="true"
android:summaryOff="禁止自动搜索"
android:summaryOn="允许自动搜素"
android:key="auto_search_enable_key"
android:title="自动搜素"
android:disableDependentsState="false"
android:layout="@layout/custom_preferece_layout"
android:widgetLayout="@layout/custom_check_widget">
</CheckBoxPreference>
通过修改资源布局来实现自定义Preference的样式,假如对Android的资源深入了解后实现起来就能随心所欲而且手到擒来了,具体就不展开了,详见示例程序。
派生类的方法
上面方法仅仅从资源的角度去修改Preference的样式,其实SDK提供了Preference基类,我们除了可以使用系统提供的CheckBoxPreference、ListPreference、EditTextPreference和RingtonePreference外,还可以自己定义我们需要的Preference。遵循从资源到代码的逻辑,使用系统自带的CheckBoxPreference我们可以方便地再xml文件中使用“CheckBoxPreference”元素来定义,那么派生类Preference的资源元素该用什么标签呢?
自定义的Preference的资源元素标签就是自定义Preference的包名加类名,下面我们实现一个自定义的带图标的设置项,由于类名为ImageOptionPreference,而包名为netease.frank.demo.selfImagePreference,所以资源中设置的元素名为netease.frank.demo.selfImagePreference.ImageOptionPreference,具体的xml文件内容如下
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<netease.frank.demo.selfImagePreference.ImageOptionPreference
android:title="发送短信"
android:summary="点击按钮将切入短信发送界面"
android:key="game_pic"
android:widgetLayout="@layout/preference_widget_image"/>
</PreferenceScreen>
在xml中用到一个widgetLayout,其xml文件内容如下所示
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ImageView
android:id="@+id/pref_current_img"
android:src="@drawable/icon"
android:layout_marginRight="4dip"
android:layout_height="54dip"
android:padding="2dip"
android:layout_width="54dip"
android:layout_gravity="center_vertical"
android:focusable="false"
android:clickable="false" />
</LinearLayout>
类定义代码,我们需要提供一个构造函数和一个点击时操作函数(在这里我们让其发送一条短信)既可,简单代码如下
public class ImageOptionPreference extends Preference
{
public ImageOptionPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
}
@Override
protected void onClick()
{
// super.onClick();
Uri uri = Uri.parse("smsto:0800000123");
Intent it = new Intent(Intent.ACTION_SENDTO, uri);
it.putExtra("sms_body", "The SMS text");
this.getContext().startActivity(it);
}
}
这样我们就可以如同SDK提供的CheckboxPreference一样在PreferenceActivity中使用我们自己的定义的ImageOptionPreference。
上面这个自定义ImageOptionPreference派生自Preference,很多情况下我们不需要从这么底层的类派生,而从DialogPreference派生我们的需求类就可以了,下面演示一个时间设置的空间。我们将这个时间设置类封装为类名为TimePreference,而包名为 netease.frank.demo.selfImagePreference,我们将其放置在上述这个ImageOptionPreference的下面一项,在selfimagepre.xml的定义就如下所示
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<netease.frank.demo.selfImagePreference.ImageOptionPreference
android:title="发送短信"
android:summary="点击按钮将切入短信发送界面"
android:key="game_pic"
android:widgetLayout="@layout/preference_widget_image"/>
<netease.frank.demo.selfImagePreference.TimePreference
android:key="time_test"
android:summary="打开修改时间"
android:title="时间设置"
android:defaultValue="1000000"/>
</PreferenceScreen>
上面的android:defaultValue是当参数没有设置情况下的缺省值。自定义DialogPreferenc自然我们还需要一个满足我们需求的对话框布局文件,这里的时间设置对话框布局time_preference.xml文件,具体代码如下所示
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TimePicker
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/timePicker_preference"
android:layout_centerHorizontal="true">
</TimePicker>
</RelativeLayout>
而相应的类的实现代码,如下所示:
public class TimePreference extends DialogPreference
{
TimePicker mPicker = null;
long mValue ;
public TimePreference(Context context, AttributeSet attrs)
{
super(context, attrs);
//设置对话框需要加载的布局文件
setDialogLayoutResource(R.layout.time_preference);
}
//弹出对话框的时候进行初始化
@Override
protected void onBindDialogView(View view)
{
super.onBindDialogView(view);
mPicker = (TimePicker)view.findViewById(R.id.timePicker_preference);
if(mPicker != null)
{
mPicker.setIs24HourView(true);
long value = mValue;
Date d = new Date(value);
mPicker.setCurrentHour(d.getHours());
mPicker.setCurrentMinute(d.getMinutes());
}
}
//关闭对话框的时候保存
@Override
protected void onDialogClosed(boolean positiveResult)
{
super.onDialogClosed(positiveResult);
if(positiveResult)
{
Date d = new Date(0, 0, 0, mPicker.getCurrentHour(), mPicker.getCurrentMinute(), 0);
long value = d.getTime();
mValue = value;
if(callChangeListener(value))
{
SharedPreferences.Editor vEditor = getEditor();
vEditor.putLong(getKey(), value);//(checkbox_key, false);
vEditor.commit();
}
}
}
//获取缺省的配置参数
@Override
protected Object onGetDefaultValue(TypedArray a, int index)
{
mValue = Long.parseLong(a.getString(index));
return mValue;
}
//获取sharepreference中的配置参数,该函数在配置文件存在时才会被调用
@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue)
{
long value;
if(restorePersistedValue)
value = getPersistedLong(1000000);
else
{
value = Long.parseLong(defaultValue.toString());
}
setDefaultValue(value);
mValue = value;
}
}
在上述的TimePreference类的代码过程中,需要注意以下几点:
onGetDefaultValue的调用,先于TimePreference的构造函数之前运行,从而确保缺省参数的获得;
查看一下DialogPreference的onClick函数的实现,我们就知道在这里我们为什么不用重载OnClick,而是重载了onBindDialogView函数就可以了,同理如果我们想重载OnClick函数也是可行的;
虽然获取参数,系统帮我们实现了,但是保存参数的操作,还是需要我们自己来提供,所以在对话框关闭的onDialogClosed,我们对设置的时间值进行了保存;
关于“包名_preferences.xml”参数配置文件,程序一开始运行的时候是不存在的,所以第一次运行程序时,程序不会调用onSetInitialValue,只有当程序执行过一次保存后,参数配置文件才被创建,从而才会被执行调用。
关于Preference就介绍到这里,非常感谢老华的指点。
另,本小结提供三个demo程序,分别如下
PreferencesDemo 用于演示和说明系统原有的Preference的特性和使用
PreferenceDemo 用于演示和说明Preference的三种SharedPreferences保存类型
selfImagePreferenceDemo 用于演示和说明自定义Preference的两种方法