Android Settings(Preferences)开发

Posted createchance

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Settings(Preferences)开发相关的知识,希望对你有一定的参考价值。

android的app开发中,很多时候我们的app需要提供用户一个用户体验统一的,友好的setting界面,也就是设置界面。在android系统中,就有一个settings设置的系统应用,专门用户设置系统的一些用户属性。那么这样的setting界面我们怎么开发呢?android为我们提供了一个程序员非常友好的preferences开发包:android.preference。这个开发包中,包含了众多我们在开发app的设置界面时需要的类,这个包的详情页:
https://developer.android.com/reference/android/preference/package-summary.html
下面我们结合google的开发指导文档,讲解一下android中的preferences设置界面的开发。Google的开发知道文档:
https://developer.android.com/guide/topics/ui/settings.html
另外,在开发setting界面的时候,这个界面的设计也是有一定的设置哲学的,在进行自定义开发的时候最好遵循这些设置哲学,不至于使你的app界面显得太过于突兀。Google关于Setting界面设置的建议文档:
https://material.google.com/patterns/settings.html
如果你想打造更好HMI的app以及线上web站点的话,那么你最好遵循这些设计原则。
我的demo代码地址:
https://github.com/CreateChance/AndroidSettingsDemo

为什么使用preferences来开发?

这是一个好问题,人们在做一些事情之前总是喜欢问下为什么。在通常app的开发过程中,我们都会为我们的app提供一个设置界面,易方便用户对我们的应用做一些必要的个性化的设置。但是很多时候,我们需要提供的设置项很多,关系也有些复杂,如果自己通过布局文件的形式去做的话,会比较麻烦,会使得UI布局的层级不够清晰,不容易日后维护和升级。因此,Google为了减轻广大开发者在开发setting界面时的困苦,google推出了preferences包。preferences包,顾名思义,就是使用android中的sharedpreferences技术来将我们的设置数据进行本地持久化,并且提供了内部封装的自动数据同步逻辑,使得我们的开发者只关心自己的app需要有那些设置,这些设置项的位置在哪里等业务逻辑,并不用关系数据怎么存储,数据怎么同步的非业务逻辑,这样一来,就大大减少了。但是由于这里使用的是sharedpreferences技术,也就是xml技术来存储数据的,因此如果你的数据结构和逻辑比较复杂的话那么你最好使用sqlite数据库来存储。这里我们总结一下preferences包的好处:
1. 简化setting界面开发,使得界面和逻辑解耦
2. 简化需要保存的数据结构和逻辑比较简单的情况下的轻量级存储的开发

开发的步骤

使用preferences包来开发基本不需要开发者了解sharedpreferences持久化技术,但是如果你了解的话,可以帮助你更好地理解preferences setting的开发。
在开始之前,我们先看下android.preference包中有哪些类:

这些类中,PreferenceActivity和PreferenceFragment是我们界面开发重点关注的类,他们是构建设置界面的基础。这里需要说明一下,从android 3.0(api 11)之后,google一直建议开发使用fragment开发不要使用activity开发,因为fragment的开发使得界面编程更加灵活,代码更加清晰易懂。因此在设置界面开发过程中,google也是推荐使用PreferenceActivity内嵌PreferenceFragment方式进行开发。其中PreferenceActivity只是负责设置选项的设置,具体的设置界面和逻辑需要使用PreferenceFragment完成。

android提供的设置界面组件

android提供了很多用于构建设置界面的ui组件,这些组件就在上面给出的包图中,那些以常用view组件名开头的preference就是android提供的设置界面组件。下面3个是最常用的组件:
1. CheckBoxPreference
这是一个提供check box的设置界面组件,使得用户可以通过勾选的方式进行设置。
2. ListPreference
这个组件当用户单击的时候会弹出一个对话框让用户以单选的形式进行选择。
3. EditTextPreference
这个组件当用户单击的时候会弹出一个对话框,对话框上有一个edit text组件,让用户输入。
下面我给出的demo中就会演示这3个组件的使用,其他的组件使用方式大同小异。

开发第一步:定义PreferenceActivity的header布局xml

前面我们说道,PreferenceActivity在3.0+系统上,只是负责设置界面的显示,不负责具体设置项的问题。因此,为了让PreferenceActivity显示我们的设置项,我们需要定义header文件,这个文件存放在res的xml目录下:
preference_headers.xml

<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
    <header
        android:fragment="com.createchance.androidsettingsdemo.PrefsFragmentOne"
        android:icon="@mipmap/ic_launcher"
        android:title="设置1"
        android:summary="第一个设置"/>

    <header
        android:fragment="com.createchance.androidsettingsdemo.PrefsFragmentTwo"
        android:icon="@mipmap/ic_launcher"
        android:title="设置2"
        android:summary="第二个设置"/>

</preference-headers>

这里我放了两个设置:设置1和设置2,并且每个设置下面都有一个summary描述。同时需要说明的是,上面的文件只是定义了两个选项,但是这两个选项中的内容是需要通过fragment来实现的,因此这里我指定了实际处理的fragment类。
定义完header文件后我们就可以在PreferenceActivity的子类中复写onBuildHeaders和isValidFragment这两个方法。onBuildHeaders方法是用来加载并且显示上面我们定义的header文件的,isValidFragment这个方法即使告诉是系统目前的这个fragment是不是一个有效的fragment,这两个方法都是强制必须实现的。下面是我的代码:

public class MainActivity extends PreferenceActivity 

    @Override
    public void onBuildHeaders(List<Header> target) 
        super.onBuildHeaders(target);

        loadHeadersFromResource(R.xml.preference_headers, target);
    

    @Override
    protected boolean isValidFragment(String fragmentName) 
        Log.d("DEBUG", "isValidFragment, fragmentName: " + fragmentName);

        return true;
    

可以看到,MainActivity根本都不用复写onCreate方法和调用setContentView方法设置布局。
运行之后可以看到这样的界面:

开发第二步:定义preference布局xml

上面的header只是定义了设置项,但是设置项中有什么我们还没有定义。现在我们就要定义这个布局文件,上面的header文件指定了处理的fragment,因此当用户点击上面的设置项的时候,就会实例化相应的fragment并且添加到当前的activity中显示。
preference布局文件必须以PreferenceScreen为根,其中的元素可以是上面我们提到的3个常见组件,并且preference提供了布局分组功能,也就是说可以让我们把几个设置归为一个类别进行分类显示,这个使用PreferenceCategory描述,下面是我的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <RingtonePreference
        android:ringtoneType="all"
        android:title="@string/ringtone_setting_title"
        android:summary="@string/ringtone_setting_summary"
        android:showDefault="false"
        android:defaultValue="true"
        android:key="ring_key"
        android:showSilent="true"/>

    <PreferenceCategory
        android:title="@string/category_setting_one">

        <CheckBoxPreference
            android:title="@string/g1_checkbox_setting_title"
            android:summaryOn="@string/checkbox_setting_summary_on"
            android:summaryOff="@string/checkbox_setting_summary_off"
            android:defaultValue="false"
            android:key="g1_checkbox_key"/>

        <ListPreference
            android:key="g1_list_key"
            android:title="@string/g1_list_setting_title"
            android:summary="@string/list_setting_summary"
            android:dialogIcon="@android:drawable/stat_sys_warning"
            android:dialogTitle="第一组列表设置"
            android:entries="@array/list_array_title"
            android:entryValues="@array/list_array_value"/>

        <EditTextPreference
            android:key="g1_edit_key"
            android:title="@string/g1_edit_setting_title"
            android:summary="@string/edit_setting_summary"
            android:dialogTitle="请输入:"/>

    </PreferenceCategory>

    <PreferenceCategory
        android:title="@string/category_setting_two">

        <CheckBoxPreference
            android:title="@string/g2_checkbox_setting_title"
            android:summaryOn="@string/checkbox_setting_summary_on"
            android:summaryOff="@string/checkbox_setting_summary_off"
            android:defaultValue="false"
            android:key="g2_checkbox_key"/>

        <ListPreference
            android:key="g2_list_key"
            android:title="@string/g2_list_setting_title"
            android:summary="@string/list_setting_summary"
            android:dialogIcon="@android:drawable/stat_sys_warning"
            android:dialogTitle="第二组列表设置"
            android:entries="@array/list_array_title"
            android:entryValues="@array/list_array_value"/>

        <EditTextPreference
            android:key="g2_edit_key"
            android:title="@string/g2_edit_setting_title"
            android:summary="@string/edit_setting_summary"
            android:dialogTitle="请输入:"/>

    </PreferenceCategory>

    <SwitchPreference
        android:key="show_advanced_setting"
        android:title="@string/show_advanced_setting_title"
        android:summary="@string/show_advanced_setting_summary"
        android:defaultValue="false"/>


</PreferenceScreen>

我定义了一个铃声设置,两个demo分组设置,还有一个高级设置的switch按钮设置。高级设置也是一个preference的xml文件定义如下:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
        android:key="advanced_settings"
        android:title="@string/advanced_setting_title">
        <CheckBoxPreference
            android:key="advanced_checkbox_setting"
            android:title="高级勾选设置"
            android:summary="勾选确认"/>
    </PreferenceCategory>
</PreferenceScreen>

最后需要说明一下,前面我们定义了一个列表设置项,因此我们需要array来提供列表显示项和相应的值,我们可以把它定义在array资源文件中:
arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="list_array_title">
        <item>item 1</item>
        <item>item 2</item>
        <item>item 3</item>
        <item>item 4</item>
        <item>item 5</item>
    </string-array>

    <string-array name="list_array_value">
        <item>value 1</item>
        <item>value 2</item>
        <item>value 3</item>
        <item>value 4</item>
        <item>value 4</item>
    </string-array>


</resources>

定义完毕preference文件之后,我们还需要定义PreferenceFragment的子类来显示这个布局文件。

开发第三步:定义PreferenceFragment的子类

我们只要新建一个类并且继承自PreferenceFragment类就可以,然后我们还需要复写它的onCreate方法,在这个方法中调用addPreferencesFromResource方法显示我们上面的布局文件。同时我们还需要监听高级设置的switch button的按下的值来决定是不是需要显示高级设置界面,因此需要在这个fragment启动的时候判断高级设置项的状态,如果发现高级设置被关闭了,那么就要隐藏高级设置,反之则要显示。
这里有几个问题需要处理:
1. 怎么监听高级设置的按下情况?
答:实现sharedpreference的OnSharedPreferenceChangeListener接口来实现数据监听,因为preference设置时使用sharedpreference存储的。
2. 怎么隐藏或者显示高级设置?
我们在启动fragment的时候调用addPreferencesFromResource显示我们定义的布局文件,这个方法是将参数中指定的resource文件添加到当前显示ui结构中,因此我们可以通过这个方法显示高级设置界面。那么怎么隐藏呢?我们通过addPreferencesFromResource添加了界面之后,fragment中就会有一个根preference,我们通过的这个根的removePreference方法来移除我们的高级设置界面,removePreference方法的参数是一个preference对象,这个可以通过PreferenceFragment的findPreference(String key)方法来获得目前已经添加到该fragment中显示的preference。这样我们就可以控制高级界面的显示和隐藏了。PreferenceFragment子类代码如下:

public class PrefsFragmentOne extends PreferenceFragment
        implements SharedPreferences.OnSharedPreferenceChangeListener

    private PreferenceScreen screen = null;

    public PrefsFragmentOne() 
        // Required empty public constructor
    


    @Override
    public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.preference_settings);

        screen = getPreferenceScreen();
    

    @Override
    public void onResume() 
        super.onResume();
        getPreferenceScreen().getSharedPreferences()
                .registerOnSharedPreferenceChangeListener(this);

        SwitchPreference switchPreference = (SwitchPreference) findPreference("show_advanced_setting");
        if (switchPreference.isChecked()) 
            addPreferencesFromResource(R.xml.preference_advanced_setting);
        
    

    @Override
    public void onPause() 
        super.onPause();
        getPreferenceScreen().getSharedPreferences()
                .unregisterOnSharedPreferenceChangeListener(this);
    

    @Override
    public void onAttach(Context context) 
        super.onAttach(context);

        getActivity().setTitle("设置1");
    

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) 
        Log.d("DEBUG", "key: " + key);
        if ("show_advanced_setting".equals(key)) 
            boolean enabled = sharedPreferences.getBoolean(key, false);

            if (enabled) 
                // show advanced setting here.
                addPreferencesFromResource(R.xml.preference_advanced_setting);
             else 
                // remove advanced setting here.
                PreferenceCategory preferenceCategory = (PreferenceCategory) findPreference("advanced_settings");
                if (preferenceCategory != null) 
                    screen.removePreference(preferenceCategory);
                
            
        
    

这里是PrefsFragmentOne的代码,PrefsFragmentTwo的代码和它一样。同时需要说明一下,当fragment resume的时候我们添加了监听器,在fragment pause的时候我们需要反注册监听器。另外如果你不是像我那样使用fragment来实现OnSharedPreferenceChangeListener,而是使用单独的一个类(内部或者外部类)来实现它的话,你需要在你的代码中明确持有一份这个监听器的强引用,否则这个监听器对象会被GC的,原因是preference manager不会包含这个监听器的任何引用。如下代码是有问题的代码:

prefs.registerOnSharedPreferenceChangeListener(
  // Bad! The listener is subject to garbage collection!
  new SharedPreferences.OnSharedPreferenceChangeListener() 
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) 
    // listener implementation
  
);

应该使用下面的代码:

SharedPreferences.OnSharedPreferenceChangeListener listener =
    new SharedPreferences.OnSharedPreferenceChangeListener() 
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) 
    // listener implementation
  
;
prefs.registerOnSharedPreferenceChangeListener(listener);

运行上面的代码可以看到这样的界面:

当用户点击“设置铃声”时,界面如下:

当用户点击“第一组列表设置”时,界面如下:

当用户点击“第一组输入设置”时,界面如下:

当用户点击“显示高级设置”时,界面如下:

同时我们将这个应用运行于平板上的时候界面如下:

我们看到了这个时候自动做了屏幕大小的适配,分屏显示以充分利用屏幕(这个真的很方便)。
前面我们说道,这里的数据会存储在sharedpreference中,因此我们这里进入平板模拟器(普通设备是需要root权限的)的/data/data/com.createchance.androidsettingsdemo/shared_prefs(这是android app的私有数据目录)这个目录下看下有什么:

我们打开com.createchance.androidsettingsdemo_preferences.xml文件看下:

我们看到我们所有的设置全部保存在这里!!并且下次我们再次进入设置界面的时候,会自动根据之前设置的值,同步界面组件的显示。
大家有没有感觉这个界面的风格和系统的settings应用很像?是的,settings的界面也是使用这个技术开发的,因此你的app的设置风格可以和系统的设置风格一致,这就满足人机交互设计学上所讲的“一致性”原则了!!这也是为什么google鼓励开发使用它开发的原因!!!

以上是关于Android Settings(Preferences)开发的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义Preference点击波纹

Android的settings命令

android.content.ActivityNotFoundException:未找到处理 Intent act=android.settings.USAGE_ACCESS_SETTINGS

android.provider.Settings.ACTION_BLUETOOTH_SETTINGS 在三星上崩溃

start com.android.settings/com.android.settings.SubSettings activity

Android5.1源码分析系列Settings源码分析