Skip to content

Commit c81069c

Browse files
author
Rafael Baboni Dominiquini
committed
added position change listener
1 parent e257dcd commit c81069c

11 files changed

+589
-534
lines changed

library/src/main/java/com/timehop/stickyheadersrecyclerview/HeaderPositionCalculator.java

Lines changed: 201 additions & 186 deletions
Large diffs are not rendered by default.

library/src/main/java/com/timehop/stickyheadersrecyclerview/StickyRecyclerHeadersAdapter.java

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,34 @@
44
import android.view.ViewGroup;
55

66
public interface StickyRecyclerHeadersAdapter<VH extends RecyclerView.ViewHolder> {
7-
/**
8-
* Get the ID of the header associated with this item. For example, if your headers group
9-
* items by their first letter, you could return the character representation of the first letter.
10-
* Return a value < 0 if the view should not have a header (like, a header view or footer view)
11-
*
12-
* @param position
13-
* @return
14-
*/
15-
public long getHeaderId(int position);
7+
/**
8+
* Get the ID of the header associated with this item. For example, if your headers group
9+
* items by their first letter, you could return the character representation of the first letter.
10+
* Return a value < 0 if the view should not have a header (like, a header view or footer view)
11+
*
12+
* @param position
13+
* @return
14+
*/
15+
public int getHeaderId(int position);
1616

17-
/**
18-
* Creates a new ViewHolder for a header. This works the same way onCreateViewHolder in
19-
* Recycler.Adapter, ViewHolders can be reused for different views. This is usually a good place
20-
* to inflate the layout for the header.
21-
*
22-
* @param parent
23-
* @return
24-
*/
25-
public VH onCreateHeaderViewHolder(ViewGroup parent);
17+
/**
18+
* Creates a new ViewHolder for a header. This works the same way onCreateViewHolder in
19+
* Recycler.Adapter, ViewHolders can be reused for different views. This is usually a good place
20+
* to inflate the layout for the header.
21+
*
22+
* @param parent
23+
* @param position
24+
* @return
25+
*/
26+
public VH onCreateHeaderViewHolder(ViewGroup parent, int position);
2627

27-
/**
28-
* Binds an existing ViewHolder to the specified adapter position.
29-
*
30-
* @param holder
31-
* @param position
32-
*/
33-
public void onBindHeaderViewHolder(VH holder, int position);
28+
/**
29+
* Binds an existing ViewHolder to the specified adapter position.
30+
*
31+
* @param holder
32+
* @param position
33+
*/
34+
public void onBindHeaderViewHolder(VH holder, int position);
3435

35-
public int getItemCount();
36+
public int getItemCount();
3637
}

library/src/main/java/com/timehop/stickyheadersrecyclerview/StickyRecyclerHeadersDecoration.java

Lines changed: 139 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -16,133 +16,152 @@
1616

1717
public class StickyRecyclerHeadersDecoration extends RecyclerView.ItemDecoration {
1818

19-
private final StickyRecyclerHeadersAdapter mAdapter;
20-
private final SparseArray<Rect> mHeaderRects = new SparseArray<>();
21-
private final HeaderProvider mHeaderProvider;
22-
private final OrientationProvider mOrientationProvider;
23-
private final HeaderPositionCalculator mHeaderPositionCalculator;
24-
private final HeaderRenderer mRenderer;
25-
private final DimensionCalculator mDimensionCalculator;
26-
27-
// TODO: Consider passing in orientation to simplify orientation accounting within calculation
28-
public StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter) {
29-
this(adapter, new LinearLayoutOrientationProvider(), new DimensionCalculator());
30-
}
31-
32-
private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, OrientationProvider orientationProvider,
33-
DimensionCalculator dimensionCalculator) {
34-
this(adapter, orientationProvider, dimensionCalculator, new HeaderRenderer(orientationProvider),
35-
new HeaderViewCache(adapter, orientationProvider));
36-
}
37-
38-
private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, OrientationProvider orientationProvider,
39-
DimensionCalculator dimensionCalculator, HeaderRenderer headerRenderer, HeaderProvider headerProvider) {
40-
this(adapter, headerRenderer, orientationProvider, dimensionCalculator, headerProvider,
41-
new HeaderPositionCalculator(adapter, headerProvider, orientationProvider,
42-
dimensionCalculator));
43-
}
44-
45-
private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, HeaderRenderer headerRenderer,
46-
OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator, HeaderProvider headerProvider,
47-
HeaderPositionCalculator headerPositionCalculator) {
48-
mAdapter = adapter;
49-
mHeaderProvider = headerProvider;
50-
mOrientationProvider = orientationProvider;
51-
mRenderer = headerRenderer;
52-
mDimensionCalculator = dimensionCalculator;
53-
mHeaderPositionCalculator = headerPositionCalculator;
54-
}
55-
56-
@Override
57-
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
58-
super.getItemOffsets(outRect, view, parent, state);
59-
int itemPosition = parent.getChildAdapterPosition(view);
60-
if (itemPosition == RecyclerView.NO_POSITION) {
61-
return;
19+
private final StickyRecyclerHeadersAdapter mAdapter;
20+
private final SparseArray<Rect> mHeaderRects = new SparseArray<>();
21+
private final HeaderProvider mHeaderProvider;
22+
private final OrientationProvider mOrientationProvider;
23+
private final HeaderPositionCalculator mHeaderPositionCalculator;
24+
private final HeaderRenderer mRenderer;
25+
private final DimensionCalculator mDimensionCalculator;
26+
private StickyRecyclerHeadersPositionChangeListener mHeaderListener;
27+
28+
// TODO: Consider passing in orientation to simplify orientation accounting within calculation
29+
public StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter) {
30+
this(adapter, new LinearLayoutOrientationProvider(), new DimensionCalculator());
6231
}
63-
if (mHeaderPositionCalculator.hasNewHeader(itemPosition)) {
64-
View header = getHeaderView(parent, itemPosition);
65-
setItemOffsetsForHeader(outRect, header, mOrientationProvider.getOrientation(parent));
32+
33+
private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, OrientationProvider orientationProvider,
34+
DimensionCalculator dimensionCalculator) {
35+
this(adapter, orientationProvider, dimensionCalculator, new HeaderRenderer(orientationProvider),
36+
new HeaderViewCache(adapter, orientationProvider));
37+
}
38+
39+
private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, OrientationProvider orientationProvider,
40+
DimensionCalculator dimensionCalculator, HeaderRenderer headerRenderer, HeaderProvider headerProvider) {
41+
this(adapter, headerRenderer, orientationProvider, dimensionCalculator, headerProvider,
42+
new HeaderPositionCalculator(adapter, headerProvider, orientationProvider,
43+
dimensionCalculator));
44+
}
45+
46+
private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, HeaderRenderer headerRenderer,
47+
OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator, HeaderProvider headerProvider,
48+
HeaderPositionCalculator headerPositionCalculator) {
49+
mAdapter = adapter;
50+
mHeaderProvider = headerProvider;
51+
mOrientationProvider = orientationProvider;
52+
mRenderer = headerRenderer;
53+
mDimensionCalculator = dimensionCalculator;
54+
mHeaderPositionCalculator = headerPositionCalculator;
55+
}
56+
57+
@Override
58+
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
59+
super.getItemOffsets(outRect, view, parent, state);
60+
int itemPosition = parent.getChildAdapterPosition(view);
61+
if (itemPosition == RecyclerView.NO_POSITION) {
62+
return;
63+
}
64+
if (mHeaderPositionCalculator.hasNewHeader(itemPosition)) {
65+
View header = getHeaderView(parent, itemPosition);
66+
setItemOffsetsForHeader(outRect, header, mOrientationProvider.getOrientation(parent));
67+
}
6668
}
67-
}
68-
69-
/**
70-
* Sets the offsets for the first item in a section to make room for the header view
71-
*
72-
* @param itemOffsets rectangle to define offsets for the item
73-
* @param header view used to calculate offset for the item
74-
* @param orientation used to calculate offset for the item
75-
*/
76-
private void setItemOffsetsForHeader(Rect itemOffsets, View header, int orientation) {
77-
Rect headerMargins = mDimensionCalculator.getMargins(header);
78-
if (orientation == LinearLayoutManager.VERTICAL) {
79-
itemOffsets.top = header.getHeight() + headerMargins.top + headerMargins.bottom;
80-
} else {
81-
itemOffsets.left = header.getWidth() + headerMargins.left + headerMargins.right;
69+
70+
/**
71+
* Sets the offsets for the first item in a section to make room for the header view
72+
*
73+
* @param itemOffsets rectangle to define offsets for the item
74+
* @param header view used to calculate offset for the item
75+
* @param orientation used to calculate offset for the item
76+
*/
77+
private void setItemOffsetsForHeader(Rect itemOffsets, View header, int orientation) {
78+
Rect headerMargins = mDimensionCalculator.getMargins(header);
79+
if (orientation == LinearLayoutManager.VERTICAL) {
80+
itemOffsets.top = header.getHeight() + headerMargins.top + headerMargins.bottom;
81+
} else {
82+
itemOffsets.left = header.getWidth() + headerMargins.left + headerMargins.right;
83+
}
84+
}
85+
86+
@Override
87+
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
88+
super.onDrawOver(canvas, parent, state);
89+
mHeaderRects.clear();
90+
91+
if (parent.getChildCount() <= 0 || mAdapter.getItemCount() <= 0) {
92+
return;
93+
}
94+
95+
for (int i = 0; i < parent.getChildCount(); i++) {
96+
View itemView = parent.getChildAt(i);
97+
int position = parent.getChildAdapterPosition(itemView);
98+
if (position == RecyclerView.NO_POSITION) {
99+
continue;
100+
}
101+
102+
boolean hasStickyHeader = mHeaderPositionCalculator.hasStickyHeader(itemView, mOrientationProvider.getOrientation(parent), position);
103+
if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position)) {
104+
View header = mHeaderProvider.getHeader(parent, position);
105+
Rect headerOffset = mHeaderPositionCalculator.getHeaderBounds(parent, header,
106+
itemView, hasStickyHeader);
107+
mRenderer.drawHeader(parent, canvas, header, headerOffset);
108+
mHeaderRects.put(position, headerOffset);
109+
110+
if (mHeaderListener != null) {
111+
mHeaderListener.onHeaderPositionChanged(mAdapter.getHeaderId(position), header, position, headerOffset);
112+
}
113+
}
114+
}
82115
}
83-
}
84116

85-
@Override
86-
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
87-
super.onDrawOver(canvas, parent, state);
88-
mHeaderRects.clear();
117+
/**
118+
* Verify if header obscure some item on RecyclerView
119+
*
120+
* @param header The header to verify
121+
* @return first item that is fully beneath a header
122+
*/
123+
public boolean headerObscuringSomeItem(RecyclerView recyclerView, View header) {
124+
return mHeaderPositionCalculator.headerObscuringSomeItem(recyclerView, header);
125+
}
89126

90-
if (parent.getChildCount() <= 0 || mAdapter.getItemCount() <= 0) {
91-
return;
127+
/**
128+
* Gets the position of the header under the specified (x, y) coordinates.
129+
*
130+
* @param x x-coordinate
131+
* @param y y-coordinate
132+
* @return position of header, or -1 if not found
133+
*/
134+
public int findHeaderPositionUnder(int x, int y) {
135+
for (int i = 0; i < mHeaderRects.size(); i++) {
136+
Rect rect = mHeaderRects.get(mHeaderRects.keyAt(i));
137+
if (rect.contains(x, y)) {
138+
return mHeaderRects.keyAt(i);
139+
}
140+
}
141+
return -1;
92142
}
93143

94-
for (int i = 0; i < parent.getChildCount(); i++) {
95-
View itemView = parent.getChildAt(i);
96-
int position = parent.getChildAdapterPosition(itemView);
97-
if (position == RecyclerView.NO_POSITION) {
98-
continue;
99-
}
100-
101-
boolean hasStickyHeader = mHeaderPositionCalculator.hasStickyHeader(itemView, mOrientationProvider.getOrientation(parent), position);
102-
if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position)) {
103-
View header = mHeaderProvider.getHeader(parent, position);
104-
Rect headerOffset = mHeaderPositionCalculator.getHeaderBounds(parent, header,
105-
itemView, hasStickyHeader);
106-
mRenderer.drawHeader(parent, canvas, header, headerOffset);
107-
mHeaderRects.put(position, headerOffset);
108-
}
144+
/**
145+
* Gets the header view for the associated position. If it doesn't exist yet, it will be
146+
* created, measured, and laid out.
147+
*
148+
* @param parent
149+
* @param position
150+
* @return Header view
151+
*/
152+
public View getHeaderView(RecyclerView parent, int position) {
153+
return mHeaderProvider.getHeader(parent, position);
109154
}
110-
}
111-
112-
/**
113-
* Gets the position of the header under the specified (x, y) coordinates.
114-
*
115-
* @param x x-coordinate
116-
* @param y y-coordinate
117-
* @return position of header, or -1 if not found
118-
*/
119-
public int findHeaderPositionUnder(int x, int y) {
120-
for (int i = 0; i < mHeaderRects.size(); i++) {
121-
Rect rect = mHeaderRects.get(mHeaderRects.keyAt(i));
122-
if (rect.contains(x, y)) {
123-
return mHeaderRects.keyAt(i);
124-
}
155+
156+
/**
157+
* Invalidates cached headers. This does not invalidate the recyclerview, you should do that manually after
158+
* calling this method.
159+
*/
160+
public void invalidateHeaders() {
161+
mHeaderProvider.invalidate();
162+
}
163+
164+
public void setHeaderPositionListener(StickyRecyclerHeadersPositionChangeListener headerListener) {
165+
this.mHeaderListener = headerListener;
125166
}
126-
return -1;
127-
}
128-
129-
/**
130-
* Gets the header view for the associated position. If it doesn't exist yet, it will be
131-
* created, measured, and laid out.
132-
*
133-
* @param parent
134-
* @param position
135-
* @return Header view
136-
*/
137-
public View getHeaderView(RecyclerView parent, int position) {
138-
return mHeaderProvider.getHeader(parent, position);
139-
}
140-
141-
/**
142-
* Invalidates cached headers. This does not invalidate the recyclerview, you should do that manually after
143-
* calling this method.
144-
*/
145-
public void invalidateHeaders() {
146-
mHeaderProvider.invalidate();
147-
}
148167
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.timehop.stickyheadersrecyclerview;
2+
3+
import android.graphics.Rect;
4+
import android.view.View;
5+
6+
/**
7+
* Listener which get called every time headers get redrawn
8+
*/
9+
public interface StickyRecyclerHeadersPositionChangeListener {
10+
/**
11+
* <p>Called for each header get redrawn.</p>
12+
* <p>Notice coordinates may not actually change for some of the headers
13+
* it's up to the client to track actual coordinates changes</p>
14+
*
15+
* @param headerId id of the header being redrawn
16+
* @param headerRect new coordinates for the header
17+
*/
18+
19+
void onHeaderPositionChanged(int headerId, View header, int position, Rect headerRect);
20+
}

0 commit comments

Comments
 (0)