35 | * +------------------------------+ 36 | * |---+ +----------------+ +---| 37 | * | | | current | | | 38 | * | | | page | | | 39 | * |---+ +----------------+ +---| 40 | * +------------------------------+ 41 | */ 42 | public RecyclerViewAttacher() { 43 | currentPageOffset = 0; // Unused when centered 44 | centered = true; 45 | } 46 | 47 | /** 48 | * Use this constructor if current page in recycler isn't centered. 49 | * All pages must have the same width. 50 | * Like this: 51 | *
52 | * +-|----------------------------+
53 | * | +--------+ +--------+ +----|
54 | * | | current| | | | |
55 | * | | page | | | | |
56 | * | +--------+ +--------+ +----|
57 | * +-|----------------------------+
58 | * | currentPageOffset
59 | * |
60 | *
61 | * @param currentPageOffset x coordinate of current view left corner/top relative to recycler view.
62 | */
63 | public RecyclerViewAttacher(int currentPageOffset) {
64 | this.currentPageOffset = currentPageOffset;
65 | this.centered = false;
66 | }
67 |
68 | @Override
69 | public void attachToPager(@NonNull final ScrollingPagerIndicator indicator, @NonNull final RecyclerView pager) {
70 | if (!(pager.getLayoutManager() instanceof LinearLayoutManager)) {
71 | throw new IllegalStateException("Only LinearLayoutManager is supported");
72 | }
73 | if (pager.getAdapter() == null) {
74 | throw new IllegalStateException("RecyclerView has not Adapter attached");
75 | }
76 | this.layoutManager = (LinearLayoutManager) pager.getLayoutManager();
77 | this.recyclerView = pager;
78 | this.attachedAdapter = pager.getAdapter();
79 | this.indicator = indicator;
80 |
81 | dataObserver = new RecyclerView.AdapterDataObserver() {
82 | @Override
83 | public void onChanged() {
84 | indicator.setDotCount(attachedAdapter.getItemCount());
85 | updateCurrentOffset();
86 | }
87 |
88 | @Override
89 | public void onItemRangeChanged(int positionStart, int itemCount) {
90 | onChanged();
91 | }
92 |
93 | @Override
94 | public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
95 | onChanged();
96 | }
97 |
98 | @Override
99 | public void onItemRangeInserted(int positionStart, int itemCount) {
100 | onChanged();
101 | }
102 |
103 | @Override
104 | public void onItemRangeRemoved(int positionStart, int itemCount) {
105 | onChanged();
106 | }
107 |
108 | @Override
109 | public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
110 | onChanged();
111 | }
112 | };
113 | attachedAdapter.registerAdapterDataObserver(dataObserver);
114 | indicator.setDotCount(attachedAdapter.getItemCount());
115 | updateCurrentOffset();
116 |
117 | scrollListener = new RecyclerView.OnScrollListener() {
118 | @Override
119 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
120 | if (newState == RecyclerView.SCROLL_STATE_IDLE && isInIdleState()) {
121 | int newPosition = findCompletelyVisiblePosition();
122 | if (newPosition != RecyclerView.NO_POSITION) {
123 | indicator.setDotCount(attachedAdapter.getItemCount());
124 | if (newPosition < attachedAdapter.getItemCount()) {
125 | indicator.setCurrentPosition(newPosition);
126 | }
127 | }
128 | }
129 | }
130 |
131 | @Override
132 | public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
133 | updateCurrentOffset();
134 | }
135 | };
136 |
137 | recyclerView.addOnScrollListener(scrollListener);
138 | }
139 |
140 | @Override
141 | public void detachFromPager() {
142 | attachedAdapter.unregisterAdapterDataObserver(dataObserver);
143 | recyclerView.removeOnScrollListener(scrollListener);
144 | measuredChildWidth = 0;
145 | }
146 |
147 | private void updateCurrentOffset() {
148 | final View firstView = findFirstVisibleView();
149 | if (firstView == null) {
150 | return;
151 | }
152 |
153 | int position = recyclerView.getChildAdapterPosition(firstView);
154 | if (position == RecyclerView.NO_POSITION) {
155 | return;
156 | }
157 | final int itemCount = attachedAdapter.getItemCount();
158 |
159 | // In case there is an infinite pager
160 | if (position >= itemCount && itemCount != 0) {
161 | position = position % itemCount;
162 | }
163 |
164 | float offset;
165 | if (layoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {
166 | offset = (getCurrentFrameLeft() - firstView.getX()) / firstView.getMeasuredWidth();
167 | } else {
168 | offset = (getCurrentFrameBottom() - firstView.getY()) / firstView.getMeasuredHeight();
169 | }
170 |
171 | if (offset >= 0 && offset <= 1 && position < itemCount) {
172 | indicator.onPageScrolled(position, offset);
173 | }
174 | }
175 |
176 | private int findCompletelyVisiblePosition() {
177 | for (int i = 0; i < recyclerView.getChildCount(); i++) {
178 | View child = recyclerView.getChildAt(i);
179 |
180 | float position = child.getX();
181 | int size = child.getMeasuredWidth();
182 | float currentStart = getCurrentFrameLeft();
183 | float currentEnd = getCurrentFrameRight();
184 | if (layoutManager.getOrientation() == LinearLayoutManager.VERTICAL) {
185 | position = child.getY();
186 | size = child.getMeasuredHeight();
187 | currentStart = getCurrentFrameTop();
188 | currentEnd = getCurrentFrameBottom();
189 | }
190 |
191 | if (position >= currentStart && position + size <= currentEnd) {
192 | RecyclerView.ViewHolder holder = recyclerView.findContainingViewHolder(child);
193 | if (holder != null && holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
194 | return holder.getAdapterPosition();
195 | }
196 | }
197 | }
198 | return RecyclerView.NO_POSITION;
199 | }
200 |
201 | private boolean isInIdleState() {
202 | return findCompletelyVisiblePosition() != RecyclerView.NO_POSITION;
203 | }
204 |
205 | @Nullable
206 | private View findFirstVisibleView() {
207 | int childCount = layoutManager.getChildCount();
208 | if (childCount == 0) {
209 | return null;
210 | }
211 |
212 | View closestChild = null;
213 | int firstVisibleChild = Integer.MAX_VALUE;
214 |
215 | for (int i = 0; i < childCount; i++) {
216 | final View child = layoutManager.getChildAt(i);
217 |
218 | if (layoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL) {
219 | // Default implementation change: use getX instead of helper
220 | int childStart = (int) child.getX();
221 |
222 | // if child is more to start than previous closest, set it as closest
223 |
224 | // Default implementation change:
225 | // Fix for any count of visible items
226 | // We make assumption that all children have the same width
227 | if (childStart + child.getMeasuredWidth() < firstVisibleChild
228 | && childStart + child.getMeasuredWidth() >= getCurrentFrameLeft()) {
229 | firstVisibleChild = childStart;
230 | closestChild = child;
231 | }
232 | } else {
233 | // Default implementation change: use geetY instead of helper
234 | int childStart = (int) child.getY();
235 |
236 | // if child is more to top than previous closest, set it as closest
237 |
238 | // Default implementation change:
239 | // Fix for any count of visible items
240 | // We make assumption that all children have the same height
241 | if (childStart + child.getMeasuredHeight() < firstVisibleChild
242 | && childStart + child.getMeasuredHeight() >= getCurrentFrameBottom()) {
243 | firstVisibleChild = childStart;
244 | closestChild = child;
245 | }
246 | }
247 | }
248 |
249 | return closestChild;
250 | }
251 |
252 | private float getCurrentFrameLeft() {
253 | if (centered) {
254 | return (recyclerView.getMeasuredWidth() - getChildWidth()) / 2;
255 | } else {
256 | return currentPageOffset;
257 | }
258 | }
259 |
260 | private float getCurrentFrameRight() {
261 | if (centered) {
262 | return (recyclerView.getMeasuredWidth() - getChildWidth()) / 2 + getChildWidth();
263 | } else {
264 | return currentPageOffset + getChildWidth();
265 | }
266 | }
267 |
268 | private float getCurrentFrameTop() {
269 | if (centered) {
270 | return (recyclerView.getMeasuredHeight() - getChildHeight()) / 2;
271 | } else {
272 | return currentPageOffset;
273 | }
274 | }
275 |
276 | private float getCurrentFrameBottom() {
277 | if (centered) {
278 | return (recyclerView.getMeasuredHeight() - getChildHeight()) / 2 + getChildHeight();
279 | } else {
280 | return currentPageOffset + getChildHeight();
281 | }
282 | }
283 |
284 | private float getChildWidth() {
285 | if (measuredChildWidth == 0) {
286 | for (int i = 0; i < recyclerView.getChildCount(); i++) {
287 | View child = recyclerView.getChildAt(i);
288 | if (child.getMeasuredWidth() != 0) {
289 | measuredChildWidth = child.getMeasuredWidth();
290 | return measuredChildWidth;
291 | }
292 | }
293 | }
294 | return measuredChildWidth;
295 | }
296 |
297 | private float getChildHeight() {
298 | if (measuredChildHeight == 0) {
299 | for (int i = 0; i < recyclerView.getChildCount(); i++) {
300 | View child = recyclerView.getChildAt(i);
301 | if (child.getMeasuredHeight() != 0) {
302 | measuredChildHeight = child.getMeasuredHeight();
303 | return measuredChildHeight;
304 | }
305 | }
306 | }
307 | return measuredChildHeight;
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/scrollingpagerindicator/src/main/java/ru/tinkoff/scrollingpagerindicator/ScrollingPagerIndicator.java:
--------------------------------------------------------------------------------
1 | package ru.tinkoff.scrollingpagerindicator;
2 |
3 | import android.animation.ArgbEvaluator;
4 | import android.content.Context;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Canvas;
7 | import android.graphics.Paint;
8 | import android.graphics.drawable.Drawable;
9 | import android.os.Build;
10 | import android.util.AttributeSet;
11 | import android.util.SparseArray;
12 | import android.view.View;
13 | import android.widget.LinearLayout;
14 |
15 | import androidx.annotation.ColorInt;
16 | import androidx.annotation.IntDef;
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.Nullable;
19 | import androidx.recyclerview.widget.LinearLayoutManager;
20 | import androidx.recyclerview.widget.RecyclerView;
21 | import androidx.viewpager.widget.ViewPager;
22 | import androidx.viewpager2.widget.ViewPager2;
23 |
24 | /**
25 | * @author Nikita Olifer
26 | */
27 | public class ScrollingPagerIndicator extends View {
28 |
29 | @IntDef({RecyclerView.HORIZONTAL, RecyclerView.VERTICAL})
30 | public @interface Orientation{}
31 |
32 | private int infiniteDotCount;
33 |
34 | private final int dotMinimumSize;
35 | private final int dotNormalSize;
36 | private final int dotSelectedSize;
37 | private final int spaceBetweenDotCenters;
38 | private int visibleDotCount;
39 | private int visibleDotThreshold;
40 | private int orientation;
41 |
42 | private float visibleFramePosition;
43 | private float visibleFrameWidth;
44 |
45 | private float firstDotOffset;
46 | private SparseArray
267 | * +------------------------------+
268 | * |---+ +----------------+ +---|
269 | * | | | current | | |
270 | * | | | page | | |
271 | * |---+ +----------------+ +---|
272 | * +------------------------------+
273 | *
274 | * @param recyclerView recycler view to attach
275 | */
276 | public void attachToRecyclerView(@NonNull RecyclerView recyclerView) {
277 | attachToPager(recyclerView, new RecyclerViewAttacher());
278 | }
279 |
280 | /**
281 | * Attaches indicator to RecyclerView. Use this method if current page of the recycler isn't centered.
282 | * All pages must have the same width.
283 | * Like this:
284 | *
285 | * +-|----------------------------+
286 | * | +--------+ +--------+ +----|
287 | * | | current| | | | |
288 | * | | page | | | | |
289 | * | +--------+ +--------+ +----|
290 | * +-|----------------------------+
291 | * | currentPageOffset
292 | * |
293 | *
294 | * @param recyclerView recycler view to attach
295 | * @param currentPageOffset x coordinate of current view left corner/top relative to recycler view
296 | */
297 | public void attachToRecyclerView(@NonNull RecyclerView recyclerView, int currentPageOffset) {
298 | attachToPager(recyclerView, new RecyclerViewAttacher(currentPageOffset));
299 | }
300 |
301 | /**
302 | * Attaches to any custom pager
303 | *
304 | * @param pager pager to attach
305 | * @param attacher helper which should setup this indicator to work with custom pager
306 | */
307 | public