Android使用RecyclerView实现设置界面

Android使用RecyclerView实现设置界面

一、目标

二、下载地址

神马笔记最新版本下载:【神马笔记 版本1.5.0——笔名功能.apk

三、功能设计

随着软件功能的增加,不可避免地会加入越来越多的设置项。如何管理不断增加的设置项?

  • 便于CRUD
  • 界面风格保持统一

首先,必修便于创建,修改,删除,并且只需要改动配置项,而不用对界面进行太大的修改。

其次,子设置界面必须与主设置界面保持风格一致。

四、准备工作

实现设置界面的方式有很多,可以选择任意的容器和控件进行组合,从而实现设置界面。

但是设置界面有其特殊性。

设置界面通常是有多个设置项组成,每一个设置项又由键值对构成。多个设置项可以组合成一个设置组,设置组之间由分割线区分开。其组成方式大概如下结构。

  • 设置界面
    • 设置组
    • 设置项
    • 设置项
    • 设置项
    • ……
    • 设置组
      • 设置项
    • 设置组
    • ……

从以上结构来看,设置界面便是一组设置项的列表。列表界面自然是使用RecyclerView来实现。

五、组合起来

1. BaseSettingItem

定义设置项UI的组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BaseSettingItem<T> {

public static final int DIVIDER_NONE = 0;
public static final int DIVIDER_NAME = 1;

String id; // 唯一ID

@DrawableRes int iconResId; // 图标资源
ColorStateList iconTintList; // 图标Tint

CharSequence name; // 名称
CharSequence text; // 文本

boolean chevron; // 是否显示chevron

Consumer<T> consumer; // 列表项处理点击事件

T userObject; // 用户数据

boolean dividerVisible; // 是否显示分割线
int dividerType; // 分割线的显示方式
}

2. 布局文件

定义基础设置项的布局。基础布局包括图标、名称、箭头3个部分组成。

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
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/settingItemHeight"
android:background="@color/colorSettingItemBackground"
android:paddingLeft="?listPreferredItemPaddingLeft"
android:paddingRight="?listPreferredItemPaddingRight"
android:gravity="center_vertical"
android:foreground="?selectableItemBackground">

<ImageView
android:id="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="?listPreferredItemPaddingRight"/>

<TextView
android:id="@+id/tv_name"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Menu"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>

<ImageView
android:id="@+id/iv_chevron"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_chevron_right_white_24dp"
android:tint="@color/colorChevron"/>

</LinearLayout>

3. BaseSettingViewHolder

RecyclerView的ViewHolder实现。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class BaseSettingViewHolder<T extends BaseSettingItem> extends BridgeViewHolder<T> implements SectionDividerDecoration.Adapter {

public static final int LAYOUT_RES_ID = R.layout.layout_setting_list_item;

ImageView iconView;
TextView nameView;
TextView textView;
ImageView chevronView;

@Keep
public BaseSettingViewHolder(View itemView) {
super(itemView);
}

@Override
public int getLayoutResourceId() {
return LAYOUT_RES_ID;
}

@Override
public void onViewCreated(@NonNull View view) {
view.setOnClickListener(this::onItemClick);

this.iconView = view.findViewById(R.id.iv_icon);
this.nameView = view.findViewById(R.id.tv_name);
this.textView = view.findViewById(R.id.tv_text);
this.chevronView = view.findViewById(R.id.iv_chevron);

if (iconView != null) {
if (iconView.getOutlineProvider() != null) {
iconView.setClipToOutline(true);
}
}
}

@Override
public void onBind(T item, int position) {

if (iconView != null) {
if (item.getIcon() > 0) {
iconView.setImageResource(item.getIcon());
iconView.setVisibility(View.VISIBLE);
} else {
iconView.setImageDrawable(null);
iconView.setVisibility(View.GONE);
}

iconView.setImageTintList(item.getIconTintList());
}

if (nameView != null) {
nameView.setText(item.getName());
}

if (textView != null) {
textView.setText(item.getText());
}

if (chevronView != null) {
chevronView.setVisibility(item.isChevron()? View.VISIBLE: View.GONE);
}

}

protected void onItemClick(View view) {
T item = getItem();
if (item.getConsumer() != null) {
item.getConsumer().accept(item.getUserObject());
}
}

@Override
public int getMargin(SectionDividerDecoration decoration) {
int margin = 0;

int type = getItem().getDividerType();
if (type == BaseSettingItem.DIVIDER_NAME) {
if (nameView != null) {
margin = nameView.getLeft();
}
}

return margin;
}

@Override
public boolean isVisible(SectionDividerDecoration decoration) {
return getItem().isDividerVisible();
}
}

4. BasePreferenceFragment

定义设置界面的抽象Fragment,所有设置界面均继承该类,子类必须实现以下2个抽象接口。

  • List<BaseSettingItem> createList()

创建设置项列表。

  • void buildAdapter(BridgeAdapter adapter)

构建适配器。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
public abstract class BasePreferenceFragment extends Fragment {

SearchTitleBar titleBar;

RecyclerView recyclerView;
BridgeAdapter adapter;
SettingProvider provider;

SectionDividerDecoration dividerDecoration;

public BasePreferenceFragment() {

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_preference, container, false);
}

@CallSuper
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

{
this.titleBar = view.findViewById(R.id.title_bar);

Toolbar toolbar = titleBar.getToolbar();
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationOnClickListener(this::onNavigationClick);
}

{
this.recyclerView = view.findViewById(R.id.recycler_list_view);

LinearLayoutManager layout = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layout);
}

{
this.dividerDecoration = new SectionDividerDecoration(getActivity());
recyclerView.addItemDecoration(dividerDecoration);
}
}

@CallSuper
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

{
this.provider = new SettingProvider(this.createList());
}

{
this.adapter = new BridgeAdapter(getActivity(), provider);
this.buildAdapter(adapter);
}

{
recyclerView.setAdapter(adapter);
}
}

void onNavigationClick(View view) {
getActivity().onBackPressed();
}

void setTitle(CharSequence text) {
titleBar.setTitle(text);
}

<T> T getItem(String id) {
return provider.getItem(id);
}

int indexOf(Object object) {
return provider.indexOf(object);
}

void notifyItemChanged(Object object) {
int index = indexOf(object);
if (index >= 0) {
adapter.notifyItemChanged(index);
}
}

abstract List<BaseSettingItem> createList();

abstract void buildAdapter(BridgeAdapter adapter);

/**
*
*/
class SettingProvider implements BridgeAdapterProvider<BaseSettingItem> {

ArrayList<BaseSettingItem> list;

public SettingProvider(List<BaseSettingItem> list) {
this.list = new ArrayList<>(list);
}

<T> T getItem(String id) {
BaseSettingItem item = list.stream()
.filter(e -> e.getId().equals(id))
.findAny()
.orElse(null);

return (T)item;
}

int indexOf(Object object) {
int size = list.size();
for (int i = 0; i < size; i++) {
if (list.get(i) == object) {
return i;
}
}

return -1;
}

@Override
public BaseSettingItem get(int position) {
return list.get(position);
}

@Override
public int size() {
return list.size();
}
}
}

5. SettingFragment

实现主设置界面。

  • List<BaseSettingItem> createList()

添加了3个设置项——ProfileSettingItem、ExplainSettingItem、BaseSettingItem。

  • void buildAdapter(BridgeAdapter adapter)

绑定了3种ViewHolder——ProfileSettingViewHolder、ExplainSettingItem、BaseSettingViewHolder。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class SettingFragment extends BasePreferenceFragment {

static final String ID_PROFILE = "profile";

RequestResultManager requestResultManager;

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

this.requestResultManager = new RequestResultManager();
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
requestResultManager.onActivityResult(requestCode, resultCode, data);
}

@Override
List<BaseSettingItem> createList() {
List<BaseSettingItem> list = new ArrayList<>();

{
ProfileSettingItem item = new ProfileSettingItem(PreferenceEntity.obtain().getProfile());
item.setId(ID_PROFILE);

item.setConsumer((obj) -> {
RequestProfileDelegate delegate = new RequestProfileDelegate(this, (data) -> onProfileChanged());

requestResultManager.request(delegate);
});

list.add(item);
}

{
ExplainSettingItem item = new ExplainSettingItem("");

list.add(item);
}

{
BaseSettingItem item = new BaseSettingItem("关于神马笔记");
item.setIcon(R.drawable.ic_info_outline_white_24dp);
item.setIconTintList(getActivity(), R.color.colorIcon);

item.setConsumer((obj) -> {
String url = "http://andnext.club/whatsnote/help.html";
String args = "ts=" + System.currentTimeMillis();
url = url + "?" + args;

PackageUtils.startBrowser(getActivity(), Uri.parse(url));

});

list.add(item);
}

return list;
}

@Override
void buildAdapter(BridgeAdapter adapter) {
adapter.bind(ExplainSettingItem.class,
new BridgeBuilder(ExplainSettingViewHolder.class, ExplainSettingViewHolder.LAYOUT_RESOURCE_ID));

adapter.bind(ProfileSettingItem.class,
new BridgeBuilder(ProfileSettingViewHolder.class, ProfileSettingViewHolder.LAYOUT_RESOURCE_ID));

adapter.bind(BaseSettingItem.class,
new BridgeBuilder(BaseSettingViewHolder.class, BaseSettingViewHolder.LAYOUT_RES_ID));
}
}

6. ProfileFragment

笔名设置界面,该界面比较复杂。

  • List<BaseSettingItem> createList()

添加了9个设置项。

  • void buildAdapter(BridgeAdapter adapter)

绑定了5种ViewHolder。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
public class ProfileFragment extends BasePreferenceFragment {

static final String ID_PROFILE = "profile";
static final String ID_VISION = "vision";

RequestResultManager requestResultManager;

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

this.requestResultManager = new RequestResultManager();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

{
this.setTitle("笔名");
}

{
dividerDecoration.setDrawTop(false);
}
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
requestResultManager.onActivityResult(requestCode, resultCode, data);
}

@Override
List<BaseSettingItem> createList() {
List<BaseSettingItem> list = new ArrayList<>();

{
ProfileSettingItem item = new ProfileSettingItem(PreferenceEntity.obtain().getProfile());
item.setId(ID_PROFILE);
item.setDividerVisible(false);

item.setConsumer((obj) -> requestPicture((file, sourceUri) -> {
ProfileEntity entity = PreferenceEntity.obtain().getProfile();
Uri targetUri = entity.nextPortraitUri();

RequestCropDelegate delegate = new RequestCropDelegate(this,
file,
sourceUri,
targetUri,

(uCrop) -> {

uCrop.withAspectRatio(1, 1);

UCrop.Options options = new UCrop.Options();
options.setCircleDimmedLayer(true);
options.setHideBottomControls(true);
options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
options.setCompressionQuality(60);
options.setShowCropGrid(false);
options.setShowCropFrame(false);

uCrop.withOptions(options);
},

(uri) -> setPortrait(uri));

requestResultManager.request(delegate);
}));

list.add(item);
}

{
ExplainSettingItem item = new ExplainSettingItem("");

list.add(item);
}

{
BaseSettingItem item = new BaseSettingItem("昵称");
item.setDividerType(BaseSettingItem.DIVIDER_NAME);
item.setChevron(true);

item.setConsumer((obj) -> {
RequestNameDelegate delegate = new RequestNameDelegate(this, (name) -> this.setName(name));
requestResultManager.request(delegate);
});

list.add(item);
}

{
BaseSettingItem item = new BaseSettingItem("个性签名");
item.setDividerType(BaseSettingItem.DIVIDER_NONE);
item.setChevron(true);

item.setConsumer((obj) -> {
RequestSignatureDelegate delegate = new RequestSignatureDelegate(this, (signature) -> this.setSignature(signature));
requestResultManager.request(delegate);
});

list.add(item);
}

{
ExplainSettingItem item = new ExplainSettingItem("");
item.setDividerVisible(false);

list.add(item);
}

{
TitleSettingItem item = new TitleSettingItem("个性图签");

list.add(item);
}

{
BaseSettingItem item = new BaseSettingItem("选取新的图片签名");
item.setDividerType(BaseSettingItem.DIVIDER_NAME);
item.setChevron(false);

item.setConsumer((obj) -> requestPicture((file, sourceUri) -> {
ProfileEntity entity = PreferenceEntity.obtain().getProfile();
Uri targetUri = entity.nextVisionUri();

RequestCropDelegate delegate = new RequestCropDelegate(this,
file,
sourceUri,
targetUri,

(uCrop) -> {

uCrop.withAspectRatio(1.f * ProfileEntity.VISION_WIDTH / ProfileEntity.VISION_HEIGHT, 1);

UCrop.Options options = new UCrop.Options();
options.setCircleDimmedLayer(false);
options.setHideBottomControls(true);
options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
options.setCompressionQuality(60);
options.setShowCropGrid(false);
options.setShowCropFrame(false);

uCrop.withOptions(options);
},

(uri) -> setVision(uri));

requestResultManager.request(delegate);
}));

list.add(item);
}

{
PictureSettingItem item = new PictureSettingItem(
PreferenceEntity.obtain().getProfile().getVisionUri(),
ProfileEntity.VISION_WIDTH, ProfileEntity.VISION_HEIGHT);
item.setId(ID_VISION);

item.setErrorResId(R.drawable.ic_profile_vision);

list.add(item);
}

{
ExplainSettingItem item = new ExplainSettingItem("以图片方式分享笔记时,将显示图片签名。");
item.setDividerVisible(false);

list.add(item);
}

return list;
}

@Override
void buildAdapter(BridgeAdapter adapter) {
adapter.bind(TitleSettingItem.class,
new BridgeBuilder(TitleSettingViewHolder.class, TitleSettingViewHolder.LAYOUT_RESOURCE_ID));

adapter.bind(ExplainSettingItem.class,
new BridgeBuilder(ExplainSettingViewHolder.class, ExplainSettingViewHolder.LAYOUT_RESOURCE_ID));

adapter.bind(ProfileSettingItem.class,
new BridgeBuilder(MasterSettingViewHolder.class, MasterSettingViewHolder.LAYOUT_RESOURCE_ID));

adapter.bind(BaseSettingItem.class,
new BridgeBuilder(BaseSettingViewHolder.class, BaseSettingViewHolder.LAYOUT_RES_ID));

adapter.bind(PictureSettingItem.class,
new BridgeBuilder(PictureSettingViewHolder.class, PictureSettingViewHolder.LAYOUT_RES_ID));
}
}

六、Finally

~荒城临古渡~落日满秋山~