rect
specifies
103 | * the number of pixels that the item view should be inset by, similar to padding or margin.
104 | * The default implementation sets the bounds of outRect to 0 and returns.
105 | *
106 | *
107 | * If this Decor does not affect the positioning of item views, it should set
108 | * all four fields of rect
(left, top, right, bottom) to zero
109 | * before returning.
110 | *
111 | * @param parent RecyclerView this ItemDecoration is decorating
112 | * @param rect Rect to receive the output.
113 | * @param view The child view to decorate
114 | * @param position The child view adapter position
115 | */
116 | void getConditionItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position);
117 |
118 |
119 | /**
120 | * Retrieve any offsets for the given item. Each field of outRect
specifies
121 | * the number of pixels that the item view should be inset by, similar to padding or margin.
122 | * The default implementation sets the bounds of outRect to 0 and returns.
123 | *
124 | *
125 | * If this Decor does not affect the positioning of item views, it should set
126 | * all four fields of rect
(left, top, right, bottom) to zero
127 | * before returning.
128 | *
129 | * @param parent RecyclerView this ItemDecoration is decorating
130 | * @param rect Rect to receive the output.
131 | * @param view The child view to decorate
132 | * @param position The child view adapter position
133 | * @param state The current state of RecyclerView.
134 | */
135 | void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state);
136 | }
137 |
138 | public static abstract class SimpleDecor implements Decor {
139 |
140 | @Override
141 | public void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
142 |
143 | }
144 |
145 | @Override
146 | public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull View child, int position, @NonNull RecyclerView.State state) {
147 |
148 | }
149 |
150 | @Override
151 | public void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
152 |
153 | }
154 |
155 | @Override
156 | public void getConditionItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position) {
157 |
158 | }
159 |
160 | @Override
161 | public void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state) {
162 |
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/SimpleTextDrawable.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Color;
5 | import android.graphics.ColorFilter;
6 | import android.graphics.Paint;
7 | import android.graphics.PixelFormat;
8 | import android.graphics.Rect;
9 | import android.graphics.RectF;
10 | import android.graphics.Typeface;
11 | import android.graphics.drawable.Drawable;
12 |
13 | import androidx.annotation.NonNull;
14 | import androidx.annotation.Nullable;
15 |
16 | public class SimpleTextDrawable extends Drawable {
17 |
18 | private final int paddingLeft;
19 | private final int paddingTop;
20 | private final int paddingRight;
21 | private final int paddingBottom;
22 | private final int bgRadius;
23 | private final Paint paintText = new Paint(Paint.ANTI_ALIAS_FLAG);
24 | private final Paint paintBack = new Paint(Paint.ANTI_ALIAS_FLAG);
25 |
26 | private final Rect textBounds = new Rect();
27 | private final RectF bgBounds = new RectF();
28 | private String textForDraw;
29 | private int textHeight;
30 | private int textWidth;
31 | private float textX = 0;
32 | private float textY = 0;
33 | private SimpleGravity textGravity;
34 |
35 | protected SimpleTextDrawable(float textSize,
36 | int colorText,
37 | SimpleGravity gravity,
38 | int colorBackground,
39 | int bgRadius,
40 | int paddingLeft,
41 | int paddingTop,
42 | int paddingRight,
43 | int paddingBottom,
44 | Typeface typeface) {
45 | this.paddingLeft = paddingLeft;
46 | this.paddingTop = paddingTop;
47 | this.paddingRight = paddingRight;
48 | this.paddingBottom = paddingBottom;
49 | this.bgRadius = bgRadius;
50 |
51 | textGravity = gravity;
52 | paintText.setColor(colorText);
53 | if (typeface != null) {
54 | paintText.setTypeface(typeface);
55 | }
56 | paintText.setTextSize(textSize);
57 |
58 | paintBack.setColor(colorBackground);
59 | }
60 |
61 | @Override
62 | public void setAlpha(int alpha) {
63 | paintText.setAlpha(alpha);
64 | paintBack.setAlpha(alpha);
65 | }
66 |
67 | @Override
68 | public void setColorFilter(@Nullable ColorFilter colorFilter) {
69 | paintText.setColorFilter(colorFilter);
70 | paintBack.setColorFilter(colorFilter);
71 | }
72 |
73 | @Override
74 | public int getOpacity() {
75 | return PixelFormat.TRANSLUCENT;
76 | }
77 |
78 |
79 | @Override
80 | public void draw(@NonNull Canvas canvas) {
81 | if (paintBack.getColor() != Color.TRANSPARENT || paintBack.getAlpha() > 0) {
82 | canvas.drawRoundRect(bgBounds, bgRadius, bgRadius, paintBack);
83 | }
84 |
85 | canvas.drawText(textForDraw, 0, textForDraw.length(), textX, textY, paintText);
86 | }
87 |
88 | /**
89 | * Update text for this drawable and recalculate size
90 | *
91 | * @param text - New text value
92 | */
93 | public void setText(@NonNull String text) {
94 | if (!text.equals(this.textForDraw)) {
95 | this.textForDraw = text;
96 | paintText.getTextBounds(text, 0, text.length(), textBounds);
97 | textHeight = textBounds.height();
98 | textWidth = textBounds.width();
99 | }
100 | }
101 |
102 | /**
103 | * Return current text
104 | *
105 | * @return current text
106 | */
107 | public String getText() {
108 | return textForDraw;
109 | }
110 |
111 | /**
112 | * Set top left position for drawable.
113 | *
114 | * This method calculate bounds based on text size and text padding 115 | *
116 | * DON'T use this method if your section or header has full width 117 | * 118 | * @param x X coordinate of top left corner 119 | * @param y Y coordinate of top left corner 120 | */ 121 | public void setTopLeft(float x, float y) { 122 | setBounds( 123 | (int) x, 124 | (int) y, 125 | (int) (x + textWidth + paddingLeft + paddingRight), 126 | (int) (y + textHeight + paddingTop + paddingBottom) 127 | ); 128 | } 129 | 130 | /** 131 | * Set top left position for drawable. 132 | *
133 | * This method calculate bounds based on text size and text padding 134 | *
135 | * DON'T use this method if your section or header has full width 136 | * 137 | * @param x X coordinate of top left corner 138 | * @param y Y coordinate of top left corner 139 | */ 140 | public void setTopLeft(int x, int y) { 141 | setTopLeft((float) x, (float) y); 142 | } 143 | 144 | /** 145 | * Set top center position for drawable 146 | *
147 | * This method calculate bounds based on text size and text padding 148 | *
149 | * DON'T use this method if your section or header has full width 150 | * 151 | * @param x X coordinate of top center 152 | * @param y Y coordinate of top center 153 | */ 154 | public void setTopCenter(float x, float y) { 155 | setBounds( 156 | (int) (x - (textWidth / 2) - paddingLeft), 157 | (int) y, 158 | (int) (x + (textWidth / 2) + paddingRight), 159 | (int) (y + textHeight + paddingTop + paddingBottom) 160 | ); 161 | } 162 | 163 | /** 164 | * Set top center position for drawable 165 | *
166 | * This method calculate bounds based on text size and text padding 167 | *
168 | * DON'T use this method if your section or header has full width 169 | * 170 | * @param x X coordinate of top center 171 | * @param y Y coordinate of top center 172 | */ 173 | public void setTopCenter(int x, int y) { 174 | setTopCenter((float) x, (float) y); 175 | } 176 | 177 | @Override 178 | public void setBounds(int left, int top, int right, int bottom) { 179 | super.setBounds(left, top, right, bottom); 180 | Rect bounds = getBounds(); 181 | bgBounds.set(bounds); 182 | textX = calculateTextX(bounds); 183 | textY = bounds.exactCenterY() - (paintText.descent() + paintText.ascent()) / 2f; 184 | } 185 | 186 | private float calculateTextX(Rect bounds) { 187 | switch (textGravity) { 188 | case LEFT: 189 | return bounds.left + paddingLeft; 190 | case RIGHT: 191 | return bounds.right - paddingRight - textWidth; 192 | default: 193 | return bounds.exactCenterX() - textWidth / 2f; 194 | } 195 | } 196 | 197 | /** 198 | * Text width with padding 199 | * 200 | * @return text width with padding 201 | */ 202 | public int getTextWidth() { 203 | return textWidth + paddingLeft + paddingRight; 204 | } 205 | 206 | /** 207 | * This is drawable bounds width 208 | *
209 | * {@link #getWidth()} >= {@link #getTextWidth()} 210 | * 211 | * @return drawable bounds width 212 | */ 213 | public int getWidth() { 214 | return getBounds().width(); 215 | } 216 | 217 | /** 218 | * Text height with padding 219 | * 220 | * @return text height with padding 221 | */ 222 | public int getHeight() { 223 | return textHeight + paddingTop + paddingBottom; 224 | } 225 | 226 | public enum SimpleGravity { 227 | LEFT, RIGHT, CENTER 228 | } 229 | 230 | public static class Builder { 231 | private float textSize = Util.dpToPxF(12); 232 | private int colorText = Color.BLACK; 233 | private int colorBackground = Color.TRANSPARENT; 234 | private int bgRadius = 0; 235 | private int paddingLeft = 0; 236 | private int paddingTop = 0; 237 | private int paddingRight = 0; 238 | private int paddingBottom = 0; 239 | private SimpleGravity gravity = SimpleGravity.CENTER; 240 | private Typeface typeface; 241 | 242 | /** 243 | * Set up text size in pixels 244 | * 245 | * @param textSize test size in pixels 246 | * @return updated builder instance 247 | */ 248 | public Builder setTextSize(float textSize) { 249 | this.textSize = textSize; 250 | return this; 251 | } 252 | 253 | /** 254 | * Set up text size in dip and convert in to pixels 255 | * 256 | * @param textSize text size in dix 257 | * @return updated builder instance 258 | */ 259 | public Builder setTextSizeDp(int textSize) { 260 | this.textSize = Util.dpToPxF(textSize); 261 | return this; 262 | } 263 | 264 | /** 265 | * Set up text color 266 | * 267 | * @param colorText text color 268 | * @return updated builder instance 269 | */ 270 | public Builder setTextColor(int colorText) { 271 | this.colorText = colorText; 272 | return this; 273 | } 274 | 275 | /** 276 | * Set up background color 277 | * 278 | * @param colorBackground background color 279 | * @return updated builder instance 280 | */ 281 | public Builder setBackgroundColor(int colorBackground) { 282 | this.colorBackground = colorBackground; 283 | return this; 284 | } 285 | 286 | /** 287 | * Set up background corners radius in pixels 288 | * 289 | * @param bgRadius background corners radius in pixels 290 | * @return updated builder instance 291 | */ 292 | public Builder setBackgroundCornerRadius(int bgRadius) { 293 | this.bgRadius = bgRadius; 294 | return this; 295 | } 296 | 297 | /** 298 | * Set up background corners radius in dip and convert in to pixels 299 | * 300 | * @param bgRadius background corners radius in dip 301 | * @return updated builder instance 302 | */ 303 | public Builder setBackgroundCornerRadiusDp(int bgRadius) { 304 | return setBackgroundCornerRadius(Util.dp2px(bgRadius)); 305 | } 306 | 307 | /** 308 | * Set up text left padding in pixels 309 | * 310 | * @param paddingLeft text left padding in pixels 311 | * @return updated builder instance 312 | */ 313 | public Builder setPaddingLeft(int paddingLeft) { 314 | this.paddingLeft = paddingLeft; 315 | return this; 316 | } 317 | 318 | /** 319 | * Set up text left padding in dip and covert in to pixels 320 | * 321 | * @param paddingLeft left padding in dip 322 | * @return updated builder instance 323 | */ 324 | public Builder setPaddingLeftDp(int paddingLeft) { 325 | this.paddingLeft = Util.dp2px(paddingLeft); 326 | return this; 327 | } 328 | 329 | /** 330 | * Set up text top padding in in pixels 331 | * 332 | * @param paddingTop top padding in pixels 333 | * @return updated builder instance 334 | */ 335 | public Builder setPaddingTop(int paddingTop) { 336 | this.paddingTop = paddingTop; 337 | return this; 338 | } 339 | 340 | /** 341 | * Set up text top padding in dip and covert in to pixels 342 | * 343 | * @param paddingTop top padding in dip 344 | * @return updated builder instance 345 | */ 346 | public Builder setPaddingTopDp(int paddingTop) { 347 | this.paddingTop = Util.dp2px(paddingTop); 348 | return this; 349 | } 350 | 351 | /** 352 | * Set up text right padding in in pixels 353 | * 354 | * @param paddingRight right padding in pixels 355 | * @return updated builder instance 356 | */ 357 | public Builder setPaddingRight(int paddingRight) { 358 | this.paddingRight = paddingRight; 359 | return this; 360 | } 361 | 362 | /** 363 | * Set up text right padding in dip and covert in to pixels 364 | * 365 | * @param paddingRight right padding in dip 366 | * @return updated builder instance 367 | */ 368 | public Builder setPaddingRightDp(int paddingRight) { 369 | this.paddingRight = Util.dp2px(paddingRight); 370 | return this; 371 | } 372 | 373 | /** 374 | * Set up text bottom padding in in pixels 375 | * 376 | * @param paddingBottom right padding in pixels 377 | * @return updated builder instance 378 | */ 379 | public Builder setPaddingBottom(int paddingBottom) { 380 | this.paddingBottom = paddingBottom; 381 | return this; 382 | } 383 | 384 | /** 385 | * Set up text bottom padding in dip and covert in to pixels 386 | * 387 | * @param paddingBottom bottom padding in dip 388 | * @return updated builder instance 389 | */ 390 | public Builder setPaddingBottomDp(int paddingBottom) { 391 | this.paddingBottom = Util.dp2px(paddingBottom); 392 | return this; 393 | } 394 | 395 | 396 | /** 397 | * Set up symmetric padding in pixels 398 | * 399 | * @param horizontal horizontal padding in pixels 400 | * @param vertical vertical padding in pixels 401 | * @return updated builder instance 402 | */ 403 | public Builder setPaddingSymmetric(int horizontal, int vertical) { 404 | paddingLeft = horizontal; 405 | paddingTop = vertical; 406 | paddingRight = horizontal; 407 | paddingBottom = vertical; 408 | return this; 409 | } 410 | 411 | 412 | /** 413 | * Set up symmetric padding in dip and convert in to pixels 414 | * 415 | * @param horizontal horizontal padding in dip 416 | * @param vertical vertical padding in dip 417 | * @return updated builder instance 418 | */ 419 | public Builder setPaddingSymmetricDp(int horizontal, int vertical) { 420 | return setPaddingSymmetric(Util.dp2px(horizontal), Util.dp2px(vertical)); 421 | } 422 | 423 | /** 424 | * Set up text typeface 425 | * 426 | * @param typeface text typeface 427 | * @return updated builder instance 428 | */ 429 | public Builder setTypeface(@Nullable Typeface typeface) { 430 | this.typeface = typeface; 431 | return this; 432 | } 433 | 434 | /** 435 | * Set up text gravity. 436 | *
437 | * Support only horizontal gravity: left,right and center 438 | *
439 | * Center by default
440 | *
441 | * @param gravity value of {@link SimpleGravity}
442 | * @return updated builder instance
443 | */
444 | public Builder setTextGravity(@NonNull SimpleGravity gravity) {
445 | this.gravity = gravity;
446 | return this;
447 | }
448 |
449 | @NonNull
450 | public SimpleTextDrawable build() {
451 | SimpleTextDrawable drawable = new SimpleTextDrawable(textSize,
452 | colorText,
453 | gravity,
454 | colorBackground,
455 | bgRadius,
456 | paddingLeft,
457 | paddingTop,
458 | paddingRight,
459 | paddingBottom,
460 | typeface);
461 |
462 | drawable.setText("0"); // init drawable height
463 | return drawable;
464 | }
465 | }
466 | }
467 |
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/Util.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.content.res.Resources;
4 |
5 | abstract class Util {
6 |
7 | static int dp2px(int px) {
8 | return (int) dpToPxF(px);
9 | }
10 |
11 | static float dpToPxF(int px) {
12 | return Resources.getSystem().getDisplayMetrics().density * px;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/VerticalDrawableSectionDecor.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 | import android.graphics.drawable.Drawable;
6 | import android.view.View;
7 |
8 | import androidx.annotation.NonNull;
9 |
10 | /**
11 | * Uses drawable to draw section above item
12 | */
13 | public abstract class VerticalDrawableSectionDecor extends VerticalSectionDecor {
14 |
15 | @Override
16 | protected void onDrawSection(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) {
17 | Drawable drawable = getDrawable(position, sectionBounds, child);
18 | onDrawDrawable(c, position, drawable, sectionBounds);
19 | }
20 |
21 | protected void onDrawDrawable(@NonNull Canvas c, int position, @NonNull Drawable drawable, Rect bounds) {
22 | drawable.setBounds(bounds);
23 | drawable.draw(c);
24 | }
25 |
26 | /**
27 | * Return drawable which will be drawn
28 | *
29 | * @param position Adapter item position
30 | * @param sectionBounds Section bounds
31 | * @param child RecyclerView's child view
32 | */
33 | @NonNull
34 | protected abstract Drawable getDrawable(int position, @NonNull Rect sectionBounds, @NonNull View child);
35 | }
36 |
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/VerticalSectionDecor.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 | import android.view.View;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.recyclerview.widget.GridLayoutManager;
9 | import androidx.recyclerview.widget.RecyclerView;
10 |
11 | /**
12 | * Draws sections decoration
13 | * By default decor draw full width and height allocated in {@link ConditionItemDecorator.Decor#getConditionItemOffsets(RecyclerView, Rect, View, int)}
14 | */
15 | public abstract class VerticalSectionDecor implements ConditionItemDecorator.Decor {
16 | protected GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager.DefaultSpanSizeLookup();
17 |
18 | protected final Rect decoratedBounds = new Rect();
19 | protected final Rect viewBounds = new Rect();
20 | private Rect sectionBounds = new Rect();
21 |
22 | @Override
23 | public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull View child, int position, @NonNull RecyclerView.State state) {
24 | Rect decoratedBounds = getDecoratedViewBounds(parent, child);
25 | Rect bounds = getViewBounds(parent, child);
26 | onDrawSectionInternal(c, position, getSectionBounds(parent, position, bounds, decoratedBounds), child);
27 | }
28 |
29 | /**
30 | * Returns the bounds of the view including its decoration and margins.
31 | * This bounds also include view translations
32 | *
33 | * @param child The view element to check
34 | * @return The bounds of the view including its decoration and margins.
35 | */
36 | protected Rect getDecoratedViewBounds(@NonNull RecyclerView parent, @NonNull View child) {
37 | parent.getDecoratedBoundsWithMargins(child, decoratedBounds);
38 | addTranslationOffsetToViewBounds(child, decoratedBounds);
39 | return decoratedBounds;
40 | }
41 |
42 | void onDrawSectionInternal(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) {
43 | onDrawSection(c, position, sectionBounds, child);
44 | }
45 |
46 | protected abstract void onDrawSection(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child);
47 |
48 | /**
49 | * Get view bounds without decoration offsets
50 | *
51 | * @param parent - current recycler view
52 | * @param child - itemView from recycler view
53 | * @return real view bounds without decoration offsets
54 | */
55 | protected Rect getViewBounds(@NonNull RecyclerView parent, @NonNull View child) {
56 | child.getDrawingRect(viewBounds);
57 | parent.offsetDescendantRectToMyCoords(child, viewBounds);
58 | addTranslationOffsetToViewBounds(child, viewBounds);
59 | return viewBounds;
60 | }
61 |
62 | /**
63 | * Add view translation to bound to support {@link androidx.recyclerview.widget.DefaultItemAnimator}
64 | *
65 | * @param child - itemView from recycler
66 | * @param viewBounds - current itemView bounds without decoration offsets
67 | */
68 | protected void addTranslationOffsetToViewBounds(@NonNull View child, @NonNull Rect viewBounds) {
69 | viewBounds.offset((int) child.getTranslationX(), (int) child.getTranslationY());
70 | }
71 |
72 | @NonNull
73 | protected Rect getSectionBounds(@NonNull RecyclerView parent, int position, @NonNull Rect viewBounds, @NonNull Rect decoratedBounds) {
74 | int left = viewBounds.left;
75 | int top = decoratedBounds.top + getSectionMarginTop();
76 | int right = viewBounds.right;
77 | int bottom = viewBounds.top - getSectionMarginBottom();
78 | sectionBounds.set(left, top, right, bottom);
79 | return sectionBounds;
80 | }
81 |
82 | @Override
83 | public void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
84 |
85 | }
86 |
87 | @Override
88 | public void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
89 |
90 | }
91 |
92 | @Override
93 | public void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state) {
94 |
95 | }
96 |
97 |
98 | @Override
99 | public void getConditionItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position) {
100 | rect.top += getSectionHeight(position) + getSectionMarginTop() + getSectionMarginBottom();
101 | }
102 |
103 | /**
104 | * Get section height
105 | *
106 | * @param position item adapter position
107 | * @return section height
108 | */
109 | protected abstract int getSectionHeight(int position);
110 |
111 | /**
112 | * Get top margin for section
113 | *
114 | * @return vertical margin
115 | */
116 | protected int getSectionMarginTop() {
117 | return 0;
118 | }
119 |
120 | /**
121 | * Get bottom margin for section
122 | *
123 | * @return vertical margin
124 | */
125 | protected int getSectionMarginBottom() {
126 | return 0;
127 | }
128 |
129 | public void setSpanSizeLookup(GridLayoutManager.SpanSizeLookup spanSizeLookup) {
130 | this.spanSizeLookup = spanSizeLookup;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/VerticalStickyDecor.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 | import android.view.View;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.recyclerview.widget.RecyclerView;
9 |
10 | /**
11 | * Draws section decoration and sticky header on the top of RecyclerView.
12 | * Now tested only with {@link androidx.recyclerview.widget.LinearLayoutManager} and vertical orientation.
13 | * Support reversed layout.
14 | */
15 | public abstract class VerticalStickyDecor extends VerticalSectionDecor {
16 | public static int HEADER_HEIGHT_UNDEFINED = -1;
17 |
18 | private int contactPosition = RecyclerView.NO_POSITION;
19 | private Section contactSection = new Section(-1, RecyclerView.NO_POSITION);
20 | private int lastHeaderHeight = 0;
21 | private Rect headerBounds = new Rect();
22 | private boolean reverseLayout;
23 |
24 | public VerticalStickyDecor() {
25 | this(false);
26 | }
27 |
28 | public VerticalStickyDecor(boolean reverseLayout) {
29 | this.reverseLayout = reverseLayout;
30 | }
31 |
32 | @Override
33 | public void getItemOffsets(@NonNull RecyclerView parent, @NonNull Rect rect, @NonNull View view, int position, @NonNull RecyclerView.State state) {
34 | super.getItemOffsets(parent, rect, view, position, state);
35 | int count = 0;
36 | RecyclerView.LayoutManager layout = parent.getLayoutManager();
37 | if (layout != null) {
38 | count = layout.getItemCount();
39 | }
40 | if (reverseLayout && position == count - 1) {
41 | rect.top += getHeaderHeight() + getHeaderMarginTop() + getHeaderMarginBottom();
42 | }
43 | }
44 |
45 | @Override
46 | public void prepareDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
47 | super.prepareDrawOver(c, parent, state);
48 |
49 | // clear last header state
50 | contactPosition = RecyclerView.NO_POSITION;
51 | contactSection.isValid = false;
52 | }
53 |
54 | @Override
55 | void onDrawSectionInternal(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) {
56 | if (sectionBounds.top > getHeaderMarginTop()) {
57 | // don't draw section above header
58 | super.onDrawSectionInternal(c, position, sectionBounds, child);
59 | }
60 |
61 | lastHeaderHeight = sectionBounds.height();
62 | int contactPoint = getHeaderHeightInternal() + getHeaderMarginTop() + getHeaderMarginBottom();
63 | if ((contactPoint >= sectionBounds.top && contactPoint < sectionBounds.bottom + getHeaderMarginBottom())) {
64 | contactSection.isValid = true;
65 | contactSection.position = position;
66 | contactSection.top = sectionBounds.top;
67 | }
68 | }
69 |
70 | @Override
71 | public void onPostDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
72 | super.onPostDrawOver(c, parent, state);
73 | for (int i = 0; i < parent.getChildCount(); i++) {
74 | View child = parent.getChildAt(i);
75 | int position = parent.getChildAdapterPosition(child);
76 | parent.getDecoratedBoundsWithMargins(child, decoratedBounds);
77 | getViewBounds(parent, child);
78 | int viewTop = decoratedBounds.top;
79 | int contactPoint = getHeaderHeightInternal() + getHeaderMarginTop();
80 | if (viewTop <= contactPoint) {
81 | contactPosition = position;
82 | }
83 | }
84 |
85 | drawHeaderInternal(c, parent, state);
86 | }
87 |
88 | private void drawHeaderInternal(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
89 | if (contactSection.isValid) {
90 | int itemPosition = getItemPosition(state);
91 | int headerBottom = contactSection.top - getHeaderMarginBottom();
92 | Rect headerBounds = getHeaderBounds(parent, headerBottom, itemPosition, viewBounds, decoratedBounds);
93 | if (!reverseLayout && contactSection.position == 0 && headerBounds.top - getHeaderMarginTop() < 0) {
94 | return;
95 | }
96 |
97 | onDrawHeader(c, itemPosition, headerBounds);
98 | return;
99 | }
100 |
101 | if (contactPosition != RecyclerView.NO_POSITION) {
102 | int itemPosition = contactPosition;
103 | int headerBottom = getHeaderMarginTop() + getHeaderHeightInternal();
104 | Rect headerBounds = getHeaderBounds(parent, headerBottom, itemPosition, viewBounds, decoratedBounds);
105 | onDrawHeader(c, itemPosition, headerBounds);
106 | }
107 | }
108 |
109 | private int getItemPosition(@NonNull RecyclerView.State state) {
110 | return reverseLayout ?
111 | Math.min(contactSection.position + 1, state.getItemCount() - 1) :
112 | Math.max(contactSection.position - 1, 0);
113 | }
114 |
115 | /**
116 | * Draw header drawable on canvas
117 | *
118 | * @param c RecyclerView canvas
119 | * @param position item position from 0 to {@link RecyclerView.Adapter#getItemCount()}
120 | * @param headerBounds header bounds
121 | */
122 | protected abstract void onDrawHeader(@NonNull Canvas c, int position, @NonNull Rect headerBounds);
123 |
124 | @NonNull
125 | protected Rect getHeaderBounds(@NonNull RecyclerView parent, int headerBottom, int itemPosition, @NonNull Rect viewBounds, @NonNull Rect decoratedBounds) {
126 | int left = decoratedBounds.left;
127 | int top = headerBottom - getHeaderHeightInternal();
128 | int right = decoratedBounds.right;
129 | headerBounds.set(left, top, right, headerBottom);
130 | return headerBounds;
131 | }
132 |
133 | protected int getHeaderMarginTop() {
134 | return getSectionMarginTop();
135 | }
136 |
137 | protected int getHeaderMarginBottom() {
138 | return getSectionMarginBottom();
139 | }
140 |
141 | /**
142 | * Return header height. As default return -1. It means that header height takes from section size
143 | *
144 | * @return header height
145 | */
146 | protected int getHeaderHeight() {
147 | return HEADER_HEIGHT_UNDEFINED;
148 | }
149 |
150 | private int getHeaderHeightInternal() {
151 | int userHeight = getHeaderHeight();
152 | if (userHeight != HEADER_HEIGHT_UNDEFINED) {
153 | return userHeight;
154 | }
155 | return lastHeaderHeight;
156 | }
157 |
158 | private static class Section {
159 | int top;
160 | int position;
161 | boolean isValid = false;
162 |
163 | Section(int top, int position) {
164 | this.top = top;
165 | this.position = position;
166 | }
167 | }
168 | }
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/VerticalStickyDrawableDecor.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 | import android.graphics.drawable.Drawable;
6 | import android.view.View;
7 |
8 | import androidx.annotation.NonNull;
9 |
10 | public abstract class VerticalStickyDrawableDecor extends VerticalStickyDecor {
11 |
12 | public VerticalStickyDrawableDecor() {
13 | }
14 |
15 | public VerticalStickyDrawableDecor(boolean reverseLayout) {
16 | super(reverseLayout);
17 | }
18 |
19 | @Override
20 | protected void onDrawHeader(@NonNull Canvas c, int position, @NonNull Rect headerBounds) {
21 | Drawable drawable = getHeaderDrawable(position, headerBounds);
22 | onDrawHeaderDrawable(c, position, headerBounds, drawable);
23 | }
24 |
25 | protected void onDrawHeaderDrawable(@NonNull Canvas c, int position, @NonNull Rect headerBounds, @NonNull Drawable drawable) {
26 | drawable.setBounds(headerBounds);
27 | drawable.draw(c);
28 | }
29 |
30 | @Override
31 | protected void onDrawSection(@NonNull Canvas c, int position, @NonNull Rect sectionBounds, @NonNull View child) {
32 | Drawable drawable = getSectionDrawable(position, sectionBounds, child);
33 | onDrawSectionDrawable(c, position, drawable, sectionBounds);
34 | }
35 |
36 | protected void onDrawSectionDrawable(@NonNull Canvas c, int position, @NonNull Drawable drawable, Rect bounds) {
37 | drawable.setBounds(bounds);
38 | drawable.draw(c);
39 | }
40 |
41 | /**
42 | * Return drawable which will be drawn for section
43 | *
44 | * @param position Adapter item position
45 | * @param sectionBounds Section bounds
46 | * @param child RecyclerView's child view
47 | */
48 | @NonNull
49 | protected abstract Drawable getSectionDrawable(int position, @NonNull Rect sectionBounds, @NonNull View child);
50 |
51 | /**
52 | * Return drawable which will be drawn for header
53 | *
54 | * @param position Adapter item position near by header
55 | * @param headerBounds Header bounds
56 | */
57 | @NonNull
58 | protected abstract Drawable getHeaderDrawable(int position, @NonNull Rect headerBounds);
59 | }
60 |
--------------------------------------------------------------------------------
/sticky/src/main/java/com/osome/stickydecorator/ViewHolderStickyDecoration.java:
--------------------------------------------------------------------------------
1 | package com.osome.stickydecorator;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Rect;
5 | import android.util.Pair;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | import androidx.annotation.NonNull;
10 | import androidx.annotation.Nullable;
11 | import androidx.recyclerview.widget.RecyclerView;
12 |
13 | public class ViewHolderStickyDecoration extends RecyclerView.ItemDecoration {
14 |
15 | private Rect bounds = new Rect();
16 | private Pair