吸顶效果RecyclerView的实现

概述

最近翻开以前fork的github代码,发现了AmazingListView,是google官方当年的时候写的实现吸顶效果的ListView组件,然后最近在项目中有遇到需要吸顶效果的RecyclerView的需求,于是看了下源码,在AmazingListView的基础上,改进了一些bug,最终出现了今天的PinnedRecyclerView.

示例

最后的结果长这个样子.

吸顶标题高度小于普通节点.
吸顶标题高度大于普通节点.
谷歌AmazingListView当吸顶标题高度大于普通节点高度的时候会有跳变,效果如下,原因后面分析.


网上有介绍RecyclerView很好的文章,我个人比较喜欢这个,大家不妨先看看.
http://blog.csdn.net/lmj623565791/article/details/45059587

核心代码

两个核心类AmazingListView.java和AmazingAdapter.java.
AmazingAdapter继承自BaseAdapter并实现了SectionIndexer, OnScrollListener接口,在onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)的时候,不断去触发调整吸顶标题View的状态.
标题栏主要有三种状态,显示,不现实和上推过程中的部分显示,如代码中所定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Pinned header state: don't show the header.
*/
public static final int PINNED_HEADER_GONE = 0;
/**
* Pinned header state: show the header at the top of the list.
*/
public static final int PINNED_HEADER_VISIBLE = 1;
/**
* Pinned header state: show the header. If the header extends beyond
* the bottom of the first shown element, push it up and clip.
*/
public static final int PINNED_HEADER_PUSHED_UP = 2;

在滚动的过程,始终去判断第一个可见元素的状态,在google的代码中,他们关心的是每个组别的最后一个节点的底部到父容器的顶部的距离的变化.
其中这三种状态的分类大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int getPinnedHeaderState(int position) {
if (position < 0 || getCount() == 0) {
return PINNED_HEADER_GONE;
}
// The header should get pushed up if the top item shown
// is the last item in a section for a particular letter.
int section = getSectionForPosition(position);
int nextSectionPosition = getPositionForSection(section + 1);
if (nextSectionPosition != -1 && position == nextSectionPosition - 1) {
return PINNED_HEADER_PUSHED_UP;
}
return PINNED_HEADER_VISIBLE;
}

呐,从代码中可以看到他们上推的条件是从上一个组的最后一个切换到下一个组.
ok初步的状态判断到此就差不多完成了,接下来要关注AmazingListView了,这个类继承自ListView,反正我们把它看成是一个ViewGroup,我们需要将一个标题栏画出来,那么首先有这个几件事情要去做,
首先,new一个View出来,它的长相要和普通标题栏一摸一样,google直接用了同一个布局R.layout.item_composer_header,在普通节点里面只是将这个布局给隐藏了.

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
public void setPinnedHeaderView(View view) {
mHeaderView = view;
// Disable vertical fading when the pinned header is present
// TODO change ListView to allow separate measures for top and bottom fading edge;
// in this particular case we would like to disable the top, but not the bottom edge.
if (mHeaderView != null) {
setFadingEdgeLength(0);
}
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mHeaderView != null) {
measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
mHeaderViewWidth = mHeaderView.getMeasuredWidth();
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG,"onLayout is called");
Log.d(TAG,String.valueOf(getFirstVisiblePosition()));
if (mHeaderView != null) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
configureHeaderView(getFirstVisiblePosition());
}
}

首先给ListView设置一个View作为吸顶头,然后在onMeasure获得其基本高度参与后面的计算,并且由于configureHeaderView中不断的调用了view的layout方法,那么会不断的触发onLayout方法,不断的根据参数放置view,最后通过

1
2
3
4
5
6
7
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHeaderViewVisible) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}

好了,现在到了最核心的地方了,就是根据传入的第一个可见位置,去计算吸顶view的状态,核心代码如下:

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
public void configureHeaderView(int position) {
if (mHeaderView == null) {
return;
}
int state = adapter.getPinnedHeaderState(position);
switch (state) {
case AmazingAdapter.PINNED_HEADER_GONE: {
mHeaderViewVisible = false;
break;
}
case AmazingAdapter.PINNED_HEADER_VISIBLE: {
adapter.configurePinnedHeader(mHeaderView, position, 255);
if (mHeaderView.getTop() != 0) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
}
mHeaderViewVisible = true;
break;
}
case AmazingAdapter.PINNED_HEADER_PUSHED_UP: {
View firstView = getChildAt(0);
if (firstView != null) {
int bottom = firstView.getBottom();
int headerHeight = mHeaderView.getHeight();
int y;
int alpha;
if (bottom < headerHeight) {
y = (bottom - headerHeight);
alpha = 255 * (headerHeight + y) / headerHeight;
} else {
y = 0;
alpha = 255;
}
adapter.configurePinnedHeader(mHeaderView, position, alpha);
if (mHeaderView.getTop() != y) {
mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
}
mHeaderViewVisible = true;
}
break;
}
}
}

如果是不可见的,那么设置为不可见,后面就不绘制了,如果是可见的,放在最顶部,直接绘制,如果是处于上推状态的,那么要计算要显示的高度和一个上推程度,这样我们实现的时候可以根据这两个数据去控制前端交互.
好了,到此,原理就完全结束了,那么怎么用呢?其实主要就是去实现几个接口.来看几个主要的接口吧:

1
2
3
4
5
6
7
8
9
10
@Override
protected void bindSectionHeader(View view, int position, boolean displaySectionHeader) {
if (displaySectionHeader) {
view.findViewById(R.id.header).setVisibility(View.VISIBLE);
TextView lSectionTitle = (TextView) view.findViewById(R.id.header);
lSectionTitle.setText(getSections()[getSectionForPosition(position)]);
} else {
view.findViewById(R.id.header).setVisibility(View.GONE);
}
}

根据是否要显示标题来确定是否要隐藏,普通节点是要隐藏的,同时在此根据索引设置标题.

1
2
3
4
5
6
7
@Override
public void configurePinnedHeader(View header, int position, int alpha) {
TextView lSectionHeader = (TextView)header;
lSectionHeader.setText(getSections()[getSectionForPosition(position)]);
lSectionHeader.setBackgroundColor(alpha << 24 | (0xbbffbb));
lSectionHeader.setTextColor(alpha << 24 | (0x000000));
}

这里是设置标题的状态变化区域,alpha可以理解为上推程度.做了设置标题和透明度设置的两种操作,这里你也可以根据alpha去便宜文字也行.

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
@Override
public int getPositionForSection(int section) {
if (section < 0) section = 0;
if (section >= all.size()) section = all.size() - 1;
int c = 0;
for (int i = 0; i < all.size(); i++) {
if (section == i) {
return c;
}
c += all.get(i).second.size();
}
return 0;
}
@Override
public int getSectionForPosition(int position) {
int c = 0;
for (int i = 0; i < all.size(); i++) {
if (position >= c && position < c + all.get(i).second.size()) {
return i;
}
c += all.get(i).second.size();
}
return -1;
}
@Override
public String[] getSections() {
String[] res = new String[all.size()];
for (int i = 0; i < all.size(); i++) {
res[i] = all.get(i).first;
}
return res;
}

这三个接口是SectionIndexer的标准接口,工作的时候通过他们来获取组相关的数据.
好了,到此怎么用也讲完了.

移植到RecyclerView

RecyclerView相较于ListView进行了解耦,把一个寻找索引的操作放在了LayoutManager中.根据以上的原理,那么移植起来其实也挺容易的,但是我觉得原来的接口写起来不是很直观的方便,那么就稍微DIY了一下,反正直接上代码吧,两个类第一版最后长这样.

PinnedRecyclerView和PinnedRecyclerAdapter
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
public class PinnedRecyclerView extends RecyclerView {
public static final String TAG = PinnedRecyclerView.class.getSimpleName();
private View mHeaderView;
private boolean mHeaderViewVisible;
private int mHeaderViewWidth;
private int mHeaderViewHeight;
private LinearLayoutManager layoutManager;
private PinnedRecyclerAdapter adapter;
private void setPinnableHeaderView(View headerView) {
mHeaderView = headerView;
requestLayout();
}
public void setRecyclerViewAdapter(PinnedRecyclerAdapter adapter) {
this.adapter = adapter;
setAdapter(adapter);
setPinnableHeaderView(LayoutInflater.from(getContext()).inflate(adapter.getPinnableHeaderView(), this, false));
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
if (mHeaderView != null) {
measureChild(mHeaderView, widthSpec, heightSpec);
mHeaderViewWidth = mHeaderView.getMeasuredWidth();
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mHeaderView != null) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
configureHeaderView(layoutManager.findFirstVisibleItemPosition());
}
}
public void configureHeaderView(int position) {
Log.d(TAG,String.valueOf(position));
if (mHeaderView == null) return;
int state = adapter.getPinnedHeaderState(position);
switch (state) {
case PinnedRecyclerAdapter.PINNED_HEADER_GONE: {
mHeaderViewVisible = false;
break;
}
case PinnedRecyclerAdapter.PINNED_HEADER_VISIBLE: {
adapter.configurePinnableHeader(mHeaderView, position, 255);
if (mHeaderView.getTop() != 0) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
}
mHeaderViewVisible = true;
break;
}
case PinnedRecyclerAdapter.PINNED_HEADER_PUSHED_UP: {
View firstView = getChildAt(0);
if (firstView != null) {
int bottom = firstView.getBottom();
int headerHeight = mHeaderView.getHeight();
int y;
int alpha;
if (bottom < headerHeight) {
y = (bottom - headerHeight);
alpha = 255 * (headerHeight + y) / headerHeight;
} else {
y = 0;
alpha = 255;
}
adapter.configurePinnableHeader(mHeaderView, position, alpha);
if (mHeaderView.getTop() != y) {
mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
}
mHeaderViewVisible = true;
}
break;
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHeaderViewVisible) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
configureHeaderView(layoutManager.findFirstVisibleItemPosition());
}
public PinnedRecyclerView(Context context) {
super(context);
init(context);
}
public PinnedRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public PinnedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
layoutManager = new LinearLayoutManager(context);
setLayoutManager(layoutManager);
setFadingEdgeLength(0);
}
}

再来看看我们的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
public abstract class PinnedRecyclerAdapter extends RecyclerView.Adapter<PinnedRecyclerAdapter.PinnedViewHolder> implements SectionIndexer {
public static final String TAG = PinnedRecyclerAdapter.class.getSimpleName();
/**
* Pinned header state: don't show the header.
*/
public static final int PINNED_HEADER_GONE = 0;
/**
* Pinned header state: show the header at the top of the list.
*/
public static final int PINNED_HEADER_VISIBLE = 1;
/**
* extends
* Pinned header state: show the header. If the header beyond
* the bottom of the first shown element, push it up and clip.
*/
public static final int PINNED_HEADER_PUSHED_UP = 2;
@Override
public PinnedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
PinnedViewHolder holder = new PinnedViewHolder(inflater.inflate(R.layout.layout_pinnable_recycler_view_abstract_item, parent, false));
holder.header.removeAllViews();
holder.content.removeAllViews();
holder.header.addView(inflater.inflate(getPinnableHeaderView(), parent, false));
holder.content.addView(inflater.inflate(getPinnableContentView(viewType), parent, false));
return holder;
}
public int getPinnedHeaderState(int position) {
if (position < 0 || getItemCount() == 0) return PINNED_HEADER_GONE;
int section = getSectionForPosition(position);
int nextSectionPosition = getPositionForSection(section + 1);
if (nextSectionPosition != -1 && position == nextSectionPosition - 1) {
return PINNED_HEADER_PUSHED_UP;
}
return PINNED_HEADER_VISIBLE;
}
@Override
public void onBindViewHolder(PinnedViewHolder holder, int position) {
final int section = getSectionForPosition(position);
boolean shouldShowHeader = (getPositionForSection(section) == position);
configureSection(holder.header, holder.content, position, getItemViewType(position), shouldShowHeader);
}
public class PinnedViewHolder extends RecyclerView.ViewHolder {
public ViewGroup header;
public ViewGroup content;
public View itemView;
public PinnedViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
header = (ViewGroup) itemView.findViewById(R.id.abstract_header);
content = (ViewGroup) itemView.findViewById(R.id.abstract_content);
}
}
@Override
public abstract int getItemViewType(int position);
@Override
public abstract int getItemCount();
@Override
public abstract Object[] getSections();
@Override
public abstract int getPositionForSection(int i);
@Override
public abstract int getSectionForPosition(int i);
public abstract Object getItem(int position);
protected abstract void configureSection(View header, View content, int position, int viewType, boolean shouldShowHeader);
public abstract int getPinnableHeaderView();
public abstract int getPinnableContentView(int viewType);
public abstract void configurePinnableHeader(View header, int position, int progress);
}

看看怎么用吧:
相较而言,主要是简化了设置view的操作,只需要提供两个布局就行了,我在代码里面自动设置了

1
2
3
4
5
6
7
8
9
@Override
public int getPinnableHeaderView() {
return R.layout.item_composer_header;
}
@Override
public int getPinnableContentView(int viewType) {
return R.layout.item_composer_content;
}

绑定数据的时候也更方便,我把头和内容节点都给出来了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void configureSection(View header, View content, int position, int viewType, boolean shouldShowHeader) {
if (shouldShowHeader) {
header.findViewById(R.id.header).setVisibility(View.VISIBLE);
TextView lSectionTitle = (TextView) header.findViewById(R.id.header);
lSectionTitle.setText(getSections()[getSectionForPosition(position)]);
} else {
header.findViewById(R.id.header).setVisibility(View.GONE);
}
TextView lName = (TextView) content.findViewById(R.id.lName);
TextView lYear = (TextView) content.findViewById(R.id.lYear);
Composer composer = (Composer) getItem(position);
lName.setText(composer.name);
lYear.setText(composer.year);
}

反正,我觉得这样用着比原来方便了许多.好了,终于可以使用它来愉快的玩耍了.

bug发现了

咦,不对啊,从这个原理上去看,要是上一个单元的最后一个节点的高度还没有吸顶标题高度高,岂不是会跳变?改一下代码实现一下,将item_composer_header.xml的高度android:layout_height=”wrap_content”改为android:layout_height=”60dp”,那么结果就像上面的gif那样.
这不是我们想要的,那么如果用原来的思路,不然没法实现,于是,换机制呗,搞发搞发,有了,那么就算第一个完全可见标题节点离顶部的距离呗,好了,按照这个思路写下去,最后就有了我们最终版本的PinnedRecyclerView.

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
public class PinnedRecyclerView extends RecyclerView {
public static final String TAG = PinnedRecyclerView.class.getSimpleName();
private View mHeaderView;
private boolean mHeaderViewVisible;
private int mHeaderViewWidth;
private int mHeaderViewHeight;
private LinearLayoutManager layoutManager;
private PinnedRecyclerAdapter adapter;
private void setPinnedHeaderView(View headerView) {
mHeaderView = headerView;
requestLayout();
}
public void setRecyclerViewAdapter(PinnedRecyclerAdapter adapter) {
this.adapter = adapter;
setAdapter(adapter);
setPinnedHeaderView(LayoutInflater.from(getContext()).inflate(adapter.getPinnedHeaderView(), this, false));
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
if (mHeaderView != null) {
measureChild(mHeaderView, widthSpec, heightSpec);
mHeaderViewWidth = mHeaderView.getMeasuredWidth();
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (mHeaderView != null) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
configureHeaderView(adapter.findFirstCompleteVisibleHeaderPosition(layoutManager.findFirstCompletelyVisibleItemPosition()));
}
}
public void configureHeaderView(int position) {
Log.d(TAG, String.valueOf(position));
if (mHeaderView == null || position == 0) return;
int state = adapter.getPinnedHeaderState(position);
switch (state) {
case PinnedRecyclerAdapter.PINNED_HEADER_GONE: {
mHeaderViewVisible = false;
break;
}
case PinnedRecyclerAdapter.PINNED_HEADER_VISIBLE: {
adapter.configurePinnedHeader(mHeaderView, position-1, 255);
if (mHeaderView.getTop() != 0) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
}
mHeaderViewVisible = true;
break;
}
case PinnedRecyclerAdapter.PINNED_HEADER_PUSHED_UP: {
View firstCompleteVisibleView = getChildAt(position - layoutManager.findFirstVisibleItemPosition());
if (firstCompleteVisibleView != null) {
int top = firstCompleteVisibleView.getTop();
int headerHeight = mHeaderView.getHeight();
int y;
int alpha;
if (top < headerHeight) {
y = (top - headerHeight);
alpha = 255 * (headerHeight + y) / headerHeight;
} else {
y = 0;
alpha = 255;
}
adapter.configurePinnedHeader(mHeaderView, position-1, alpha);
if (mHeaderView.getTop() != y) {
mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
}
mHeaderViewVisible = true;
}
break;
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHeaderViewVisible) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}
@Override
protected void onScrollChanged(int l, int t, int old_l, int old_t) {
super.onScrollChanged(l, t, old_l, old_t);
configureHeaderView(adapter.findFirstCompleteVisibleHeaderPosition(layoutManager.findFirstCompletelyVisibleItemPosition()));
}
public PinnedRecyclerView(Context context) {
super(context);
init(context);
}
public PinnedRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public PinnedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
layoutManager = new LinearLayoutManager(context);
setLayoutManager(layoutManager);
setFadingEdgeLength(0);
}
}

我们的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
public abstract class PinnedRecyclerAdapter extends RecyclerView.Adapter<PinnedRecyclerAdapter.PinnedViewHolder> {
public static final String TAG = PinnedRecyclerAdapter.class.getSimpleName();
public static final int INDEX_NOT_FOUND = -1;
/**
* Pinned header state: don't show the header.
*/
public static final int PINNED_HEADER_GONE = 0;
/**
* Pinned header state: show the header at the top of the list.
*/
public static final int PINNED_HEADER_VISIBLE = 1;
/**
* extends
* Pinned header state: show the header. If the header beyond
* the bottom of the first shown element, push it up and clip.
*/
public static final int PINNED_HEADER_PUSHED_UP = 2;
@Override
public PinnedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
PinnedViewHolder holder = new PinnedViewHolder(inflater.inflate(R.layout.layout_pinnable_recycler_view_abstract_item, parent, false));
holder.header.removeAllViews();
holder.content.removeAllViews();
holder.header.addView(inflater.inflate(getPinnedHeaderView(), parent, false));
holder.content.addView(inflater.inflate(getPinnedContentView(viewType), parent, false));
return holder;
}
@Override
public void onBindViewHolder(PinnedViewHolder holder, int position) {
configureSection(holder.header, holder.content, position, getItemViewType(position), isPositionHeader(position));
}
public class PinnedViewHolder extends RecyclerView.ViewHolder {
public ViewGroup header;
public ViewGroup content;
public View itemView;
public PinnedViewHolder(View itemView) {
super(itemView);
this.itemView = itemView;
header = (ViewGroup) itemView.findViewById(R.id.abstract_header);
content = (ViewGroup) itemView.findViewById(R.id.abstract_content);
}
}
public int getPinnedHeaderState(int firstCompleteVisiblePosition) {
if (firstCompleteVisiblePosition < 0 || getItemCount() == 0) return PINNED_HEADER_GONE;
if (isPositionHeader(firstCompleteVisiblePosition)) return PINNED_HEADER_PUSHED_UP;
return PINNED_HEADER_VISIBLE;
}
public int findFirstCompleteVisibleHeaderPosition(int firstCompleteVisiblePosition) {
for (int i = firstCompleteVisiblePosition; i < getItemCount(); i++) {
if (isPositionHeader(i)) return i;
}
return INDEX_NOT_FOUND;
}
@Override
public abstract int getItemViewType(int position);
@Override
public abstract int getItemCount();
protected abstract void configureSection(View header, View content, int position, int viewType, boolean shouldShowHeader);
protected abstract int getPinnedHeaderView();
protected abstract int getPinnedContentView(int viewType);
protected abstract boolean isPositionHeader(int position);
protected abstract void configurePinnedHeader(View header, int position, int progress);
protected abstract List<Integer> getHeaderIndexes();
protected abstract Object[] getHeaderData();
protected abstract Object getItem(int position);
protected abstract int getHeaderIndexForPosition(int position);

可以看到,我在原来的基础上又优化了一些接口,实现起来更加直观,自己认为哈…
最后我们要实现的页面需要如下操作就行了:

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
public class DemoActivity extends Activity {
PinnedRecyclerView lsComposer;
SectionComposerRecyclerAdapter adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_section_recycler);
lsComposer = (PinnedRecyclerView) findViewById(R.id.lsComposer);
lsComposer.setRecyclerViewAdapter(adapter = new SectionComposerRecyclerAdapter());
}
class SectionComposerRecyclerAdapter extends PinnedRecyclerAdapter {
List<Pair<String, List<Composer>>> all = Data.getAllData();
@Override
protected void configureSection(View header, View content, int position, int viewType, boolean shouldShowHeader) {
if (shouldShowHeader) {
header.findViewById(R.id.header).setVisibility(View.VISIBLE);
TextView lSectionTitle = (TextView) header.findViewById(R.id.header);
lSectionTitle.setText(getHeaderData()[getHeaderIndexForPosition(position)]);
} else {
header.findViewById(R.id.header).setVisibility(View.GONE);
}
TextView lName = (TextView) content.findViewById(R.id.lName);
TextView lYear = (TextView) content.findViewById(R.id.lYear);
Composer composer = (Composer) getItem(position);
lName.setText(composer.name);
lYear.setText(composer.year);
}
@Override
public void configurePinnedHeader(View header, int position, int progress) {
TextView lSectionHeader = (TextView) header;
lSectionHeader.setText(getHeaderData()[getHeaderIndexForPosition(position)]);
lSectionHeader.setTextColor(progress << 24 | (0x000000));
}
@Override
public Object getItem(int position) {
int c = 0;
for (int i = 0; i < all.size(); i++) {
if (position >= c && position < c + all.get(i).second.size()) {
return all.get(i).second.get(position - c);
}
c += all.get(i).second.size();
}
return null;
}
@Override
public int getPinnedHeaderView() {
return R.layout.item_composer_header;
}
@Override
public int getPinnedContentView(int viewType) {
return R.layout.item_composer_content;
}
@Override
public boolean isPositionHeader(int position) {
return getHeaderIndexes().contains(position);
}
@Override
public List<Integer> getHeaderIndexes() {
List<Integer> indexes = new ArrayList<>();
int c = 0;
for (int i = 0; i < all.size(); i++) {
indexes.add(c);
c += all.get(i).second.size();
}
return indexes;
}
@Override
public int getItemViewType(int position) {
return 0;
}
@Override
public int getItemCount() {
int res = 0;
for (int i = 0; i < all.size(); i++) {
res += all.get(i).second.size();
}
return res;
}
@Override
public String[] getHeaderData() {
String[] res = new String[all.size()];
for (int i = 0; i < all.size(); i++) {
res[i] = all.get(i).first;
}
return res;
}
@Override
public int getHeaderIndexForPosition(int position) {
int c = 0;
for (int i = 0; i < all.size(); i++) {
if (position >= c && position < c + all.get(i).second.size()) {
return i;
}
c += all.get(i).second.size();
}
return -1;
}
}

总结

最终的代码放在了github上,链接是 https://github.com/gordon-rawe/pinned-recycler-view .有兴趣的来看一发.对了,我们的RecyclerView是支持多种节点类型的哦.
代码一共三个分支,bottom是一开始的根据上个组最后节点状态计算的机制实现的分支.top是最新的根据第一个可见标题节点距顶部距离计算机制实现的分支.

写博客好费劲啊,是时候来我的github上搓一下了star了撒.