设置页面在每个App中都是必不可少的,用户可以在设置页面中更改App的各种配置,例如开关通知、是否播放背景音等,用户修改的配置信息通常会使用SharedPreferences
或DataStore
本地保存,以确保用户关闭App后重新进入配置仍然生效。
最近经常会看官方文档,发现官方针对设置页面专门出了一个库Preference Library,该库实现了标准UI以及数据存储功能,可以快速实现设置页面,并且无需额外处理数据存取,下面介绍一下如何使用。
添加依赖库
在项目app module的build.gradle中的dependencies中添加依赖:
dependencies {
implementation("androidx.preference:preference:1.2.0")
implementation("androidx.preference:preference-ktx:1.2.0")
}
实现设置页
通过XML配置创建设置页
- 在res/xml目录下创建资源文件,根标签为
PreferenceScreen
,如下:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="app_setting"
android:title="App Setting">
<SwitchPreferenceCompat
android:icon="@drawable/notification"
android:key="notifications_open_status"
android:title="Enable Notifications"
app:summary="Turn on notifications to receive application messages" />
</PreferenceCategory>
<PreferenceCategory
android:key="user_categore"
android:title="Account">
<SwitchPreferenceCompat
android:icon="@drawable/login"
android:key="auto_login_enable"
android:summary="No need to actively log in"
android:title="Auto Login" />
</PreferenceCategory>
</PreferenceScreen>
- 自定义SettingFragment继承
PreferenceFragmentCompat
,重写onCreatePreferences
方法, 如下:
class SettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// 使用xml填充页面
setPreferencesFromResource(R.xml.example_setting, rootKey)
}
}
- 将
Fragment
添加到Activity
中,如下:
class SettingActivity : AppCompatActivity() {
private lateinit var binding: LayoutSettingActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_setting_activity)
supportFragmentManager
.beginTransaction()
.replace(R.id.ct_setting_container, SettingFragment())
.commitAllowingStateLoss()
}
}
效果如图:
在代码中创建设置页
可以不生成XML文件,直接在代码中生成设置页。
- 自定义SettingFragment继承
PreferenceFragmentCompat
,重写onCreatePreferences
方法,并在此方法中通过代码填充页面,如下:
class SettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
val onlineContactPreference = Preference(context)
onlineContactPreference.key = "online_customer"
onlineContactPreference.title = "Online customer"
onlineContactPreference.summary = "Contact online customer service to solve your problem"
//设置Preference的点击监听
onlineContactPreference.setOnPreferenceClickListener {
requireActivity().runOnUiThread { Toast.makeText(requireContext(), "Click online customer", Toast.LENGTH_SHORT).show() }
true
}
screen.addPreference(onlineContactPreference)
val qaPreferenceCategory = PreferenceCategory(context)
qaPreferenceCategory.key = "qa"
qaPreferenceCategory.title = "QA"
val qaPreference = Preference(context)
qaPreference.key = "qa1"
qaPreference.title = "How to open Notification"
qaPreference.summary = "Open system setting"
//这里要特别注意,如果要对设置进行分组,需要先将PreferenceCategory添加到PreferenceScreen中
//然后才可以在PreferenceCategory中添加Preference,否做会报错
screen.addPreference(qaPreferenceCategory)
qaPreferenceCategory.addPreference(qaPreference)
// 将创建的PreferenceScreen设置到页面中
preferenceScreen = screen
}
}
- 将
Fragment
添加到Activity
中,如下:
class SettingActivity : AppCompatActivity() {
private lateinit var binding: LayoutSettingActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_setting_activity)
supportFragmentManager
.beginTransaction()
.replace(R.id.ct_setting_container, SettingFragment())
.commitAllowingStateLoss()
}
}
效果如图:
拆分为多个设置页面
如果设置项很多,需要拆分为不同子页面,整体实现方式与前两部分差不多,区别如下:
- xml配置调整如下:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--设置此Preference跳转的Fragment-->
<Preference
android:fragment="xxx.xxx.SubpageFragment"
... />
</PreferenceScreen>
- 代码中创建调整如下:
class SettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
val subpagePreference = Preference(context)
subpagePreference.fragment = SubpageFragment::class.java.canonicalName
screen.addPreference(onlineContactPreference)
preferenceScreen = screen
}
}
- Activity 实现
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
接口,如下:
class SettingActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
...
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
pref.fragment?.let {
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, it)
supportFragmentManager.beginTransaction()
.replace(R.id.ct_setting_container, fragment)
.addToBackStack(null)
.commitAllowingStateLoss()
}
return true
}
}
此部分效果可以在最后的示例中看到。
更改样式
系统默认实现的样式可能不满足我们的需求,可以通过重写style文件来修改系统样式。
这边用PreferenceCategory
标题的文字样式来做示范,代码如下:
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="PreferenceCategoryTitleTextStyle" tools:override="true">
<item name="android:textAppearance">?attr/preferenceCategoryTitleTextAppearance</item>
<item name="android:textSize">16sp</item>
<item name="android:fontFamily">@font/noto_serif_bold</item>
<item name="android:textColor">@color/color_blue_0083ff</item>
</style>
</resources>
这边与通过XML配置创建设置页中的效果图作对比,可以看见PreferenceCategory
标题文字的颜色、大小、字体都改变了:
如果对控件的UI不满意,例如需要调整间距、大小等,可以通过自定义同名的layout文件来修改库中控件的UI。
注意,要确保自定义layout的命名,以及layout中的控件id均与库中的相同。
更改PreferenceDataStore
Preference Library默认通过SharedPreferences
来实现数据存储,可以替换为自己想要的方式。需要注意的是,如果更改了PreferenceDataStore
,那么打开设置页面时,所有类型的Preference
组件都不会被自动赋值。
自定义DataStore继承PreferenceDataStore
,重写存取方法,如下:
object ExampleDataStore : PreferenceDataStore() {
override fun putInt(key: String?, value: Int) {
...
}
override fun getInt(key: String?, defValue: Int): Int {
...
}
override fun putLong(key: String?, value: Long) {
...
}
override fun getLong(key: String?, defValue: Long): Long {
...
}
override fun putFloat(key: String?, value: Float) {
...
}
override fun getFloat(key: String?, defValue: Float): Float {
...
}
override fun putBoolean(key: String?, value: Boolean) {
...
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
...
}
override fun putString(key: String?, value: String?) {
...
}
override fun getString(key: String?, defValue: String?): String? {
...
}
override fun putStringSet(key: String?, values: MutableSet<String>?) {
...
}
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
...
}
}
然后通过PreferenceManager
的setPreferenceDataStore
方法,设置自定义的DataStore,如下:
class SettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.example_setting, rootKey)
preferenceManager.preferenceDataStore = ExampleDataStore
}
}
示例
整合之后做了个示例Demo,并演示了一些Preference
组件的用法,示例代码如下:
//主Settin页面XML
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="app_setting"
android:title="App Setting">
<SwitchPreferenceCompat
android:icon="@drawable/notification"
android:key="notifications_open_status"
android:title="Enable Notifications"
app:summary="Turn on notifications to receive application messages" />
<Preference
android:fragment="com.chenyihong.exampledemo.setting.HelperFragment"
android:key="help"
android:summary="View FAQ or contact customer service"
app:title="Help Center" />
<Preference
android:key="system_system"
android:summary="Modify system settings"
app:title="System Setting" />
</PreferenceCategory>
<PreferenceCategory
android:key="user_categore"
android:title="Account">
<Preference
android:fragment="com.chenyihong.exampledemo.setting.UserInfoFragment"
android:key="user_info"
android:summary="Modify and view user information"
app:title="User Info" />
<SwitchPreferenceCompat
android:icon="@drawable/login"
android:key="auto_login_enable"
android:summary="No need to actively log in"
android:title="Auto Login" />
<SwitchPreferenceCompat
android:icon="@drawable/google"
android:key="google_account_bind_status"
android:summaryOff="Bind Google account for login"
android:title="Bind Google Account" />
<SwitchPreferenceCompat
android:icon="@drawable/facebook"
android:key="facebook_account_bind_status"
android:summaryOff="Bind Facebook account for login"
android:title="Bind Facebook Account" />
<!--配置Intent用于跳转页面-->
<Preference
android:key="goto_user_profile"
android:summary="View User Agreement page"
app:title="Agreement">
<intent
android:targetClass="com.chenyihong.exampledemo.web.WebViewActivity"
android:targetPackage="com.chenyihong.exampledemo" />
</Preference>
</PreferenceCategory>
</PreferenceScreen>
//用户信息页XML
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="user_info"
android:title="User Info">
<EditTextPreference
android:key="user_info_nick_name"
app:title="Nickname" />
<EditTextPreference
android:key="user_info_real_name"
app:title="Real name" />
<EditTextPreference
android:key="user_info_age"
app:title="Age" />
<ListPreference
android:entries="@array/gender"
android:entryValues="@array/gender"
android:key="user_info_gender"
app:title="Gender"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>
// 自定义DataStore
object ExampleDataStore : PreferenceDataStore() {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "ExamplePreferencesDataStore")
var lifecycleScope: LifecycleCoroutineScope? = null
override fun putInt(key: String?, value: Int) {
lifecycleScope?.launch {
putIntImpl(key, value)
}
}
override fun getInt(key: String?, defValue: Int): Int {
var getValue: Int
runBlocking {
getValue = getIntImpl(key, defValue)
}
return getValue
}
override fun putLong(key: String?, value: Long) {
lifecycleScope?.launch {
putLongImpl(key, value)
}
}
override fun getLong(key: String?, defValue: Long): Long {
var getValue: Long
runBlocking {
getValue = getLongImpl(key, defValue)
}
return getValue
}
override fun putFloat(key: String?, value: Float) {
lifecycleScope?.launch {
putFloatImpl(key, value)
}
}
override fun getFloat(key: String?, defValue: Float): Float {
var getValue: Float
runBlocking {
getValue = getFloatImpl(key, defValue)
}
return getValue
}
override fun putBoolean(key: String?, value: Boolean) {
lifecycleScope?.launch {
putBooleanImpl(key, value)
}
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
var getValue: Boolean
runBlocking {
getValue = getBooleanImpl(key, defValue)
}
return getValue
}
override fun putString(key: String?, value: String?) {
lifecycleScope?.launch {
putStringImpl(key, value)
}
}
override fun getString(key: String?, defValue: String?): String? {
var getValue: String?
runBlocking {
getValue = getStringImpl(key, defValue)
}
return getValue
}
override fun putStringSet(key: String?, values: MutableSet<String>?) {
lifecycleScope?.launch {
putStringSetImpl(key, values)
}
}
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
val getValue = mutableSetOf<String>()
runBlocking {
getValue.addAll(getStringSetImpl(key, defValues))
}
return getValue
}
private suspend fun putIntImpl(key: String?, value: Int?) {
if (key?.isNotEmpty() == true && value != null) {
val preferencesKey = intPreferencesKey(key)
ExampleApplication.exampleContext?.run {
dataStore.edit {
it[preferencesKey] = value
}
}
}
}
private suspend fun getIntImpl(key: String?, defaultValue: Int?): Int {
return if (key?.isNotEmpty() == true) {
val preferencesKey = intPreferencesKey(key)
ExampleApplication.exampleContext?.dataStore?.data?.map {
it[preferencesKey] ?: (defaultValue ?: 0)
}?.first() ?: 0
} else {
0
}
}
private suspend fun putLongImpl(key: String?, value: Long?) {
if (key?.isNotEmpty() == true && value != null) {
val preferencesKey = longPreferencesKey(key)
ExampleApplication.exampleContext?.run {
dataStore.edit {
it[preferencesKey] = value
}
}
}
}
private suspend fun getLongImpl(key: String?, defaultValue: Long?): Long {
return if (key?.isNotEmpty() == true) {
val preferencesKey = longPreferencesKey(key)
ExampleApplication.exampleContext?.dataStore?.data?.map {
it[preferencesKey] ?: (defaultValue ?: 0L)
}?.first() ?: 0L
} else {
0L
}
}
private suspend fun putFloatImpl(key: String?, value: Float?) {
if (key?.isNotEmpty() == true && value != null) {
val preferencesKey = floatPreferencesKey(key)
ExampleApplication.exampleContext?.run {
dataStore.edit {
it[preferencesKey] = value
}
}
}
}
private suspend fun getFloatImpl(key: String?, defaultValue: Float?): Float {
return if (key?.isNotEmpty() == true) {
val preferencesKey = floatPreferencesKey(key)
ExampleApplication.exampleContext?.dataStore?.data?.map {
it[preferencesKey] ?: (defaultValue ?: 0f)
}?.first() ?: 0f
} else {
0f
}
}
private suspend fun putBooleanImpl(key: String?, value: Boolean?) {
if (key?.isNotEmpty() == true && value != null) {
val preferencesKey = booleanPreferencesKey(key)
ExampleApplication.exampleContext?.run {
dataStore.edit {
it[preferencesKey] = value
}
}
}
}
private suspend fun getBooleanImpl(key: String?, defaultValue: Boolean?): Boolean {
return if (key?.isNotEmpty() == true) {
val preferencesKey = booleanPreferencesKey(key)
ExampleApplication.exampleContext?.dataStore?.data?.map {
it[preferencesKey] ?: (defaultValue ?: false)
}?.first() ?: false
} else {
false
}
}
private suspend fun putStringImpl(key: String?, value: String?) {
if (key?.isNotEmpty() == true && value?.isNotEmpty() == true) {
val preferencesKey = stringPreferencesKey(key)
ExampleApplication.exampleContext?.run {
dataStore.edit {
it[preferencesKey] = value
}
}
}
}
private suspend fun getStringImpl(key: String?, defaultValue: String?): String? {
return if (key?.isNotEmpty() == true) {
val preferencesKey = stringPreferencesKey(key)
ExampleApplication.exampleContext?.dataStore?.data?.map {
it[preferencesKey] ?: (defaultValue ?: "")
}?.first()
} else {
""
}
}
private suspend fun putStringSetImpl(key: String?, value: Set<String>?) {
if (key?.isNotEmpty() == true && value?.isNotEmpty() == true) {
val preferencesKey = stringSetPreferencesKey(key)
ExampleApplication.exampleContext?.run {
dataStore.edit {
it[preferencesKey] = value
}
}
}
}
private suspend fun getStringSetImpl(key: String?, defaultValue: Set<String>?): Set<String> {
return if (key?.isNotEmpty() == true) {
val preferencesKey = stringSetPreferencesKey(key)
ExampleApplication.exampleContext?.dataStore?.data?.map {
it[preferencesKey] ?: (defaultValue ?: setOf())
}?.first() ?: setOf()
} else {
setOf()
}
}
}
//SettingActivity
class SettingActivity : BaseGestureDetectorActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private lateinit var binding: LayoutSettingActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_setting_activity)
ExampleDataStore.lifecycleScope = lifecycleScope
supportFragmentManager
.beginTransaction()
.replace(R.id.ct_setting_container, SettingFragment())
.commitAllowingStateLoss()
}
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
pref.fragment?.let {
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, it)
supportFragmentManager.beginTransaction()
.replace(R.id.ct_setting_container, fragment)
.addToBackStack(null)
.commitAllowingStateLoss()
}
return true
}
}
//SettingFragment(XML配置)
class SettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.example_setting, rootKey)
preferenceManager.preferenceDataStore = ExampleDataStore
val notificationOpenStatus = findPreference<SwitchPreferenceCompat>("notifications_open_status")
val autoLoginStatus = findPreference<SwitchPreferenceCompat>("auto_login_enable")
val googleAccountStatus = findPreference<SwitchPreferenceCompat>("google_account_bind_status")
val facebookAccountStatus = findPreference<SwitchPreferenceCompat>("facebook_account_bind_status")
findPreference<Preference>("system_system")?.setOnPreferenceClickListener {
try {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri: Uri = Uri.fromParts("package", requireActivity().packageName, null)
intent.data = uri
startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
}
true
}
val notificationOpenStatusValue = ExampleDataStore.getBoolean("notifications_open_status", false)
val autoLoginStatusValue = ExampleDataStore.getBoolean("auto_login_enable", false)
val googleAccountStatusValue = ExampleDataStore.getBoolean("google_account_bind_status", false)
val facebookAccountStatusValue = ExampleDataStore.getBoolean("facebook_account_bind_status", false)
// 修改了DataStore,初始值需要自己处理
notificationOpenStatus?.isChecked = notificationOpenStatusValue
autoLoginStatus?.isChecked = autoLoginStatusValue
googleAccountStatus?.run {
isChecked = googleAccountStatusValue
// 设置状态变化监听,返回true,则控件的状态会立刻改变,返回false,则维持原样。
// 可以在此处理异步事件,先返回false,根据事件处理结果再修改控件的状态。
onPreferenceChangeListener = OnPreferenceChangeListener { preference, newValue ->
googleAccountStatus.summaryOn = "xxxxx@gmail.com"
true
}
}
facebookAccountStatus?.run {
isChecked = facebookAccountStatusValue
onPreferenceChangeListener = OnPreferenceChangeListener { preference, newValue ->
facebookAccountStatus.summaryOn = "xxxxxxfbaccount"
true
}
}
}
}
//HelperFragmetn(代码创建)
class HelperFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
val qaPreferenceCategory = PreferenceCategory(context)
qaPreferenceCategory.key = "qa"
qaPreferenceCategory.title = "QA"
val qa1Preference = Preference(context)
qa1Preference.key = "qa1"
qa1Preference.title = "How to open Notification"
qa1Preference.summary = "Open system setting"
val qa2Preference = Preference(context)
qa2Preference.key = "qa2"
qa2Preference.title = "How to bind google account"
qa2Preference.summary = "oOpen settings, bind Google login"
screen.addPreference(qaPreferenceCategory)
qaPreferenceCategory.addPreference(qa1Preference)
qaPreferenceCategory.addPreference(qa2Preference)
val contactPreferenceCategory = PreferenceCategory(context)
contactPreferenceCategory.key = "contact"
contactPreferenceCategory.title = "Contact"
val onlineContactPreference = Preference(context)
onlineContactPreference.key = "online_customer"
onlineContactPreference.title = "Online customer"
onlineContactPreference.summary = "Contact online customer service to solve your problem"
onlineContactPreference.setOnPreferenceClickListener {
requireActivity().runOnUiThread { Toast.makeText(requireContext(), "Click online customer", Toast.LENGTH_SHORT).show() }
true
}
val telephoneContactPreference = Preference(context)
telephoneContactPreference.key = "telephone_customer"
telephoneContactPreference.title = "Telephone customer"
telephoneContactPreference.summary = "Contact telephone_contact customer service to solve your problem"
telephoneContactPreference.setOnPreferenceClickListener {
requireActivity().runOnUiThread { Toast.makeText(requireContext(), "Click Telephone customer", Toast.LENGTH_SHORT).show() }
true
}
screen.addPreference(contactPreferenceCategory)
contactPreferenceCategory.addPreference(onlineContactPreference)
contactPreferenceCategory.addPreference(telephoneContactPreference)
preferenceScreen = screen
}
}
//UserInfoFragment(XML创建)
class UserInfoFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.example_user_info, rootKey)
// 自定义摘要提供者,可以更改默认值的文案。
findPreference<EditTextPreference>("user_info_nick_name")?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { preference ->
preference.text ?: "Please enter a nickname"
}
findPreference<EditTextPreference>("user_info_real_name")?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { preference ->
preference.text ?: "Please enter real name"
}
findPreference<EditTextPreference>("user_info_age")?.run {
summaryProvider = Preference.SummaryProvider<EditTextPreference> { preference ->
preference.text ?: "Please enter your age"
}
// 调整EditTextPreference的输入类型
setOnBindEditTextListener { it.inputType = InputType.TYPE_CLASS_NUMBER }
}
}
}
效果如图: