Skip to content

Instantly share code, notes, and snippets.

@Dev-Husnain
Last active April 22, 2025 07:42
Show Gist options
  • Select an option

  • Save Dev-Husnain/86e53b36bc7094166be1e0320cdb01d9 to your computer and use it in GitHub Desktop.

Select an option

Save Dev-Husnain/86e53b36bc7094166be1e0320cdb01d9 to your computer and use it in GitHub Desktop.
DotsIndicatorView
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.content.withStyledAttributes
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.viewpager2.widget.ViewPager2
class DotsIndicatorView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
enum class Shape { CIRCLE, RECTANGLE }
private var dotShapeSelected = Shape.CIRCLE
private var dotShapeUnselected = Shape.CIRCLE
private var colorSelected = Color.BLACK
private var colorUnselected = Color.GRAY
private var cornerRadii = FloatArray(8)
private var numberOfDots = 0
private var selectedDot = 0
private var dotWidth = 8.dp
private var dotHeight = 8.dp
private var dotSpacing = 6.dp
init {
orientation = HORIZONTAL
context.withStyledAttributes(attrs, R.styleable.DotsIndicatorView) {
dotShapeSelected = Shape.entries.toTypedArray()[getInt(
R.styleable.DotsIndicatorView_dotShapeSelected,
0
)]
dotShapeUnselected = Shape.entries.toTypedArray()[getInt(
R.styleable.DotsIndicatorView_dotShapeUnselected,
0
)]
colorSelected = getColor(R.styleable.DotsIndicatorView_colorSelected, Color.BLACK)
colorUnselected = getColor(R.styleable.DotsIndicatorView_colorUnselected, Color.GRAY)
numberOfDots = getInt(R.styleable.DotsIndicatorView_numberOfDots, 0)
selectedDot = getInt(R.styleable.DotsIndicatorView_selectedDot, 0)
dotWidth = getDimensionPixelSize(R.styleable.DotsIndicatorView_dotWidth, 8.dp)
dotHeight = getDimensionPixelSize(R.styleable.DotsIndicatorView_dotHeight, 8.dp)
dotSpacing = getDimensionPixelSize(R.styleable.DotsIndicatorView_dotSpacing, 6.dp)
val radiusAll = getDimension(R.styleable.DotsIndicatorView_cornerRadiusAll, -1f)
if (radiusAll != -1f) {
cornerRadii = FloatArray(8) { radiusAll }
} else {
cornerRadii[0] =
getDimension(R.styleable.DotsIndicatorView_cornersRadiusTopLeft, 0f)
cornerRadii[1] = cornerRadii[0]
cornerRadii[2] =
getDimension(R.styleable.DotsIndicatorView_cornersRadiusTopRight, 0f)
cornerRadii[3] = cornerRadii[2]
cornerRadii[4] =
getDimension(R.styleable.DotsIndicatorView_cornersRadiusBottomRight, 0f)
cornerRadii[5] = cornerRadii[4]
cornerRadii[6] =
getDimension(R.styleable.DotsIndicatorView_cornersRadiusBottomLeft, 0f)
cornerRadii[7] = cornerRadii[6]
}
}
refreshDots()
}
private fun refreshDots() {
removeAllViews()
for (i in 0 until numberOfDots) {
val isSelected = i == selectedDot
val shape = if (isSelected) dotShapeSelected else dotShapeUnselected
val color = if (isSelected) colorSelected else colorUnselected
val container = FrameLayout(context).apply {
layoutParams = MarginLayoutParams(dotWidth, dotHeight).apply {
if (i < numberOfDots - 1) {
marginEnd = dotSpacing
}
}
}
val dotView = View(context).apply {
layoutParams = FrameLayout.LayoutParams(dotWidth, dotHeight, Gravity.CENTER)
scaleX = 1f
scaleY = 1f
background = createDotDrawable(shape, color)
}
container.addView(dotView)
addView(container)
}
}
private fun createDotDrawable(shape: Shape, color: Int): GradientDrawable {
return GradientDrawable().apply {
setColor(color) // Set the background color
this.shape = GradientDrawable.RECTANGLE
if (shape == Shape.CIRCLE) {
cornerRadius = 100f // for circle shape
} else {
// For the rectangle shape, use cornerRadii array if it was set
cornerRadii = this@DotsIndicatorView.cornerRadii
}
}
}
fun setSelectedDot(index: Int) {
if (index == selectedDot || index < 0 || index >= numberOfDots) return
val previousDot = getChildAt(selectedDot)
val newDot = getChildAt(index)
animateDot(previousDot, false) // unselect previous
animateDot(newDot, true) // select new
selectedDot = index
refreshDots()
}
fun attachTo(view: View) {
when (view) {
is ViewPager2 -> attachToViewPager(view)
is RecyclerView -> attachToRecyclerView(view)
else -> throw IllegalArgumentException("Unsupported view type: ${view::class.java.simpleName}\n\n" +
" Bonus Point: You can manually set index of selected dot and total number of dots by calling dotsIndicatorView.setSelectedDot(index) & dotsIndicatorView.setNumberOfDots(total)\n\n")
}
}
private fun attachToViewPager(viewPager: ViewPager2) {
val adapter = viewPager.adapter
?: throw IllegalArgumentException("ViewPager2 has no adapter set yet.\n\n" +
" Bonus Point: You can manually set index of selected dot and total number of dots by calling dotsIndicatorView.setSelectedDot(index) & dotsIndicatorView.setNumberOfDots(total)\n\n")
setNumberOfDots(adapter.itemCount)
setSelectedDot(viewPager.currentItem)
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
setSelectedDot(position)
}
})
}
private fun attachToRecyclerView(recyclerView: RecyclerView) {
val adapter = recyclerView.adapter
?: throw IllegalArgumentException("ViewPager2 has no adapter set yet.\n\n" +
" Bonus Point: You can manually set index of selected dot and total number of dots by calling dotsIndicatorView.setSelectedDot(index) & dotsIndicatorView.setNumberOfDots(total)\n\n")
setNumberOfDots(adapter.itemCount)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
val layoutManager = rv.layoutManager ?: return
val position = when (layoutManager) {
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
is StaggeredGridLayoutManager -> {
val positions = IntArray(layoutManager.spanCount)
layoutManager.findFirstVisibleItemPositions(positions)
positions.minOrNull() ?: 0
}
else -> {
throw IllegalArgumentException("Unsupported LayoutManager: ${layoutManager::class.java.simpleName}\n\n" +
" Bonus Point: You can manually set index of selected dot and total number of dots by calling dotsIndicatorView.setSelectedDot(index) & dotsIndicatorView.setNumberOfDots(total)\n\n")
}
}
if (position != RecyclerView.NO_POSITION) {
setSelectedDot(position)
}
}
})
}
fun setNumberOfDots(count: Int) {
numberOfDots = count
refreshDots()
}
private fun animateDot(dot: View?, isSelected: Boolean) {
dot ?: return
val fromColor = if (isSelected) colorUnselected else colorSelected
val toColor = if (isSelected) colorSelected else colorUnselected
val shape = if (isSelected) dotShapeSelected else dotShapeUnselected
val drawable = createDotDrawable(shape, fromColor)
// Animate scale
val scale = if (isSelected) 1.2f else 1f
dot.animate()
.scaleX(scale)
.scaleY(scale)
.setDuration(200)
.start()
// Animate color
ValueAnimator.ofArgb(fromColor, toColor).apply {
duration = 200
addUpdateListener {
drawable.setColor(it.animatedValue as Int)
dot.background = drawable
}
start()
}
}
private val Int.dp get() = (this * resources.displayMetrics.density).toInt()
}
<com.example.app.views.DotsIndicatorView
android:id="@+id/dots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/_10sdp"
app:colorSelected="@color/primaryColor"
app:colorUnselected="@color/grayColor"
app:dotShapeSelected="rectangle"
app:dotShapeUnselected="rectangle"
app:dotHeight="@dimen/_8sdp"
app:dotWidth="@dimen/_25sdp"
app:dotSpacing="@dimen/_10sdp"
app:cornerRadiusAll="@dimen/_50sdp"
app:selectedDot="4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:numberOfDots="9" />
//make dot selected in viewpager like
//dots.setSelectedDot(position)
//add this to your attrs
<declare-styleable name="DotsIndicatorView">
<attr name="dotShapeSelected" format="enum">
<enum name="circle" value="0" />
<enum name="rectangle" value="1" />
</attr>
<attr name="dotShapeUnselected" format="enum">
<enum name="circle" value="0" />
<enum name="rectangle" value="1" />
</attr>
<attr name="cornerRadiusAll" format="dimension" />
<attr name="cornersRadiusTopLeft" format="dimension" />
<attr name="cornersRadiusTopRight" format="dimension" />
<attr name="cornersRadiusBottomLeft" format="dimension" />
<attr name="cornersRadiusBottomRight" format="dimension" />
<attr name="colorSelected" format="color" />
<attr name="colorUnselected" format="color" />
<attr name="numberOfDots" format="integer" />
<attr name="selectedDot" format="integer" />
<attr name="dotWidth" format="dimension"/>
<attr name="dotHeight" format="dimension"/>
<attr name="dotSpacing" format="dimension"/>
</declare-styleable>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment