LeanBack 之Preference
LeanBack 是 JectPack的一个用于开发Tv App的一个库,对UI的展示比较友好,也比较常用。它里面有一个Prefrerence的库是专门用来开发Setting 界面Ui的一个库。
我们项目里面使用了preference,经常会遇到preference的一些问题。 我们再希望这篇文章达到得目的:可以通过这篇文章正常使用LeanBack 得preference。
为何使用leanback-preference
开发setting界面,方便简介,易于理解,很多个嵌套setting页面(menu)布局只需要一个xml文件就可以完成。
添加引用
buildgradle 里面添加依赖
implementation 'androidx.leanback:leanback-preference:1.0.0'
使用 demo1
2 界面效果
点击inner 之后得界面
1 xml文件
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/pref_title_settings">
<SwitchPreference
android:title="@string/pref_title_recommendations"
android:defaultValue="true"
android:key="@string/pref_key_recommendations" />
<Preference
android:key="setting1"
android:title="pref_setting_1" />
</PreferenceCategory>
<Preference
android:key="@string/pref_key_login"
android:title="@string/pref_title_login" />
<PreferenceScreen android:key="key_inner" android:title="inner">
<Preference
android:key="inner_1"
android:title="pref_title_login" />
<Preference
android:key="inner_2"
android:title="pref_title_login2" />
</PreferenceScreen>
</PreferenceScreen>
xml 文件标签和图片界面对照
preferenceScreen 对应的是一个页面
PreferenceCategory 对应了一个标签 (一组item)
第一个界面的inner 点开调用到第二个界面,它对应的也是一个prefernceScreen.
各个控件使用介绍(常用的用法和属性)
Preference 控件(自动存取)
Preference 为啥叫Preference 跟SharedPreference 有啥关系吗?
答案是有关系的。
Preference 的一些值都存在本地的。跟他的一个属性 mPersistent (后面属性的时候会讲到)有关系 可以在xml 里面设置 android:persistent="true" 在java代码里面也可以设置。
看下怎么存储的。
Preference的一些代码
// 判断是否可以本地化存储一些内容
// hasKey Preference是否设置 key 是以这个key 作为本地化的键来存储的
// isPersistent 就是属性的设置
protected boolean shouldPersist() {
return mPreferenceManager != null && isPersistent() && hasKey();
}
// 存储字符串的 里面用的是 PreferenceDataStore 或者 SharedPreferences 用来存储的
protected boolean persistString(String value) {
if (!shouldPersist()) { // 判断应不应该本地化
return false;
}
// Shouldn't store null
if (TextUtils.equals(value, getPersistedString(null))) {
// It's already there, so the same as persisting
return true;
}
PreferenceDataStore dataStore = getPreferenceDataStore();
if (dataStore != null) {
dataStore.putString(mKey, value);
} else {
SharedPreferences.Editor editor = mPreferenceManager.getEditor();
editor.putString(mKey, value);
tryCommit(editor);
}
return true;
}
EdittextPreference 的存值取值(初始化)的方法
// 调用父类 persistString 保存本地化
public void setText(String text) {
....
persistString(text);
....
}
}
初始化
//restoreValue 是否取本地存的值 跟父类的 shouldPersist 有关
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setText(restoreValue ? getPersistedString(mText) : (String) defaultValue);
}
上面说的是 保存字符串的方法,在EdittextPreference 会调用自动保存edittext的字符串,当在重新进入页面的时候会默认去取原来存的值。
默认是用SharedPreferences 存储的,其实上面先 判断用的有没有PreferenceDataStore 再去使用 SharedPreferences 保存的了,如果设置了PreferenceDataStore 的话先用PreferenceDataStore 存储。PreferenceDataStore 提供了了一些自动存值取值的 abstract class,自己去通过别的方式实现存值取值。
下面在看其他的一些保存值的方式
// Preference class
- persistBoolean TwoStatePreference 比如 switchPreference checkboxPreference 等。
- persistInt SeekBarPreference 使用
- persistFloat 暂时还没发现自带的preference 使用 ,但是得封装好给自定义的需要存这个类型的使用
- persistLong 同上
- persistStringSet MultiSelectListPreference 多选列表类型
- persistString EditTextPreference ListPreference 单选列表
Preference 的属性
一般都可以在xml 和 java代码进行设置,一般是知道的就在xml进行静态设置,不确定的一般在java代码里面进行动态设置。
- key preference 的key就相当于 view的id 一样在java代码里面可以通过key 来找到preference。例如Preference placement = findPreference(KEY_SOUND_PLACEMENT)。
- title preference的 title普通显示的大的文字,比较常用。
- icon preference的上显示的icon 一般在左侧。
- layout 是用来替换单独的布局。下面单独有布局的讲解。
上面四个属性是配置的跟页面ui展示的内容有关系。 - enable preference的enable 类似于view的enable,这个时候不可获取焦点,颜色比较暗。例如原生setting 里面屏保 startnow 这个当屏保关闭的时候就是enable false 不可获取焦点,颜色也变暗了。
- visible preference 的visible的属性是显示不显示的属性,比如一个页面的设置项在不同的信号源里面有的项显示有的不显示就使用的设个。
还有在intent 跳转activity 和data 的内容设置
<Preference
android:key="key_epg_program_search_travel"
android:title="TRAVEL">
<intent android:targetClass="com.android.tv.epg.activity.EpgSearchProgramActivity"
android:targetPackage="com.android.tv"
android:data="key_epg_program_search_travel"/>
</Preference>
常用的监听方法
setOnPreferenceClickListener 跟View的onclick listenner类似 用preference 的key来进行判断
setOnPreferenceChangeListener twoState 的常用这个这个方法稍微有一个小特点,比较有用
public boolean onPreferenceChange(Preference preference, Object newValue) {
return false;
}
这个方法的这个返回值 false 是ui 和值都不变化 newValue 是将要变化的值。 比如原来是switch 关的状态,点击一下 newValue 是true返回false的话,switch的ui 不变,返回true的话switch的UI变化。
常用的preference 子类
官方已经封装好了 常用的Preference 的子类 常用
- CheckBoxPreference 单选框
- SwitchPreference switch的preference
CheckBoxPreference 和 SwitchPreference 都是TwoStatePreference check的方法 SwitchPreference 多了 on 和off的时候text的设置 - ListPreference 列表使用的也可以是单选框使用
下面是使用例子
<ListPreference
android:defaultValue="0"
android:entries="@array/pref_prefecture_area"
android:entryValues="@array/pref_prefecture_area_code"
android:key="key_prefs_area"
android:title="@string/menu_prefecture">
defaultValue 是默认选中的item。
enteries 是默认的显示的列表
entryValues 是和显示的列表一一对应的类似于key 和 defaultValue是对应的。
上面几行设置之后功能 就把列表展示出来了,还会记录位置,不需要我们单独记录。
- MultiSelectListPreference 多选框的列表
类似于ListPreference ,ListPreference是单选框 MultiSelectListPreference是多选框
MultiSelectListPreference 会把选中的 value 列表存下来下次就可以使用了。
<MultiSelectListPreference
android:defaultValue="@array/pref_tuners"
android:entries="@array/pref_tuners"
android:entryValues="@array/pref_tuners"
android:key="key_prefs_tuner_mode"
android:title="MultiSelectListPreference" >
下面是图片
- 注意上面 ListPreference 和 MultiSelectListPreference 里面 entryValues 不能按照语言等 翻译的 entries 需要按照语言翻译的。
Preference 的fragment 界面处理
有时候我们都在xm了里面嵌套了很多的页面,Fragment 也就一个类也就展示所有的界面这样的话如果在java里面写的逻辑比较多的话代码就非常复杂全都写在一个类里面。
就像
public class SettingsFragment extends LeanbackSettingsFragment
implements DialogPreference.TargetFragment {
private final static String PREFERENCE_RESOURCE_ID = "preferenceResource";
private final static String PREFERENCE_ROOT = "root";
private PreferenceFragment mPreferenceFragment;
@Override
public void onPreferenceStartInitialScreen() {
mPreferenceFragment = buildPreferenceFragment(R.xml.settings, null);
startPreferenceFragment(mPreferenceFragment);
}
@Override
public boolean onPreferenceStartScreen(PreferenceFragment preferenceFragment,
PreferenceScreen preferenceScreen) {
PreferenceFragment frag = buildPreferenceFragment(R.xml.settings,
preferenceScreen.getKey());
startPreferenceFragment(frag);
return true;
}
···
private PreferenceFragment buildPreferenceFragment(int preferenceResId, String root) {
PreferenceFragment fragment = new PrefFragment();
Bundle args = new Bundle();
args.putInt(PREFERENCE_RESOURCE_ID, preferenceResId);
args.putString(PREFERENCE_ROOT, root);
fragment.setArguments(args);
return fragment;
}
public static class PrefFragment extends LeanbackPreferenceFragment {
@Override
public void onCreatePreferences(Bundle bundle, String s) {
String root = getArguments().getString(PREFERENCE_ROOT, null);
int prefResId = getArguments().getInt(PREFERENCE_RESOURCE_ID);
if (root == null) {
addPreferencesFromResource(prefResId);
} else {
setPreferencesFromResource(prefResId, root);
}
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(getString(R.string.pref_key_login))) {
// Open an AuthenticationActivity
// startActivity(new Intent(getActivity(), AuthenticationActivity.class));
}
return super.onPreferenceTreeClick(preference);
}
}
}
上面是官方的demo的界面 所有的一般的业务逻辑都是在PrefFragment 里面处理的
buildPreferenceFragment 这个方法 第一和xml 文件名字 第二个是PreferencePage 对应的key 根页面的话就是 null。
每一个 PrefFragment 对应的是一个界面也就是对应一个PreferencePage。
如果都在PrefFragment 处理的话就很复杂。
我们这个地方可以多创建 LeanbackPreferenceFragment 来单独处理一些逻辑。
private PreferenceFragment buildPreferenceFragment(int preferenceResId, String screenKey) {
PreferenceFragment fragment = null;
if (TextUtils.isEmpty(screenKey)) {
fragment = new PrefFragment();
} else {
switch (screenKey) {
case KEY_RECEPTION_SETTINGS:
fragment = new ReceptionFragment();
break;
case KEY_PREFECTURE_SETTINGS:
fragment = new PrefectureFragment();
break;
case KEY_CHANNEL_SKIP_SETTINGS:
fragment = new ChannelSkipFragment();
break;
case Constants.KEY_PREFS_REC_SETTINGS:
fragment = new RecordingSettingsFragment();
break;
case KEY_SYSTEM_SETTINGS:
fragment = new MenuSettingsSystemFragment();
break;
case KEY_SYSTEM_PARENTAL_CONTROL:
fragment = new MenuParentalControlFragment();
break;
case KEY_SYSTEM_PARENTAL_CONTROL_JAPANESE_BROADCAST:
fragment = new JapaneseBroadcastFragment();
break;
case Constants.KEY_ADVANCED_SETTINGS_OADL_SETTINGS:
fragment = new OADLSettingsFragment();
break;
case Constants.KEY_PREFS_B_CAS_CARD_INFO:
fragment = new BCasFragment();
break;
default:
fragment = new PrefFragment();
break;
}
}
Bundle args = new Bundle();
args.putInt(Constants.KEY_PREFS_RES_DI, preferenceResId);
args.putString(Constants.KEY_PREFS_SCREEN, screenKey);
fragment.setArguments(args);
return fragment;
}
Preference 的fragment ui自定义
Preference 页面替换 比如文字大小位置等
1 Preferene 里面有一个属性 layout 我们可以创建一个 layout 布局。但是限制条件是里面的view的布局必须和原来leanback_preference.xml 里面设置的id 一样。
icon 的id android:id="@android:id/icon"
title的id android:id="@android:id/title"
summary的id android:id="@android:id/summary"
2 假如我们的布局不单单这些控件 我们比如 左右键箭头
需要我们自定义Preferenc 了。
例如
public class PictureModePreference extends Preference {
···
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mItemView = holder.itemView;
mLeftImg = mItemView.findViewById(R.id.img_arrow_left);
mRightImg = mItemView.findViewById(R.id.img_arrow_right);
mModeText = mItemView.findViewById(android.R.id.title);
}
···
}
3 为什么我们的setting 在右边呢
LeanbackSettingFragment对应的布局
leanback_settings_fragment.xml
<androidx.leanback.preference.LeanbackSettingsRootView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settings_dialog_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.leanback.preference.internal.OutlineOnlyWithChildrenFrameLayout
android:id="@+id/settings_preference_fragment_container"
android:layout_width="@dimen/lb_settings_pane_width"
android:layout_height="match_parent"
android:layout_gravity="end"
android:elevation="@dimen/lb_preference_decor_elevation"
android:outlineProvider="bounds" />
</androidx.leanback.preference.LeanbackSettingsRootView>
这个里面有这个布局配置的
修改显示的布局的话可以设置 LeanbackSettingFragment 的对应的布局
例如 我把这个改到左边
我重写了 onCreateView 方法 指向我自己的布局 但是里面的id 不要变。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.leanback_settings_fragment_start, container, false);
return v;
}
····
// 布局
<androidx.leanback.preference.LeanbackSettingsRootView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settings_dialog_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings_preference_fragment_container"
android:layout_width="@dimen/lb_settings_pane_width"
android:layout_height="match_parent"
android:layout_gravity="start"
android:elevation="@dimen/lb_preference_decor_elevation"
android:outlineProvider="bounds" />
</androidx.leanback.preference.LeanbackSettingsRootView>
4 带有头部的布局怎么改变
-
全局修改
写一个和leanback_preference_fragment.xml 一样的文件修改里面的布局,布局就跟着变化了,他会优先使用我们定义的这个布局。
右边的箭头吧高度改了一下,上面的布局高度已经跟着改变了。
- 假如是多个页面不一致的话
public abstract class LeanbackPreferenceFragment extends BaseLeanbackPreferenceFragment {
···
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.leanback_preference_fragment, container, false);
final ViewGroup innerContainer = (ViewGroup) view.findViewById(R.id.main_frame);
final View innerView = super.onCreateView(inflater, innerContainer, savedInstanceState);
if (innerView != null) {
innerContainer.addView(innerView);
}
return view;
}
···
}
我们可以重写onCreateView 的方法去修改这个布局。