Skip to content

Commit c311b97

Browse files
committed
2 parents 1baf27f + 0ba0fa3 commit c311b97

File tree

16 files changed

+224
-122
lines changed

16 files changed

+224
-122
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Here is a quick video of it in action (click to see the full video):
1616
Download
1717
--------
1818

19-
compile 'com.github.smartcanvas:sticky-headers-recyclerview:0.5'
19+
compile 'com.github.smartcanvas:sticky-headers-recyclerview:0.5.0'
2020

2121
Usage
2222
-----
@@ -94,10 +94,14 @@ Known Issues
9494

9595
* I haven't tested this with ItemAnimators yet.
9696

97+
* The header views are drawn to a canvas, and are not actually a part of the view hierarchy. As such, they can't have touch states, and you may run into issues if you try to load images into them asynchronously.
98+
9799
Version History
98100
---------------
99101
0.5 (8/20/2015) - Added position change listener
100102

103+
0.4.2 (8/21/2015) - Add support for reverse `ReverseLayout` in `LinearLayoutManager` by [AntonPukhonin](https://github.com/AntonPukhonin)
104+
101105
0.4.1 (6/24/2015) - Fix "dancing headers" by DarkJaguar91
102106

103107
0.4.0 (4/16/2015) - Code reorganization by danoz73, fixes for different sized headers, performance improvements

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ ext {
2424
targetSdkVersion = 22
2525
minSdkVersion = 14
2626
versionCode = 12
27-
versionName = "0.5"
27+
versionName = "0.5.0"
2828
}

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
1818
# org.gradle.parallel=true
1919

20-
VERSION_NAME=0.4.1
21-
VERSION_CODE=11
20+
VERSION_NAME=0.4.2
21+
VERSION_CODE=12
2222
GROUP=com.timehop.stickyheadersrecyclerview
2323

2424
POM_DESCRIPTION=RecyclerView decorator for sticky list headers.

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

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ public class HeaderPositionCalculator {
2020
private final HeaderProvider mHeaderProvider;
2121
private final DimensionCalculator mDimensionCalculator;
2222

23+
/**
24+
* The following fields are used as buffers for internal calculations. Their sole purpose is to avoid
25+
* allocating new Rect every time we need one.
26+
*/
27+
private final Rect mTempRect1 = new Rect();
28+
private final Rect mTempRect2 = new Rect();
29+
2330
public HeaderPositionCalculator(StickyRecyclerHeadersAdapter adapter, HeaderProvider headerProvider,
2431
OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator) {
2532
mAdapter = adapter;
@@ -41,12 +48,13 @@ public HeaderPositionCalculator(StickyRecyclerHeadersAdapter adapter, HeaderProv
4148
*/
4249
public boolean hasStickyHeader(View itemView, int orientation, int position) {
4350
int offset, margin;
51+
mDimensionCalculator.initMargins(mTempRect1, itemView);
4452
if (orientation == LinearLayout.VERTICAL) {
4553
offset = itemView.getTop();
46-
margin = mDimensionCalculator.getMargins(itemView).top;
54+
margin = mTempRect1.top;
4755
} else {
4856
offset = itemView.getLeft();
49-
margin = mDimensionCalculator.getMargins(itemView).left;
57+
margin = mTempRect1.left;
5058
}
5159

5260
return offset <= margin && mAdapter.getHeaderId(position) >= 0;
@@ -56,11 +64,12 @@ public boolean hasStickyHeader(View itemView, int orientation, int position) {
5664
* Determines if an item in the list should have a header that is different than the item in the
5765
* list that immediately precedes it. Items with no headers will always return false.
5866
*
59-
* @param position of the list item in questions
67+
* @param position of the list item in questions
68+
* @param isReverseLayout TRUE if layout manager has flag isReverseLayout
6069
* @return true if this item has a different header than the previous item in the list
6170
* @see {@link StickyRecyclerHeadersAdapter#getHeaderId(int)}
6271
*/
63-
public boolean hasNewHeader(int position) {
72+
public boolean hasNewHeader(int position, boolean isReverseLayout) {
6473
if (indexOutOfBounds(position)) {
6574
return false;
6675
}
@@ -71,28 +80,20 @@ public boolean hasNewHeader(int position) {
7180
return false;
7281
}
7382

74-
return position == 0 || headerId != mAdapter.getHeaderId(position - 1);
83+
long nextItemHeaderId = -1;
84+
int nextItemPosition = position + (isReverseLayout ? 1 : -1);
85+
if (!indexOutOfBounds(nextItemPosition)) {
86+
nextItemHeaderId = mAdapter.getHeaderId(nextItemPosition);
87+
}
88+
int firstItemPosition = isReverseLayout ? mAdapter.getItemCount() - 1 : 0;
89+
90+
return position == firstItemPosition || headerId != nextItemHeaderId;
7591
}
7692

7793
private boolean indexOutOfBounds(int position) {
7894
return position < 0 || position >= mAdapter.getItemCount();
7995
}
8096

81-
public Rect getHeaderBounds(RecyclerView recyclerView, View header, View firstView, boolean firstHeader) {
82-
int orientation = mOrientationProvider.getOrientation(recyclerView);
83-
Rect bounds = getDefaultHeaderOffset(recyclerView, header, firstView, orientation);
84-
85-
if (firstHeader && isStickyHeaderBeingPushedOffscreen(recyclerView, header)) {
86-
View viewAfterNextHeader = getFirstViewUnobscuredByHeader(recyclerView, header);
87-
int firstViewUnderHeaderPosition = recyclerView.getChildAdapterPosition(viewAfterNextHeader);
88-
View secondHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition);
89-
translateHeaderWithNextHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds,
90-
header, viewAfterNextHeader, secondHeader);
91-
}
92-
93-
return bounds;
94-
}
95-
9697
/**
9798
* Verify if header obscure some item on RecyclerView
9899
*
@@ -109,22 +110,35 @@ public boolean headerObscuringSomeItem(RecyclerView parent, View firstHeader) {
109110
return false;
110111
}
111112

112-
private Rect getDefaultHeaderOffset(RecyclerView recyclerView, View header, View firstView, int orientation) {
113+
public void initHeaderBounds(Rect bounds, RecyclerView recyclerView, View header, View firstView, boolean firstHeader) {
114+
int orientation = mOrientationProvider.getOrientation(recyclerView);
115+
initDefaultHeaderOffset(bounds, recyclerView, header, firstView, orientation);
116+
117+
if (firstHeader && isStickyHeaderBeingPushedOffscreen(recyclerView, header)) {
118+
View viewAfterNextHeader = getFirstViewUnobscuredByHeader(recyclerView, header);
119+
int firstViewUnderHeaderPosition = recyclerView.getChildAdapterPosition(viewAfterNextHeader);
120+
View secondHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition);
121+
translateHeaderWithNextHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds,
122+
header, viewAfterNextHeader, secondHeader);
123+
}
124+
}
125+
126+
private void initDefaultHeaderOffset(Rect headerMargins, RecyclerView recyclerView, View header, View firstView, int orientation) {
113127
int translationX, translationY;
114-
Rect headerMargins = mDimensionCalculator.getMargins(header);
128+
mDimensionCalculator.initMargins(mTempRect1, header);
115129
if (orientation == LinearLayoutManager.VERTICAL) {
116-
translationX = firstView.getLeft() + headerMargins.left;
130+
translationX = firstView.getLeft() + mTempRect1.left;
117131
translationY = Math.max(
118-
firstView.getTop() - header.getHeight() - headerMargins.bottom,
119-
getListTop(recyclerView) + headerMargins.top);
132+
firstView.getTop() - header.getHeight() - mTempRect1.bottom,
133+
getListTop(recyclerView) + mTempRect1.top);
120134
} else {
121-
translationY = firstView.getTop() + headerMargins.top;
135+
translationY = firstView.getTop() + mTempRect1.top;
122136
translationX = Math.max(
123-
firstView.getLeft() - header.getWidth() - headerMargins.right,
124-
getListLeft(recyclerView) + headerMargins.left);
137+
firstView.getLeft() - header.getWidth() - mTempRect1.right,
138+
getListLeft(recyclerView) + mTempRect1.left);
125139
}
126140

127-
return new Rect(translationX, translationY, translationX + header.getWidth(),
141+
headerMargins.set(translationX, translationY, translationX + header.getWidth(),
128142
translationY + header.getHeight());
129143
}
130144

@@ -135,20 +149,21 @@ private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, Vi
135149
return false;
136150
}
137151

138-
if (firstViewUnderHeaderPosition > 0 && hasNewHeader(firstViewUnderHeaderPosition)) {
152+
boolean isReverseLayout = mOrientationProvider.isReverseLayout(recyclerView);
153+
if (firstViewUnderHeaderPosition > 0 && hasNewHeader(firstViewUnderHeaderPosition, isReverseLayout)) {
139154
View nextHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition);
140-
Rect nextHeaderMargins = mDimensionCalculator.getMargins(nextHeader);
141-
Rect headerMargins = mDimensionCalculator.getMargins(stickyHeader);
155+
mDimensionCalculator.initMargins(mTempRect1, nextHeader);
156+
mDimensionCalculator.initMargins(mTempRect2, stickyHeader);
142157

143158
if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) {
144-
int topOfNextHeader = viewAfterHeader.getTop() - nextHeaderMargins.bottom - nextHeader.getHeight() - nextHeaderMargins.top;
145-
int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + headerMargins.top + headerMargins.bottom;
159+
int topOfNextHeader = viewAfterHeader.getTop() - mTempRect1.bottom - nextHeader.getHeight() - mTempRect1.top;
160+
int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + mTempRect2.top + mTempRect2.bottom;
146161
if (topOfNextHeader < bottomOfThisHeader) {
147162
return true;
148163
}
149164
} else {
150-
int leftOfNextHeader = viewAfterHeader.getLeft() - nextHeaderMargins.right - nextHeader.getWidth() - nextHeaderMargins.left;
151-
int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + headerMargins.left + headerMargins.right;
165+
int leftOfNextHeader = viewAfterHeader.getLeft() - mTempRect1.right - nextHeader.getWidth() - mTempRect1.left;
166+
int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + mTempRect2.left + mTempRect2.right;
152167
if (leftOfNextHeader < rightOfThisHeader) {
153168
return true;
154169
}
@@ -160,17 +175,17 @@ private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, Vi
160175

161176
private void translateHeaderWithNextHeader(RecyclerView recyclerView, int orientation, Rect translation,
162177
View currentHeader, View viewAfterNextHeader, View nextHeader) {
163-
Rect nextHeaderMargins = mDimensionCalculator.getMargins(nextHeader);
164-
Rect stickyHeaderMargins = mDimensionCalculator.getMargins(currentHeader);
178+
mDimensionCalculator.initMargins(mTempRect1, nextHeader);
179+
mDimensionCalculator.initMargins(mTempRect2, currentHeader);
165180
if (orientation == LinearLayoutManager.VERTICAL) {
166-
int topOfStickyHeader = getListTop(recyclerView) + stickyHeaderMargins.top + stickyHeaderMargins.bottom;
167-
int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - nextHeaderMargins.bottom - nextHeaderMargins.top - currentHeader.getHeight() - topOfStickyHeader;
181+
int topOfStickyHeader = getListTop(recyclerView) + mTempRect2.top + mTempRect2.bottom;
182+
int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - mTempRect1.bottom - mTempRect1.top - currentHeader.getHeight() - topOfStickyHeader;
168183
if (shiftFromNextHeader < topOfStickyHeader) {
169184
translation.top += shiftFromNextHeader;
170185
}
171186
} else {
172-
int leftOfStickyHeader = getListLeft(recyclerView) + stickyHeaderMargins.left + stickyHeaderMargins.right;
173-
int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - nextHeaderMargins.right - nextHeaderMargins.left - currentHeader.getWidth() - leftOfStickyHeader;
187+
int leftOfStickyHeader = getListLeft(recyclerView) + mTempRect2.left + mTempRect2.right;
188+
int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - mTempRect1.right - mTempRect1.left - currentHeader.getWidth() - leftOfStickyHeader;
174189
if (shiftFromNextHeader < leftOfStickyHeader) {
175190
translation.left += shiftFromNextHeader;
176191
}
@@ -180,11 +195,14 @@ private void translateHeaderWithNextHeader(RecyclerView recyclerView, int orient
180195
/**
181196
* Returns the first item currently in the RecyclerView that is not obscured by a header.
182197
*
183-
* @param parent RecyclerView containing all the list items
198+
* @param parent Recyclerview containing all the list items
184199
* @return first item that is fully beneath a header
185200
*/
186201
private View getFirstViewUnobscuredByHeader(RecyclerView parent, View firstHeader) {
187-
for (int i = 0; i < parent.getChildCount(); i++) {
202+
boolean isReverseLayout = mOrientationProvider.isReverseLayout(parent);
203+
int step = isReverseLayout ? -1 : 1;
204+
int from = isReverseLayout ? parent.getChildCount() - 1 : 0;
205+
for (int i = from; i >= 0 && i <= parent.getChildCount() - 1; i += step) {
188206
View child = parent.getChildAt(i);
189207
if (!itemIsObscuredByHeader(parent, child, firstHeader, mOrientationProvider.getOrientation(parent))) {
190208
return child;
@@ -204,7 +222,7 @@ private View getFirstViewUnobscuredByHeader(RecyclerView parent, View firstHeade
204222
*/
205223
private boolean itemIsObscuredByHeader(RecyclerView parent, View item, View header, int orientation) {
206224
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) item.getLayoutParams();
207-
Rect headerMargins = mDimensionCalculator.getMargins(header);
225+
mDimensionCalculator.initMargins(mTempRect1, header);
208226

209227
int adapterPosition = parent.getChildAdapterPosition(item);
210228
if (adapterPosition == RecyclerView.NO_POSITION || mHeaderProvider.getHeader(parent, adapterPosition) != header) {
@@ -215,13 +233,13 @@ private boolean itemIsObscuredByHeader(RecyclerView parent, View item, View head
215233

216234
if (orientation == LinearLayoutManager.VERTICAL) {
217235
int itemTop = item.getTop() - layoutParams.topMargin;
218-
int headerBottom = header.getBottom() + headerMargins.bottom + headerMargins.top;
236+
int headerBottom = header.getBottom() + mTempRect1.bottom + mTempRect1.top;
219237
if (itemTop >= headerBottom) {
220238
return false;
221239
}
222240
} else {
223241
int itemLeft = item.getLeft() - layoutParams.leftMargin;
224-
int headerRight = header.getRight() + headerMargins.right + headerMargins.left;
242+
int headerRight = header.getRight() + mTempRect1.right + mTempRect1.left;
225243
if (itemLeft >= headerRight) {
226244
return false;
227245
}

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public class StickyRecyclerHeadersDecoration extends RecyclerView.ItemDecoration
2323
private final HeaderPositionCalculator mHeaderPositionCalculator;
2424
private final HeaderRenderer mRenderer;
2525
private final DimensionCalculator mDimensionCalculator;
26+
/**
27+
* The following field is used as a buffer for internal calculations. Its sole purpose is to avoid
28+
* allocating new Rect every time we need one.
29+
*/
30+
private final Rect mTempRect = new Rect();
2631
private StickyRecyclerHeadersPositionChangeListener mHeaderListener;
2732

2833
// TODO: Consider passing in orientation to simplify orientation accounting within calculation
@@ -61,7 +66,7 @@ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, Recycle
6166
if (itemPosition == RecyclerView.NO_POSITION) {
6267
return;
6368
}
64-
if (mHeaderPositionCalculator.hasNewHeader(itemPosition)) {
69+
if (mHeaderPositionCalculator.hasNewHeader(itemPosition, mOrientationProvider.isReverseLayout(parent))) {
6570
View header = getHeaderView(parent, itemPosition);
6671
setItemOffsetsForHeader(outRect, header, mOrientationProvider.getOrientation(parent));
6772
}
@@ -75,37 +80,41 @@ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, Recycle
7580
* @param orientation used to calculate offset for the item
7681
*/
7782
private void setItemOffsetsForHeader(Rect itemOffsets, View header, int orientation) {
78-
Rect headerMargins = mDimensionCalculator.getMargins(header);
83+
mDimensionCalculator.initMargins(mTempRect, header);
7984
if (orientation == LinearLayoutManager.VERTICAL) {
80-
itemOffsets.top = header.getHeight() + headerMargins.top + headerMargins.bottom;
85+
itemOffsets.top = header.getHeight() + mTempRect.top + mTempRect.bottom;
8186
} else {
82-
itemOffsets.left = header.getWidth() + headerMargins.left + headerMargins.right;
87+
itemOffsets.left = header.getWidth() + mTempRect.left + mTempRect.right;
8388
}
8489
}
8590

8691
@Override
8792
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
8893
super.onDrawOver(canvas, parent, state);
89-
mHeaderRects.clear();
9094

91-
if (parent.getChildCount() <= 0 || mAdapter.getItemCount() <= 0) {
95+
final int childCount = parent.getChildCount();
96+
if (childCount <= 0 || mAdapter.getItemCount() <= 0) {
9297
return;
9398
}
9499

95-
for (int i = 0; i < parent.getChildCount(); i++) {
100+
for (int i = 0; i < childCount; i++) {
96101
View itemView = parent.getChildAt(i);
97102
int position = parent.getChildAdapterPosition(itemView);
98103
if (position == RecyclerView.NO_POSITION) {
99104
continue;
100105
}
101106

102107
boolean hasStickyHeader = mHeaderPositionCalculator.hasStickyHeader(itemView, mOrientationProvider.getOrientation(parent), position);
103-
if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position)) {
108+
if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position, mOrientationProvider.isReverseLayout(parent))) {
104109
View header = mHeaderProvider.getHeader(parent, position);
105-
Rect headerOffset = mHeaderPositionCalculator.getHeaderBounds(parent, header,
106-
itemView, hasStickyHeader);
110+
//re-use existing Rect, if any.
111+
Rect headerOffset = mHeaderRects.get(position);
112+
if (headerOffset == null) {
113+
headerOffset = new Rect();
114+
mHeaderRects.put(position, headerOffset);
115+
}
116+
mHeaderPositionCalculator.initHeaderBounds(headerOffset, parent, header, itemView, hasStickyHeader);
107117
mRenderer.drawHeader(parent, canvas, header, headerOffset);
108-
mHeaderRects.put(position, headerOffset);
109118

110119
if (mHeaderListener != null) {
111120
mHeaderListener.onHeaderPositionChanged(mAdapter.getHeaderId(position), header, position, headerOffset);

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,18 @@ public void setOnHeaderClickListener(OnHeaderClickListener listener) {
3535

3636
@Override
3737
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
38-
return mOnHeaderClickListener != null && mTapDetector.onTouchEvent(e);
38+
if (this.mOnHeaderClickListener != null) {
39+
boolean tapDetectorResponse = this.mTapDetector.onTouchEvent(e);
40+
if (tapDetectorResponse) {
41+
// Don't return false if a single tap is detected
42+
return true;
43+
}
44+
if (e.getAction() == MotionEvent.ACTION_DOWN) {
45+
int position = mDecor.findHeaderPositionUnder((int) e.getX(), (int) e.getY());
46+
return position != -1;
47+
}
48+
}
49+
return false;
3950
}
4051

4152
@Override

0 commit comments

Comments
 (0)