Any idea how to create a page indicator for recyclerview list ?
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?
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.
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 !!!!!
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);
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
}
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());