Skip to content

Commit 126d271

Browse files
committed
Merge pull request timehop#72 from denisk20/perf-improvements
timehop#19: reducing the number of Rect allocations during list scrolling.
2 parents 8da3abd + 9a3d8d1 commit 126d271

File tree

4 files changed

+85
-61
lines changed

4 files changed

+85
-61
lines changed

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

Lines changed: 38 additions & 32 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;
@@ -86,9 +94,9 @@ private boolean indexOutOfBounds(int position) {
8694
return position < 0 || position >= mAdapter.getItemCount();
8795
}
8896

89-
public Rect getHeaderBounds(RecyclerView recyclerView, View header, View firstView, boolean firstHeader) {
97+
public void initHeaderBounds(Rect bounds, RecyclerView recyclerView, View header, View firstView, boolean firstHeader) {
9098
int orientation = mOrientationProvider.getOrientation(recyclerView);
91-
Rect bounds = getDefaultHeaderOffset(recyclerView, header, firstView, orientation);
99+
initDefaultHeaderOffset(bounds, recyclerView, header, firstView, orientation);
92100

93101
if (firstHeader && isStickyHeaderBeingPushedOffscreen(recyclerView, header)) {
94102
View viewAfterNextHeader = getFirstViewUnobscuredByHeader(recyclerView, header);
@@ -97,27 +105,25 @@ public Rect getHeaderBounds(RecyclerView recyclerView, View header, View firstVi
97105
translateHeaderWithNextHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds,
98106
header, viewAfterNextHeader, secondHeader);
99107
}
100-
101-
return bounds;
102108
}
103109

104-
private Rect getDefaultHeaderOffset(RecyclerView recyclerView, View header, View firstView, int orientation) {
110+
private void initDefaultHeaderOffset(Rect headerMargins, RecyclerView recyclerView, View header, View firstView, int orientation) {
105111
int translationX, translationY;
106-
Rect headerMargins = mDimensionCalculator.getMargins(header);
112+
mDimensionCalculator.initMargins(mTempRect1, header);
107113
if (orientation == LinearLayoutManager.VERTICAL) {
108-
translationX = firstView.getLeft() + headerMargins.left;
114+
translationX = firstView.getLeft() + mTempRect1.left;
109115
translationY = Math.max(
110-
firstView.getTop() - header.getHeight() - headerMargins.bottom,
111-
getListTop(recyclerView) + headerMargins.top);
116+
firstView.getTop() - header.getHeight() - mTempRect1.bottom,
117+
getListTop(recyclerView) + mTempRect1.top);
112118
} else {
113-
translationY = firstView.getTop() + headerMargins.top;
119+
translationY = firstView.getTop() + mTempRect1.top;
114120
translationX = Math.max(
115-
firstView.getLeft() - header.getWidth() - headerMargins.right,
116-
getListLeft(recyclerView) + headerMargins.left);
121+
firstView.getLeft() - header.getWidth() - mTempRect1.right,
122+
getListLeft(recyclerView) + mTempRect1.left);
117123
}
118124

119-
return new Rect(translationX, translationY, translationX + header.getWidth(),
120-
translationY + header.getHeight());
125+
headerMargins.set(translationX, translationY, translationX + header.getWidth(),
126+
translationY + header.getHeight());
121127
}
122128

123129
private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, View stickyHeader) {
@@ -130,18 +136,18 @@ private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, Vi
130136
boolean isReverseLayout = mOrientationProvider.isReverseLayout(recyclerView);
131137
if (firstViewUnderHeaderPosition > 0 && hasNewHeader(firstViewUnderHeaderPosition, isReverseLayout)) {
132138
View nextHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition);
133-
Rect nextHeaderMargins = mDimensionCalculator.getMargins(nextHeader);
134-
Rect headerMargins = mDimensionCalculator.getMargins(stickyHeader);
139+
mDimensionCalculator.initMargins(mTempRect1, nextHeader);
140+
mDimensionCalculator.initMargins(mTempRect2, stickyHeader);
135141

136142
if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) {
137-
int topOfNextHeader = viewAfterHeader.getTop() - nextHeaderMargins.bottom - nextHeader.getHeight() - nextHeaderMargins.top;
138-
int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + headerMargins.top + headerMargins.bottom;
143+
int topOfNextHeader = viewAfterHeader.getTop() - mTempRect1.bottom - nextHeader.getHeight() - mTempRect1.top;
144+
int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + mTempRect2.top + mTempRect2.bottom;
139145
if (topOfNextHeader < bottomOfThisHeader) {
140146
return true;
141147
}
142148
} else {
143-
int leftOfNextHeader = viewAfterHeader.getLeft() - nextHeaderMargins.right - nextHeader.getWidth() - nextHeaderMargins.left;
144-
int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + headerMargins.left + headerMargins.right;
149+
int leftOfNextHeader = viewAfterHeader.getLeft() - mTempRect1.right - nextHeader.getWidth() - mTempRect1.left;
150+
int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + mTempRect2.left + mTempRect2.right;
145151
if (leftOfNextHeader < rightOfThisHeader) {
146152
return true;
147153
}
@@ -152,18 +158,18 @@ private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, Vi
152158
}
153159

154160
private void translateHeaderWithNextHeader(RecyclerView recyclerView, int orientation, Rect translation,
155-
View currentHeader, View viewAfterNextHeader, View nextHeader) {
156-
Rect nextHeaderMargins = mDimensionCalculator.getMargins(nextHeader);
157-
Rect stickyHeaderMargins = mDimensionCalculator.getMargins(currentHeader);
161+
View currentHeader, View viewAfterNextHeader, View nextHeader) {
162+
mDimensionCalculator.initMargins(mTempRect1, nextHeader);
163+
mDimensionCalculator.initMargins(mTempRect2, currentHeader);
158164
if (orientation == LinearLayoutManager.VERTICAL) {
159-
int topOfStickyHeader = getListTop(recyclerView) + stickyHeaderMargins.top + stickyHeaderMargins.bottom;
160-
int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - nextHeaderMargins.bottom - nextHeaderMargins.top - currentHeader.getHeight() - topOfStickyHeader;
165+
int topOfStickyHeader = getListTop(recyclerView) + mTempRect2.top + mTempRect2.bottom;
166+
int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - mTempRect1.bottom - mTempRect1.top - currentHeader.getHeight() - topOfStickyHeader;
161167
if (shiftFromNextHeader < topOfStickyHeader) {
162168
translation.top += shiftFromNextHeader;
163169
}
164170
} else {
165-
int leftOfStickyHeader = getListLeft(recyclerView) + stickyHeaderMargins.left + stickyHeaderMargins.right;
166-
int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - nextHeaderMargins.right - nextHeaderMargins.left - currentHeader.getWidth() - leftOfStickyHeader;
171+
int leftOfStickyHeader = getListLeft(recyclerView) + mTempRect2.left + mTempRect2.right;
172+
int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - mTempRect1.right - mTempRect1.left - currentHeader.getWidth() - leftOfStickyHeader;
167173
if (shiftFromNextHeader < leftOfStickyHeader) {
168174
translation.left += shiftFromNextHeader;
169175
}
@@ -201,7 +207,7 @@ private View getFirstViewUnobscuredByHeader(RecyclerView parent, View firstHeade
201207
*/
202208
private boolean itemIsObscuredByHeader(RecyclerView parent, View item, View header, int orientation) {
203209
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) item.getLayoutParams();
204-
Rect headerMargins = mDimensionCalculator.getMargins(header);
210+
mDimensionCalculator.initMargins(mTempRect1, header);
205211

206212
int adapterPosition = parent.getChildAdapterPosition(item);
207213
if (adapterPosition == RecyclerView.NO_POSITION || mHeaderProvider.getHeader(parent, adapterPosition) != header) {
@@ -212,13 +218,13 @@ private boolean itemIsObscuredByHeader(RecyclerView parent, View item, View head
212218

213219
if (orientation == LinearLayoutManager.VERTICAL) {
214220
int itemTop = item.getTop() - layoutParams.topMargin;
215-
int headerBottom = header.getBottom() + headerMargins.bottom + headerMargins.top;
221+
int headerBottom = header.getBottom() + mTempRect1.bottom + mTempRect1.top;
216222
if (itemTop > headerBottom) {
217223
return false;
218224
}
219225
} else {
220226
int itemLeft = item.getLeft() - layoutParams.leftMargin;
221-
int headerRight = header.getRight() + headerMargins.right + headerMargins.left;
227+
int headerRight = header.getRight() + mTempRect1.right + mTempRect1.left;
222228
if (itemLeft > headerRight) {
223229
return false;
224230
}

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public class StickyRecyclerHeadersDecoration extends RecyclerView.ItemDecoration
2424
private final HeaderRenderer mRenderer;
2525
private final DimensionCalculator mDimensionCalculator;
2626

27+
/**
28+
* The following field is used as a buffer for internal calculations. Its sole purpose is to avoid
29+
* allocating new Rect every time we need one.
30+
*/
31+
private final Rect mTempRect = new Rect();
32+
2733
// TODO: Consider passing in orientation to simplify orientation accounting within calculation
2834
public StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter) {
2935
this(adapter, new LinearLayoutOrientationProvider(), new DimensionCalculator());
@@ -74,24 +80,24 @@ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, Recycle
7480
* @param orientation used to calculate offset for the item
7581
*/
7682
private void setItemOffsetsForHeader(Rect itemOffsets, View header, int orientation) {
77-
Rect headerMargins = mDimensionCalculator.getMargins(header);
83+
mDimensionCalculator.initMargins(mTempRect, header);
7884
if (orientation == LinearLayoutManager.VERTICAL) {
79-
itemOffsets.top = header.getHeight() + headerMargins.top + headerMargins.bottom;
85+
itemOffsets.top = header.getHeight() + mTempRect.top + mTempRect.bottom;
8086
} else {
81-
itemOffsets.left = header.getWidth() + headerMargins.left + headerMargins.right;
87+
itemOffsets.left = header.getWidth() + mTempRect.left + mTempRect.right;
8288
}
8389
}
8490

8591
@Override
8692
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
8793
super.onDrawOver(canvas, parent, state);
88-
mHeaderRects.clear();
8994

90-
if (parent.getChildCount() <= 0 || mAdapter.getItemCount() <= 0) {
95+
final int childCount = parent.getChildCount();
96+
if (childCount <= 0 || mAdapter.getItemCount() <= 0) {
9197
return;
9298
}
9399

94-
for (int i = 0; i < parent.getChildCount(); i++) {
100+
for (int i = 0; i < childCount; i++) {
95101
View itemView = parent.getChildAt(i);
96102
int position = parent.getChildAdapterPosition(itemView);
97103
if (position == RecyclerView.NO_POSITION) {
@@ -101,9 +107,14 @@ public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State st
101107
boolean hasStickyHeader = mHeaderPositionCalculator.hasStickyHeader(itemView, mOrientationProvider.getOrientation(parent), position);
102108
if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position, mOrientationProvider.isReverseLayout(parent))) {
103109
View header = mHeaderProvider.getHeader(parent, position);
104-
Rect headerOffset = mHeaderPositionCalculator.getHeaderBounds(parent, header, 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);
105117
mRenderer.drawHeader(parent, canvas, header, headerOffset);
106-
mHeaderRects.put(position, headerOffset);
107118
}
108119
}
109120
}

library/src/main/java/com/timehop/stickyheadersrecyclerview/calculation/DimensionCalculator.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,32 @@
1212
public class DimensionCalculator {
1313

1414
/**
15-
* Returns {@link Rect} representing margins for any view.
15+
* Populates {@link Rect} with margins for any view.
1616
*
17+
*
18+
* @param margins rect to populate
1719
* @param view for which to get margins
18-
* @return margins for the given view. All 0 if the view does not support margins
1920
*/
20-
public Rect getMargins(View view) {
21+
public void initMargins(Rect margins, View view) {
2122
LayoutParams layoutParams = view.getLayoutParams();
2223

2324
if (layoutParams instanceof MarginLayoutParams) {
2425
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
25-
return getMarginRect(marginLayoutParams);
26+
initMarginRect(margins, marginLayoutParams);
2627
} else {
27-
return new Rect();
28+
margins.set(0, 0, 0, 0);
2829
}
2930
}
3031

3132
/**
32-
* Converts {@link MarginLayoutParams} into a representative {@link Rect}
33+
* Converts {@link MarginLayoutParams} into a representative {@link Rect}.
3334
*
34-
* @param marginLayoutParams margins to convert to a Rect
35-
* @return Rect representing margins, where {@link MarginLayoutParams#leftMargin} is equivalent to
36-
* {@link Rect#left}, etc.
35+
* @param marginRect Rect to be initialized with margins coordinates, where
36+
* {@link MarginLayoutParams#leftMargin} is equivalent to {@link Rect#left}, etc.
37+
* @param marginLayoutParams margins to populate the Rect with
3738
*/
38-
private Rect getMarginRect(MarginLayoutParams marginLayoutParams) {
39-
return new Rect(
39+
private void initMarginRect(Rect marginRect, MarginLayoutParams marginLayoutParams) {
40+
marginRect.set(
4041
marginLayoutParams.leftMargin,
4142
marginLayoutParams.topMargin,
4243
marginLayoutParams.rightMargin,

library/src/main/java/com/timehop/stickyheadersrecyclerview/rendering/HeaderRenderer.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public class HeaderRenderer {
1717
private final DimensionCalculator mDimensionCalculator;
1818
private final OrientationProvider mOrientationProvider;
1919

20+
/**
21+
* The following field is used as a buffer for internal calculations. Its sole purpose is to avoid
22+
* allocating new Rect every time we need one.
23+
*/
24+
private final Rect mTempRect = new Rect();
25+
2026
public HeaderRenderer(OrientationProvider orientationProvider) {
2127
this(orientationProvider, new DimensionCalculator());
2228
}
@@ -41,8 +47,8 @@ public void drawHeader(RecyclerView recyclerView, Canvas canvas, View header, Re
4147

4248
if (recyclerView.getLayoutManager().getClipToPadding()) {
4349
// Clip drawing of headers to the padding of the RecyclerView. Avoids drawing in the padding
44-
Rect clipRect = getClipRectForHeader(recyclerView, header);
45-
canvas.clipRect(clipRect);
50+
initClipRectForHeader(mTempRect, recyclerView, header);
51+
canvas.clipRect(mTempRect);
4652
}
4753

4854
canvas.translate(offset.left, offset.top);
@@ -52,30 +58,30 @@ public void drawHeader(RecyclerView recyclerView, Canvas canvas, View header, Re
5258
}
5359

5460
/**
55-
* Gets a clipping rect for the header based on the margins of the header and the padding of the
61+
* Initializes a clipping rect for the header based on the margins of the header and the padding of the
5662
* recycler.
5763
* FIXME: Currently right margin in VERTICAL orientation and bottom margin in HORIZONTAL
5864
* orientation are clipped so they look accurate, but the headers are not being drawn at the
5965
* correctly smaller width and height respectively.
6066
*
67+
* @param clipRect {@link Rect} for clipping a provided header to the padding of a recycler view
6168
* @param recyclerView for which to provide a header
6269
* @param header for clipping
63-
* @return a {@link Rect} for clipping a provided header to the padding of a recycler view
6470
*/
65-
private Rect getClipRectForHeader(RecyclerView recyclerView, View header) {
66-
Rect headerMargins = mDimensionCalculator.getMargins(header);
71+
private void initClipRectForHeader(Rect clipRect, RecyclerView recyclerView, View header) {
72+
mDimensionCalculator.initMargins(clipRect, header);
6773
if (mOrientationProvider.getOrientation(recyclerView) == LinearLayout.VERTICAL) {
68-
return new Rect(
74+
clipRect.set(
6975
recyclerView.getPaddingLeft(),
7076
recyclerView.getPaddingTop(),
71-
recyclerView.getWidth() - recyclerView.getPaddingRight() - headerMargins.right,
77+
recyclerView.getWidth() - recyclerView.getPaddingRight() - clipRect.right,
7278
recyclerView.getHeight() - recyclerView.getPaddingBottom());
7379
} else {
74-
return new Rect(
80+
clipRect.set(
7581
recyclerView.getPaddingLeft(),
7682
recyclerView.getPaddingTop(),
7783
recyclerView.getWidth() - recyclerView.getPaddingRight(),
78-
recyclerView.getHeight() - recyclerView.getPaddingBottom() - headerMargins.bottom);
84+
recyclerView.getHeight() - recyclerView.getPaddingBottom() - clipRect.bottom);
7985
}
8086
}
8187

0 commit comments

Comments
 (0)