49

Any idea how to add page indicator

Any idea how to create a page indicator for recyclerview list ?

Dan Cornilescu
  • 39,470
  • 12
  • 57
  • 97
Nima
  • 1,892
  • 4
  • 24
  • 32

11 Answers11

62

You can add an indicator by using RecyclerView.ItemDecoration.

Just draw some lines or circles at the bottom and use layoutManager.findFirstVisibleItemPosition() to get the current active item. Since pagers tend to fill the whole width this is an accruate way of getting the displayed item. This also allows us to calculate the scrolling distance by comparing the childs left edge to the parents.

Below you find a sample decoration that draws some lines and animates between them

public class LinePagerIndicatorDecoration extends RecyclerView.ItemDecoration {

  private int colorActive = 0xFFFFFFFF;
  private int colorInactive = 0x66FFFFFF;

  private static final float DP = Resources.getSystem().getDisplayMetrics().density;

  /**
   * Height of the space the indicator takes up at the bottom of the view.
   */
  private final int mIndicatorHeight = (int) (DP * 16);

  /**
   * Indicator stroke width.
   */
  private final float mIndicatorStrokeWidth = DP * 2;

  /**
   * Indicator width.
   */
  private final float mIndicatorItemLength = DP * 16;
  /**
   * Padding between indicators.
   */
  private final float mIndicatorItemPadding = DP * 4;

  /**
   * Some more natural animation interpolation
   */
  private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();

  private final Paint mPaint = new Paint();

  public LinePagerIndicatorDecoration() {
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setStrokeWidth(mIndicatorStrokeWidth);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setAntiAlias(true);
  }

  @Override
  public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);

    int itemCount = parent.getAdapter().getItemCount();

    // center horizontally, calculate width and subtract half from center
    float totalLength = mIndicatorItemLength * itemCount;
    float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;
    float indicatorTotalWidth = totalLength + paddingBetweenItems;
    float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;

    // center vertically in the allotted space
    float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;

    drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);


    // find active page (which should be highlighted)
    LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
    int activePosition = layoutManager.findFirstVisibleItemPosition();
    if (activePosition == RecyclerView.NO_POSITION) {
      return;
    }

    // find offset of active page (if the user is scrolling)
    final View activeChild = layoutManager.findViewByPosition(activePosition);
    int left = activeChild.getLeft();
    int width = activeChild.getWidth();

    // on swipe the active item will be positioned from [-width, 0]
    // interpolate offset for smooth animation
    float progress = mInterpolator.getInterpolation(left * -1 / (float) width);

    drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress, itemCount);
  }

  private void drawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) {
    mPaint.setColor(colorInactive);

    // width of item indicator including padding
    final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

    float start = indicatorStartX;
    for (int i = 0; i < itemCount; i++) {
      // draw the line for every item
      c.drawLine(start, indicatorPosY, start + mIndicatorItemLength, indicatorPosY, mPaint);
      start += itemWidth;
    }
  }

  private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,
                              int highlightPosition, float progress, int itemCount) {
    mPaint.setColor(colorActive);

    // width of item indicator including padding
    final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

    if (progress == 0F) {
      // no swipe, draw a normal indicator
      float highlightStart = indicatorStartX + itemWidth * highlightPosition;
      c.drawLine(highlightStart, indicatorPosY,
          highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);
    } else {
      float highlightStart = indicatorStartX + itemWidth * highlightPosition;
      // calculate partial highlight
      float partialLength = mIndicatorItemLength * progress;

      // draw the cut off highlight
      c.drawLine(highlightStart + partialLength, indicatorPosY,
          highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);

      // draw the highlight overlapping to the next item as well
      if (highlightPosition < itemCount - 1) {
        highlightStart += itemWidth;
        c.drawLine(highlightStart, indicatorPosY,
            highlightStart + partialLength, indicatorPosY, mPaint);
      }
    }
  }

  @Override
  public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    outRect.bottom = mIndicatorHeight;
  }
}

Which will give you a result like the following

pager indicator


There is also a blog post that goes more into detail about how the decoration works here and the full source code is available at GitHub

David Medenjak
  • 33,993
  • 14
  • 106
  • 134
  • so i have a recyclerview with a 3x3 grid, when items exceed the 3x3 it goes to the next page and so on,like so https://i.stack.imgur.com/JYPu7.gif, im asking because you have mentioned "make sure that the item layouts have layout_width='match_parent' " – usr30911 Sep 13 '17 at 20:28
  • 1
    @ViVekH Heh, I was just looking at your question when you commented...None of the above applies to your use case. The match_parent is so that the indicator draws correctly (to calculate scroll) and it is intended for a ViewPager-like setup. – David Medenjak Sep 13 '17 at 20:31
  • so ill have to make another indicator or recyclerview which i keep changing according to my main recycler's scroll right? – usr30911 Sep 13 '17 at 20:35
  • @ViVekH You could still use a decoration, but it would need a lot more calculations and setup than shown above to work with a GridLayout and non-full-width views – David Medenjak Sep 13 '17 at 20:38
  • got it, off topic but if you dont mind relooking at my question https://stackoverflow.com/questions/46206283/recyclerview-horizontal-span-and-spacing-issues , can you tellme if for the functionality i am aiming is recyclerview the way to go,or is there a easier way to do what im trying to do. – usr30911 Sep 13 '17 at 20:43
  • 1
    @David Medenjak I tried your answer but it is not working in my case. – Manoj Fegde Oct 04 '17 at 06:52
  • @DavidMedenjak I cannot figure out how to put the indicator ABOVE the recyclerView items. Can you help me out? Also, "Some Content" is not centered vertically in the recyclerView anymore as the indicator takes up space. Can I make it centered again? – HideAndSeek Aug 20 '18 at 11:48
  • 1
    @HideAndSeek `getItemOffsets(..)` declares the space used by the decoration. Instead of bottom you'd set top here, or nothing if you want to draw _over_ the content. To put it on top you'd modify `indicatorPosY` and remove the height call ( just `mIndicatorHeight / 2F` should be enough) – David Medenjak Aug 20 '18 at 11:50
  • @DavidMedenjak, I have 18 items in recyclerview. At a time showing 6 items so i can add 3 indicators to screen. How do i achieve this. – Manukumar May 24 '19 at 10:51
  • @DavidMedenjak, i tried above code but top of recycleview noting is showing can pls tell me where can i call above code exactly – Manideep Dec 01 '21 at 19:20
  • @DavidMedenjak, first thanks for your answer but need to know, if i have to use this decorator then how can i give margin between items – Reprator Mar 17 '22 at 04:44
  • suppose we have recyclerview stackfromend true then how to handle the viewpagerindicator in reverse order? – android dev Dec 16 '22 at 14:04
47

I have changed the code for circles. Removed the the code to draw line and the same is replaced with draw circle methods. Please find below the complete class:

public class CirclePagerIndicatorDecoration extends RecyclerView.ItemDecoration {
    private int colorActive = 0xDE000000;
    private int colorInactive = 0x33000000;

    private static final float DP = Resources.getSystem().getDisplayMetrics().density;

    /**
     * Height of the space the indicator takes up at the bottom of the view.
     */
    private final int mIndicatorHeight = (int) (DP * 16);

    /**
     * Indicator stroke width.
     */
    private final float mIndicatorStrokeWidth = DP * 4;

    /**
     * Indicator width.
     */
    private final float mIndicatorItemLength = DP * 4;
    /**
     * Padding between indicators.
     */
    private final float mIndicatorItemPadding = DP * 8;

    /**
     * Some more natural animation interpolation
     */
    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();

    private final Paint mPaint = new Paint();

    public CirclePagerIndicatorDecoration() {

        mPaint.setStrokeWidth(mIndicatorStrokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        int itemCount = parent.getAdapter().getItemCount();

        // center horizontally, calculate width and subtract half from center
        float totalLength = mIndicatorItemLength * itemCount;
        float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;
        float indicatorTotalWidth = totalLength + paddingBetweenItems;
        float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;

        // center vertically in the allotted space
        float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;

        drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);

        // find active page (which should be highlighted)
        LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
        int activePosition = layoutManager.findFirstVisibleItemPosition();
        if (activePosition == RecyclerView.NO_POSITION) {
            return;
        }

        // find offset of active page (if the user is scrolling)
        final View activeChild = layoutManager.findViewByPosition(activePosition);
        int left = activeChild.getLeft();
        int width = activeChild.getWidth();
        int right = activeChild.getRight();

        // on swipe the active item will be positioned from [-width, 0]
        // interpolate offset for smooth animation
        float progress = mInterpolator.getInterpolation(left * -1 / (float) width);

        drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress);
    }

    private void drawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) {
        mPaint.setColor(colorInactive);

        // width of item indicator including padding
        final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

        float start = indicatorStartX;
        for (int i = 0; i < itemCount; i++) {

            c.drawCircle(start, indicatorPosY, mIndicatorItemLength / 2F, mPaint);

            start += itemWidth;
        }
    }

    private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,
                                int highlightPosition, float progress) {
        mPaint.setColor(colorActive);

        // width of item indicator including padding
        final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

        if (progress == 0F) {
            // no swipe, draw a normal indicator
            float highlightStart = indicatorStartX + itemWidth * highlightPosition;

            c.drawCircle(highlightStart, indicatorPosY, mIndicatorItemLength / 2F, mPaint);

        } else {
            float highlightStart = indicatorStartX + itemWidth * highlightPosition;
            // calculate partial highlight
            float partialLength = mIndicatorItemLength * progress + mIndicatorItemPadding*progress;

            c.drawCircle(highlightStart + partialLength, indicatorPosY, mIndicatorItemLength / 2F, mPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = mIndicatorHeight;
    }
}

Many Thanks, LB Gupta Happy Coding !!!!!

JCDecary
  • 557
  • 1
  • 7
  • 18
lbgupta
  • 471
  • 4
  • 3
11

I have copied the same answer which is given by David Medenjak , but to make the circles below the recyclerview . I have updated few lines of code in the above answer , please have a look and use accordingly .

/**
 * Created by shobhan on 4/10/17.
 */

public class CirclePagerIndicatorDecoration extends RecyclerView.ItemDecoration {

    private int colorActive = 0x727272;
    private int colorInactive = 0xF44336;

    private static final float DP = Resources.getSystem().getDisplayMetrics().density;

    /**
     * Height of the space the indicator takes up at the bottom of the view.
     */
    private final int mIndicatorHeight = (int) (DP * 16);

    /**
     * Indicator stroke width.
     */
    private final float mIndicatorStrokeWidth = DP * 2;

    /**
     * Indicator width.
     */
    private final float mIndicatorItemLength = DP * 16;
    /**
     * Padding between indicators.
     */
    private final float mIndicatorItemPadding = DP * 4;

    /**
     * Some more natural animation interpolation
     */
    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();

    private final Paint mPaint = new Paint();

    public CirclePagerIndicatorDecoration() {
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(mIndicatorStrokeWidth);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);

        int itemCount = parent.getAdapter().getItemCount();

        // center horizontally, calculate width and subtract half from center
        float totalLength = mIndicatorItemLength * itemCount;
        float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;
        float indicatorTotalWidth = totalLength + paddingBetweenItems;
        float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;

        // center vertically in the allotted space
        float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;

        drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);


        // find active page (which should be highlighted)
        LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
        int activePosition = layoutManager.findFirstVisibleItemPosition();
        if (activePosition == RecyclerView.NO_POSITION) {
            return;
        }

        // find offset of active page (if the user is scrolling)
        final View activeChild = layoutManager.findViewByPosition(activePosition);
        int left = activeChild.getLeft();
        int width = activeChild.getWidth();

        // on swipe the active item will be positioned from [-width, 0]
        // interpolate offset for smooth animation
        float progress = mInterpolator.getInterpolation(left * -1 / (float) width);

        drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress, itemCount);
    }

    private void drawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) {
        mPaint.setColor(Color.GRAY);

        // width of item indicator including padding
        final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

        float start = indicatorStartX;
        for (int i = 0; i < itemCount; i++) {
            // draw the line for every item
            c.drawCircle(start + mIndicatorItemLength,indicatorPosY,itemWidth/6,mPaint);
          //  c.drawLine(start, indicatorPosY, start + mIndicatorItemLength, indicatorPosY, mPaint);
            start += itemWidth;
        }
    }

    private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,
                                int highlightPosition, float progress, int itemCount) {
        mPaint.setColor(Color.RED);

        // width of item indicator including padding
        final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

        if (progress == 0F) {
            // no swipe, draw a normal indicator
            float highlightStart = indicatorStartX + itemWidth * highlightPosition;
         /*   c.drawLine(highlightStart, indicatorPosY,
                    highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);
        */
            c.drawCircle(highlightStart,indicatorPosY,itemWidth/6,mPaint);

        } else {
            float highlightStart = indicatorStartX + itemWidth * highlightPosition;
            // calculate partial highlight
            float partialLength = mIndicatorItemLength * progress;
            c.drawCircle(highlightStart + mIndicatorItemLength,indicatorPosY,itemWidth/6,mPaint);

            // draw the cut off highlight
           /* c.drawLine(highlightStart + partialLength, indicatorPosY,
                    highlightStart + mIndicatorItemLength, indicatorPosY, mPaint);
*/
            // draw the highlight overlapping to the next item as well
           /* if (highlightPosition < itemCount - 1) {
                highlightStart += itemWidth;
                *//*c.drawLine(highlightStart, indicatorPosY,
                        highlightStart + partialLength, indicatorPosY, mPaint);*//*
                c.drawCircle(highlightStart ,indicatorPosY,itemWidth/4,mPaint);

            }*/
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = mIndicatorHeight;
    }
}

And Apply it to the recyclerview as follows

//for horizontal scroll for recycler view 
 LinearLayoutManager linearLayoutManager
                = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
recyclerview.setLayoutManager(linearLayoutManager);
recyclerview.addItemDecoration(new CirclePagerIndicatorDecoration());
shobhan
  • 252
  • 3
  • 10
  • @shoban,how can i show vertically.Please check this link https://stackoverflow.com/questions/47708974/create-custom-scroll-bar-in-android-wear-2-0 – kavie Dec 11 '17 at 06:57
  • instead of high light color how can i set drawable to that – blackHawk May 12 '18 at 22:04
  • How do you add this type of multi-colored indicator? [![enter image description here][1]][1] [1]: https://i.stack.imgur.com/xqsjN.png – bkm Jun 14 '18 at 20:37
  • suppose we have recyclerview stackfromend true then how to handle the viewpagerindicator in reverse order? – android dev Dec 16 '22 at 14:04
6

in case of anyone need, I wrote my own library for this (after a lot of searching): RecyclerView indicator. Here's how you do it:

 <com.kingfisher.easyviewindicator.RecyclerViewIndicator
    android:id="@+id/circleIndicator"
    android:layout_width="match_parent"
    android:layout_height="20dp"
    app:avi_animation_enable="true"
    app:avi_drawable="@drawable/blue_radius"
    app:avi_drawable_unselected="@drawable/gray_radius"
    app:avi_height="10dp"
    app:avi_margin="10dp"
    app:avi_width="10dp"
    app:layout_constraintTop_toBottomOf="@+id/recyclerView">

</com.kingfisher.easyviewindicator.RecyclerViewIndicator>
// In code:
recyclerView.setAdapter(new TestAdapter());
recyclerViewIndicator.setRecyclerView(recyclerView);
Kingfisher Phuoc
  • 8,052
  • 9
  • 46
  • 86
3

there is actually a very good library for this purpose. just add this to your gradle dependencies

implementation "ru.tinkoff.scrollingpagerindicator:scrollingpagerindicator:1.0.6"

then add the layout

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<ru.tinkoff.scrollingpagerindicator.ScrollingPagerIndicator
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

then add the indicator to your recyclerview like this

RecyclerView recyclerView = findViewById(R.id.recycler);
LayoutManager layoutManager = new LinearLayoutManager(this, 
LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(layoutManager);
DemoRecyclerViewAdapter recyclerAdapter = new DemoRecyclerViewAdapter();
recyclerView.setAdapter(recyclerAdapter);

ScrollingPagerIndicator recyclerIndicator = findViewById(R.id.indicator);
recyclerIndicator.attachToRecyclerView(recyclerView);
A.sobhdel
  • 234
  • 4
  • 11
  • Check their GitHub repo for latest version: https://github.com/Tinkoff/ScrollingPagerIndicator, only recent versions (currently 1.2.2) supports androidx recycler views – Lexon Li Aug 23 '22 at 14:26
3

I made adjustments to CirclePagerIndicatorDecoration, so it will support RTL (Right-to-Left) languages. It took me a couple of a days, hope it will help someone:

    import android.content.res.Resources;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.view.View;
    import android.view.animation.AccelerateDecelerateInterpolator;
    import android.view.animation.Interpolator;
    
    import androidx.annotation.ColorInt;
    import androidx.recyclerview.widget.LinearLayoutManager;
    import androidx.recyclerview.widget.RecyclerView;
    import java.util.Locale;
 
    public class CirclePagerIndicatorDecoration extends RecyclerView.ItemDecoration {
        private int colorActive = 0xDE000000;
        private int colorInactive = 0x33000000;
    
        private static final float DP = Resources.getSystem().getDisplayMetrics().density;
    
        /**
         * Height of the space the indicator takes up at the bottom of the view.
         */
        private final int mIndicatorHeight = (int) (DP * 16);
    
        /**
         * Indicator stroke width.
         */
        private final float mIndicatorStrokeWidth = DP * 4;
    
        /**
         * Indicator width.
         */
        private final float mIndicatorItemLength = DP * 4;
        /**
         * Padding between indicators.
         */
        private final float mIndicatorItemPadding = DP * 8;
    
        /**
         * Some more natural animation interpolation
         */
        private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
    
        private final Paint mPaint = new Paint();
    
        public CirclePagerIndicatorDecoration(@ColorInt int colorInactive) {
    
            mPaint.setStrokeWidth(mIndicatorStrokeWidth);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setAntiAlias(true);
            colorActive = colorInactive;
        }
    
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
    
            int itemCount = parent.getAdapter().getItemCount();
    
            // center horizontally, calculate width and subtract half from center
            float totalLength = mIndicatorItemLength * itemCount;
            float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;
            float indicatorTotalWidth = totalLength + paddingBetweenItems;
            float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;
    
            // center vertically in the allotted space
            float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;
    
            drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);
    
            // find active page (which should be highlighted)
            LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
            int activePosition;
            if (isRtlLanguage()) {
                activePosition = layoutManager.findLastVisibleItemPosition();
            } else {
                activePosition = layoutManager.findFirstVisibleItemPosition();
            }
    
            if (activePosition == RecyclerView.NO_POSITION) {
                return;
            }
    
            // find offset of active page (if the user is scrolling)
            final View activeChild = layoutManager.findViewByPosition(activePosition);
            int left = activeChild.getLeft();
            int width = activeChild.getWidth();
            int right = activeChild.getRight();
    
            // on swipe the active item will be positioned from [-width, 0]
            // interpolate offset for smooth animation
            float progress = mInterpolator.getInterpolation(left * -1 / (float) width);
    
            if (isRtlLanguage()) {
                indicatorStartX = (parent.getWidth() + indicatorTotalWidth) / 2F - (mIndicatorItemLength + DP * 4) / 2;
            }
    
    //        float indicatorStartXhl = (parent.getWidth() + indicatorTotalWidth) / 2F - (mIndicatorItemLength + DP * 4) / 2;
            drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress);
        }
    
        private void drawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) {
            mPaint.setColor(colorInactive);
    
            // width of item indicator including padding
            final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;
    
            float start = indicatorStartX;
            for (int i = 0; i < itemCount; i++) {
    
                c.drawCircle(start, indicatorPosY, mIndicatorItemLength / 2F, mPaint);
    
                start += itemWidth;
            }
        }
    
        private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,
                                    int highlightPosition, float progress) {
            mPaint.setColor(colorActive);
    
            // width of item indicator including padding
            final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;
    
            if (progress == 0F) {
                // no swipe, draw a normal indicator
                float highlightStart;
                if (isRtlLanguage()) {
                    highlightStart = indicatorStartX - itemWidth * highlightPosition;
                } else {
                    highlightStart = indicatorStartX + itemWidth * highlightPosition;
                }
    
                c.drawCircle(highlightStart, indicatorPosY, mIndicatorItemLength / 2F, mPaint);
    
            } else {
    
                float highlightStart;
                if (isRtlLanguage()) {
                    highlightStart = indicatorStartX - itemWidth * highlightPosition;
                } else {
                    highlightStart = indicatorStartX + itemWidth * highlightPosition;
                }
    
                float partialLength = mIndicatorItemLength * progress + mIndicatorItemPadding * progress;
    
                c.drawCircle(highlightStart + partialLength, indicatorPosY, mIndicatorItemLength / 2F, mPaint);
            }
        }
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.bottom = mIndicatorHeight;
        }
    }


//The method that checks if it's RTL language:
    private boolean isRtlLanguage() {
            String deviceLanguage = Locale.getDefault().getLanguage();
            return (deviceLanguage.contains("iw") || deviceLanguage.contains("ar")); //You can change here to your specific language
        }
MorZa
  • 2,215
  • 18
  • 33
1

Nowadays you could use ViewPager2.

It basically wraps a RecyclerView (so you will be using a RecyclerView.Adapter) but it allows the attachment of a TabLayout with the help of a TabLayoutMediator.

The TabLayout can then be styled to act like a dot page indicator. See for example How do you create an Android View Pager with a dots indicator?

Oderik
  • 2,242
  • 2
  • 16
  • 25
1

You can add PageIndicator.java class to your .xml below recycler view widget

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/color_white">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_notice_board_cards"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimen_16dp"
        android:orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_high_lights" />

    <com.abc.widget.PageIndicator
        android:id="@+id/ll_image_indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|start"
        android:layout_marginStart="@dimen/dimen_16dp"
        android:layout_marginTop="@dimen/dimen_12dp"
        android:layout_marginEnd="@dimen/dimen_16dp"
        android:gravity="center"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/tv_cards_count"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/rv_notice_board_cards"
        app:layout_constraintVertical_bias="0.0" />

</androidx.constraintlayout.widget.ConstraintLayout>

The PageIndicator.java class like below

public class PageIndicator extends LinearLayout {
  private ImageView[] imageIndications;

  public PageIndicator(Context context) {
    super(context);
  }

  public PageIndicator(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  /**
   * method to create the pageIndicator
   */
  public void createPageIndicator(int pageCount, int focusedPageDrawable,
      int unFocusedPageDrawable) {
    imageIndications = new ImageView[pageCount];
    ImageView indicatorImageView;
    for (int i = 0; i < pageCount; i++) {
      indicatorImageView = new ImageView(getContext());
      int size = BaseUtils.INSTANCE.getDensityPixelValue(getContext(), 8);
      final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(size, size);
      params.setMargins(8, 0, 4, 0);
      indicatorImageView.setLayoutParams(params);
      // method to change the page icon
      changePageIcon(i, 0, indicatorImageView, focusedPageDrawable, unFocusedPageDrawable);
      imageIndications[i] = indicatorImageView;
      this.addView(indicatorImageView);
    }
  }

  /**
   * method to handle the PageChangeListener for ViewPager
   *
   * @param size the total number of images available for product
   * @param position the current position of ViewPager
   * @param focusedPageDrawable
   * @param unFocusedPageDrawable
   */
  public void handleViewPagerScroll(int size, int position, int focusedPageDrawable,
      int unFocusedPageDrawable) {
    for (int i = 0; i < size && i < imageIndications.length; i++) {
      changePageIcon(position, i, imageIndications[i], focusedPageDrawable, unFocusedPageDrawable);
      imageIndications[i].getLayoutParams().width = imageIndications[i].getDrawable().getIntrinsicWidth();
    }
  }

  /**
   * method to change the page icon
   *
   * @param position
   * @param indicatorImageView
   * @param focusedPageDrawable
   * @param unFocusedPageDrawable
   */
  private void changePageIcon(int position, int pageIndex, ImageView indicatorImageView,
      int focusedPageDrawable, int unFocusedPageDrawable) {
    if (pageIndex == position) {
      if (focusedPageDrawable != 0) {
        indicatorImageView.setImageResource(focusedPageDrawable);
      } else {
        indicatorImageView.setImageResource(R.drawable.rounded_style_blue);
      }
    } else {
      if (unFocusedPageDrawable != 0) {
        indicatorImageView.setImageResource(unFocusedPageDrawable);
      } else {
        indicatorImageView.setImageResource(R.drawable.rounded_style2);
      }
    }
  }
}

In your recycler view adapter, you can add override below with interface

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
    super.onAttachedToRecyclerView(recyclerView)
    val manager = recyclerView.layoutManager
    if (manager is LinearLayoutManager && itemCount > 0) {
      recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
          super.onScrolled(recyclerView, dx, dy)
          val visiblePosition: Int = manager.findFirstCompletelyVisibleItemPosition()
          if (visiblePosition > -1) {
            iFragmentCommunicator.updateCount(visiblePosition)
          }
        }
      })
    }
  }

  interface IFragmentCommunicator {
    fun updateCount(count: Int)
  }

and last in your Activity or Fragment you can add below code initially to call Page Indicator method.

private fun initCircularIndicator() {
    val margin =
      (screenWidth - (0.8F * screenWidth).toInt()) / 2 - 8)
    (mBinder?.llImageIndicator?.layoutParams as? FrameLayout.LayoutParams)?.apply {
      setMargins(margin, 0, 0, 32))
    }
    mBinder?.llImageIndicator?.requestLayout()
    mBinder?.llImageIndicator?.run {
      removeAllViews()
      createPageIndicator(
        8,
        R.drawable.selected_item_indicator,
        0
      )
      handleViewPagerScroll(8, 0, R.drawable.selected_blue_item_indicator, 0)
    }
  }

In above code you can add your drawable for selected_blue_item_indicator like below

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/color_brand" />

    <corners android:radius="@dimen/four_dp" />

    <size
        android:width="@dimen/sixteen_dp"
        android:height="@dimen/ten_dp" />
</shape>

and once you override the updateCount() method in Activity or Fragment call handleViewPagerScroll() method of Page Indicator

override fun updateCount(count: Int) {
    mBinder?.llImageIndicator?.handleViewPagerScroll(
      8,
      count,
      R.drawable.selected_blue_item_indicator,
      0
    )
  }

that's all you have to do.

Vinod Pattanshetti
  • 2,465
  • 3
  • 22
  • 36
1

Shoban's answer did not work for me, so here is how I got it to work.

Keep in mind that I removed the animations as they were not necessary for my usecase. The result looks exactly like the screenshot attached in this question.

Also, in order to allow scrolling only one element at a time, I used PagerSnapHelper() to achieve the desired result.

PagerSnapHelper helper = new PagerSnapHelper();

helper.attachToRecyclerView(recyclerView);

recyclerView.addItemDecoration(new CirclePagerIndicatorDecoration());

Here is my code for the decorator:

public class CirclePagerIndicatorDecoration extends RecyclerView.ItemDecoration {

private final int colorActive = 0xFFFFFFFF;
private final int colorInactive = 0x66FFFFFF;
private final int circleRadius = 8;

private static final float DP = Resources.getSystem().getDisplayMetrics().density;

/**
 * Height of the space the indicator takes up at the bottom of the view.
 */
private final int mIndicatorHeight = (int) (DP * 16);

/**
 * Indicator stroke width.
 */
private final float mIndicatorStrokeWidth = DP * 2;

/**
 * Indicator width.
 */
private final float mIndicatorItemLength = DP * 16;
/**
 * Padding between indicators.
 */
private final float mIndicatorItemPadding = DP * 4;

/**
 * Some more natural animation interpolation
 */
private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();

private final Paint mPaint = new Paint();

public CirclePagerIndicatorDecoration() {
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    mPaint.setStrokeWidth(mIndicatorStrokeWidth);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setAntiAlias(true);
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);

    int itemCount = parent.getAdapter().getItemCount();

    // center horizontally, calculate width and subtract half from center
    float totalLength = mIndicatorItemLength * itemCount;
    float paddingBetweenItems = Math.max(0, itemCount - 1) * mIndicatorItemPadding;
    float indicatorTotalWidth = totalLength + paddingBetweenItems;
    float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2F;

    // center vertically in the allotted space
    float indicatorPosY = parent.getHeight() - mIndicatorHeight / 2F;

    drawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);

    // find active page (which should be highlighted)
    LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
    int activePosition = layoutManager.findFirstVisibleItemPosition();
    if (activePosition == RecyclerView.NO_POSITION) {
        return;
    }

    // find offset of active page (if the user is scrolling)
    final View activeChild = layoutManager.findViewByPosition(activePosition);
    int left = activeChild.getLeft();
    int width = activeChild.getWidth();

    // on swipe the active item will be positioned from [-width, 0]
    // interpolate offset for smooth animation
    float progress = mInterpolator.getInterpolation(left * -1 / (float) width);

    drawHighlights(c, indicatorStartX, indicatorPosY, activePosition, progress, itemCount);
}

private void drawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) {
    mPaint.setColor(colorInactive);

    // width of item indicator including padding
    final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

    float start = indicatorStartX;
    for (int i = 0; i < itemCount; i++) {
        c.drawCircle(start, indicatorPosY, circleRadius, mPaint);
        start += itemWidth;
    }
}

private void drawHighlights(Canvas c, float indicatorStartX, float indicatorPosY,
                            int highlightPosition, float progress, int itemCount) {
    mPaint.setColor(colorActive);

    //width of item indicator including padding
    final float itemWidth = mIndicatorItemLength + mIndicatorItemPadding;

    float highlightStart = indicatorStartX + itemWidth * highlightPosition;

    if (progress == 0F) {
        // no swipe, draw a normal indicator
        c.drawCircle(highlightStart, indicatorPosY, circleRadius, mPaint);
    }
}

@Override
public void getItemOffsets(@NotNull Rect outRect,
                           @NotNull View view,
                           @NotNull RecyclerView parent,
                           @NotNull RecyclerView.State state) {

    super.getItemOffsets(outRect, view, parent, state);
    outRect.bottom = mIndicatorHeight;
}
Ana Ilies
  • 41
  • 2
1

In case anyone looking for Xamarin.Android solution, this is one based on answers above:

public class RecyclerViewPageIndicator: RecyclerView.ItemDecoration
{
    private readonly int size = ConvertDpToPixels(8);
    private readonly int spacing = ConvertDpToPixels(10);
    private readonly AccelerateDecelerateInterpolator interpolator;
    private readonly Paint paint;

    public RecyclerViewPageIndicator()
    {
        interpolator = new AccelerateDecelerateInterpolator();
        paint = new Paint
        {
            AntiAlias = true,
            StrokeCap = Paint.Cap.Round,
            StrokeWidth = ConvertDpToPixels(2)
        };
        paint.SetStyle(Paint.Style.Fill);
    }

    public override void OnDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)
    {
        base.OnDrawOver(c, parent, state);

        var itemCount = parent.GetAdapter()?.ItemCount ?? 0;

        var totalWidth = size * itemCount;
        var totalSpacingWidth = Math.Max(0, itemCount - 1) * spacing;
        var indicatorWidth = totalWidth + totalSpacingWidth;

        var indicatorStartX = (parent.Width - indicatorWidth + size) / 2f;
        var indicatorPosY = parent.Height - size * 2;

        DrawInactiveIndicators(c, indicatorStartX, indicatorPosY, itemCount);

        var layoutManager = (LinearLayoutManager)parent.GetLayoutManager();
        var position = layoutManager.FindFirstVisibleItemPosition();
        if (position == RecyclerView.NoPosition)
        {
            return;
        }

        var activeChild = layoutManager.FindViewByPosition(position);
        var progress = interpolator.GetInterpolation(activeChild.Left * -1 / (float)activeChild.Width);

        DrawHighlights(c, indicatorStartX, indicatorPosY, position, progress);
    }

    private void DrawInactiveIndicators(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount)
    {
        paint.Color = Color.ParseColor("#D2E7F6");

        var itemWidth = size + spacing;
        var drawPosition = indicatorStartX;

        for (var i = 0; i < itemCount; i++)
        {
            c.DrawCircle(drawPosition, indicatorPosY, size / 2f, paint);
            drawPosition += itemWidth;
        }
    }

    private void DrawHighlights(Canvas c, float indicatorStartX, float indicatorPosY, int position, float progress)
    {
        paint.Color = Color.ParseColor("#007AFF");

        var itemWidth = size + spacing;

        if (progress == 0)
        {
            var highlightStart = indicatorStartX + itemWidth * position;
            c.DrawCircle(highlightStart, indicatorPosY, size / 2f, paint);
        }
        else
        {
            var highlightStart = indicatorStartX + itemWidth * position;
            var partialLength = size * progress + spacing * progress;
            c.DrawCircle(highlightStart + partialLength, indicatorPosY, size / 2f, paint);
        }
    }

    public override void GetItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
    {
        base.GetItemOffsets(outRect, view, parent, state);
        outRect.Bottom = size * 3;
    }

    private static int ConvertDpToPixels(int dpSize)
    {
        var scale = Resources.System.DisplayMetrics.Density;
        return (int)(dpSize * scale + 0.5f);
    }

}
Evgeniy Vaganov
  • 345
  • 5
  • 6
-1

First you have to create another RecyclerView for the circles and put this code in the OnScrollListener of the first RecyclerView.

int[] firstVisibleItemPositions = new int[appList.size()];
int[] lastVisibleItemPositions = new int[appList.size()];

int position = ((StaggeredGridLayoutManager) listView.getLayoutManager()).findFirstVisibleItemPositions(firstVisibleItemPositions)[0];
View currentView = circlesList.getChildAt(position);
int lastPosition = ((StaggeredGridLayoutManager) listView.getLayoutManager()).findLastVisibleItemPositions(lastVisibleItemPositions)[0];
View lastView = circlesList.getChildAt(lastPosition);
ImageView circle;
if (dx>0) {
    if (currentView != null && lastPosition != position) {
        circle = (ImageView) currentView.findViewById(R.id.img);
        circle.setImageResource(R.drawable.empty_circle);
    }
    if (lastView != null && lastPosition != position) {
        circle = (ImageView) lastView.findViewById(R.id.img);
        circle.setImageResource(R.drawable.selected_circle);
    }
}else if (dx<0){
    if (currentView != null && lastPosition != position) {
        circle = (ImageView) currentView.findViewById(R.id.img);
        circle.setImageResource(R.drawable.selected_circle);
    }
    if (lastView != null && lastPosition != position) {
        circle = (ImageView) lastView.findViewById(R.id.img);
        circle.setImageResource(R.drawable.empty_circle);
    }
}
David Medenjak
  • 33,993
  • 14
  • 106
  • 134
Hanaa Mohamed
  • 107
  • 2
  • 11