RecyclerView中装饰者模式应用

近段时间一直在加班,在赶一个项目,现在项目接近尾声,那么需要对过去一段时间工作内容进行复盘,总结下比较好的解决方案,积累一些经验,我认为的学习方式,是「理论—实践—总结—分享」,这一种很好的沉淀方式。

在之前项目中,有个需求是这样的,要显示书的阅读足迹列表,具体要求是显示最近30天阅读情况,布局是用列表项布局,然后如果有更早的书,则显示更早的阅读情况,布局是用网格布局,如图所示:

显示效果

要是放在之前的做法,一般都是ListView,再对特殊item样式进行单独处理,后来Android在5.0的时候出了一个RecyclerView组件,简单介绍下RecyclerView,一句话:只管回收与复用View,其他的你可以自己去设置,有着高度的解耦,充分的扩展性。至于用法,大家可以去官网查看文档即可,网上也很多文章介绍如何使用,这里不多说。想讲的重点是关于装饰者模式如何在RecyclerView中应用,如下:

  • 装饰者模式介绍
  • RecyclerView中应用
  • 小结

装饰者模式介绍

定义:Decorator模式(别名Wrapper),动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。

也就是说动态地给一个对象添加一些额外的职责,比如你可以增加功能,相比继承来说,有些父类的功能我是不需要的,我可能只用到某部分功能,那么我就可以自由组合,这样就显得灵活点,而不是那么冗余。

有几个要点:

  • 多用组合,少用继承。利用继承在设计子类的行为,在编译时静态决定的,而且所有子类都会继承相同的行为,然而,如果用到组合,则可以在运行时动态地进行扩展,对一些对象做一些改变。
  • 类应该对扩展开发,对修改关闭。
  • 装饰者和被装饰对象有相同的超类型。
  • 可以用一个或多个装饰者包装一个对象
  • 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。
  • 对象可以在任何时候被装饰,所以可以在运行时动态的,不限量的用你喜欢的装饰者来装饰对象。
  • 装饰模式中使用继承的关键是想达到装饰者和被装饰对象的类型匹配,而不是获得其行为。
  • 装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。在实际项目中可以根据需要为装饰者添加新的行为,做到“半透明”装饰者。
  • 适配器模式的用意是改变对象的接口而不一定改变对象的性能,而装饰模式的用意是保持接口并增加对象的职责。

UML图:

装饰者模式UML

RecyclerView中应用

我们既然知道了装饰者和被装饰对象有相同的超类型,在做书的阅读足迹这个页面的时候,整个页面外部是一个RecyclerView,比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<com.dracom.android.sfreader.widget.recyclerview.FeedRootRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="true"/>

</android.support.v4.widget.SwipeRefreshLayout>

同时每个Item项里面又嵌套一个RecyclerView,但外部只有2个Item项,一个Item项代表最近30天要显示的书的内容,一个Item项是显示更早书的内容。其中因为涉及到RecyclerView嵌套的问题,所以需要做滑动冲突的相关处理。所以这里用到自定义扩展的RecyclerView,具体解决滑动冲突代码如下:

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
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);

switch (action) {
case MotionEvent.ACTION_DOWN:
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mInitialTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = (int) (e.getY() + 0.5f);
return super.onInterceptTouchEvent(e);

case MotionEventCompat.ACTION_POINTER_DOWN:
mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
mInitialTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
mInitialTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
return super.onInterceptTouchEvent(e);

case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
if (index < 0) {
return false;
}

final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
if (getScrollState() != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
final boolean canScrollVertically = getLayoutManager().canScrollVertically();
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy) || canScrollVertically)) {
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop && (Math.abs(dy) >= Math.abs(dx) || canScrollHorizontally)) {
startScroll = true;
}
return startScroll && super.onInterceptTouchEvent(e);
}
return super.onInterceptTouchEvent(e);
}

default:
return super.onInterceptTouchEvent(e);
}
}

按照思路就是内部拦截法,也就是RecyclerView自己处理,默认是不拦截,如果滑动距离超过所规定距离,我们就拦截自己处理,设置是可滚动的状态。

解决完滑动冲突之后,具体看看item项中的布局:

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="4dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="4dp">

<LinearLayout
android:id="@+id/head_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="24dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textColor="@color/black_alpha"
android:textSize="16sp"
android:text="@string/zq_account_read_footprint_recent_month"/>

</RelativeLayout>

<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/divider_horizontal_bright"/>

</LinearLayout>

<com.dracom.android.sfreader.widget.recyclerview.BetterRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_marginTop="8dp"
android:orientation="vertical"
android:scrollbars="none"/>

</LinearLayout>

可以看到,每个Item项目一个头部,一个RecyclerView。然后是Adapter的适配,这里就是常用的RecyclerView Adapter的方式,要继承RecyclerView.Adapter<RecyclerView.ViewHolder>方法,同时要实现onCreateViewHolder、onBindViewHolder、getItemCount、getItemViewType方法,具体代码如下:

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
public class ReadFootPrintAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

private final static int RECENT_MONTH = 1002; //最近三十天的Item
private final static int MORE_EARLY = 1003; //更早的Item

private Context mContext;
private List<ReadBookColumnInfo> mColumns;
private RecentReadAdapter mRecentReadAdapter;
private MoreEarlyAdapter mMoreEarlyAdapter;

public ReadFootPrintAdapter(Context context){
this.mContext = context;
mColumns = new ArrayList<ReadBookColumnInfo>();
mRecentReadAdapter = new RecentReadAdapter(context);
mMoreEarlyAdapter = new MoreEarlyAdapter(context);
}

public void setLoadEnable(boolean loadEnable) {
mIsLoadEnable = loadEnable;
}

public void setColumns(List<ReadBookColumnInfo> columns) {
this.mColumns = columns;
notifyDataSetChanged();
}

public void setRecentReadListener(OnOpenBookListener onOpenBookListener){
mRecentReadAdapter.setOnListener(onOpenBookListener);
}

public void setMoreEarlyListener(OnOpenBookListener onOpenBookListener){
mMoreEarlyAdapter.setOnListener(onOpenBookListener);
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == RECENT_MONTH){
View view = LayoutInflater.from(mContext).inflate(R.layout.recycler_read_footprint_recent_month,parent,false);
return new ColumnViewHolder1(view);
} if(viewType == MORE_EARLY){
View view = LayoutInflater.from(mContext).inflate(R.layout.recycler_read_footprint_more_early,parent,false);
return new ColumnViewHolder2(view);
}
else{
return null;
}
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof ColumnViewHolder){
ColumnViewHolder columnViewHolder = (ColumnViewHolder) holder;
ReadBookColumnInfo readBookColumnInfo = mColumns.get(position);
if(readBookColumnInfo.getReadBookInfos().size() > 0){
columnViewHolder.headLayout.setVisibility(View.VISIBLE);
}
else{
columnViewHolder.headLayout.setVisibility(View.GONE);
}
columnViewHolder.loadData(readBookColumnInfo);
}
}

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

@Override
public int getItemViewType(int position) {
if (position == 0 )
return RECENT_MONTH;
else
return MORE_EARLY;
}

private abstract class ColumnViewHolder extends RecyclerView.ViewHolder {
View headLayout;
RecyclerView recyclerView;

public ColumnViewHolder(View itemView) {
super(itemView);
headLayout = itemView.findViewById(R.id.head_layout);
recyclerView = (RecyclerView) itemView.findViewById(R.id.recycler_view);
}

abstract void loadData(ReadBookColumnInfo readBookColumnInfo);
}

private class ColumnViewHolder1 extends ColumnViewHolder {
public ColumnViewHolder1(View itemView) {
super(itemView);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(mContext);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(mRecentReadAdapter);
}

@Override
void loadData(ReadBookColumnInfo readBookColumnInfo) {
mRecentReadAdapter.setData(readBookColumnInfo.getReadBookInfos());
}
}

private class ColumnViewHolder2 extends ColumnViewHolder {
public ColumnViewHolder2(View itemView) {
super(itemView);
GridLayoutManager gridLayoutManager = new GridLayoutManager(mContext,3);
gridLayoutManager.setOrientation(GridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(mMoreEarlyAdapter);
}

@Override
void loadData(ReadBookColumnInfo readBookColumnInfo) {
mMoreEarlyAdapter.setData(readBookColumnInfo.getReadBookInfos());
}
}
}

本来到这里,基本功能是完成了,可后来产品说要加个上下刷新,加载更多的操作。需求是随时可变的, 我们能不变的就是修改的心,那应该怎么做合适呢,是再增加itemType类型,加个加载更多的item项,那样修改的点会更多,此时想到了装饰者模式,是不是可以有个装饰类对这个adapter类进行组合呢,这样不需要修改原来的代码,只要扩展出去,况且我们知道都需要继承RecyclerView.Adapter,那么就可以把ReadFootPrintAdapter当做一个内部成员设置进入。我们来看下装饰者类:

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
public class ReadFootPrintAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

private final static int LOADMORE = 1001;
private final static int NORMAL = 1002;

private RecyclerView.Adapter internalAdapter;
private View mFooterView;

public ReadFootPrintAdapterWrapper(RecyclerView.Adapter adapter){
this.internalAdapter = adapter;
this.mFooterView = null;
}

public void addFooterView(View footView) {
mFooterView = footView;
}

public void notifyDataChanged() {
internalAdapter.notifyDataSetChanged();
notifyDataSetChanged();
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == LOADMORE){
return new LoadMoreViewHolder(mFooterView);
}
return internalAdapter.createViewHolder(parent,viewType);
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof LoadMoreViewHolder){
return;
}else{
internalAdapter.onBindViewHolder(holder,position);
}
}

@Override
public int getItemCount() {
int count = internalAdapter.getItemCount();
if (mFooterView != null && count != 0) count++;
return count;
}

@Override
public int getItemViewType(int position) {
if(mFooterView != null && getItemCount() - 1 == position)
return LOADMORE;
return NORMAL;
}

public class LoadMoreViewHolder extends RecyclerView.ViewHolder {
public LoadMoreViewHolder(View itemView) {
super(itemView);
}
}
}

这个Wrapper类就是装饰类,里面包含了一个RecyclerView.Adapter类型的成员,一个底部View,到时候在外部调用的时候,只需要传递一个RecyclerView.Adapter类型的参数进去即可,这样就形成了组合的关系。具体使用如下:

1
2
3
4
mFooterView = LayoutInflater.from(mContext).inflate(R.layout.refresh_loadmore_layout, mRecyclerView, false);
mReadFootPrintAdapterWrapper = new ReadFootPrintAdapterWrapper(mReadFootPrintAdapter);
mReadFootPrintAdapterWrapper.addFooterView(mFooterView);
mRecyclerView.setAdapter(mReadFootPrintAdapterWrapper);

这样即达到需求要求,又能对原来已有的代码不进行修改,只进行扩展,何乐而不为。

小结

这虽然是工作中一个应用点,但我想在开发过程中还有很多应用点,用上设计模式。日常开发中基本都强调设计模式的重要性,或许你对23种设计模式都很熟悉,都了解到它们各自的定义,可是等真正应用了,却发现没有踪迹可寻,写代码也是按照以前老的思路去做,那样就变成了知道是知道,却不会用的尴尬局面。如何突破呢,我觉得事后复盘和重构很有必要,就是利用项目尾声阶段,空的时候去review下自己写过的代码,反思是否有更简洁的写法,还有可以参考优秀代码,它们是怎么写,这样给自己找找灵感,再去结合自己已有的知识存储,说不定就能走上理论和实践相结合道路上。

,