Last active
April 22, 2025 07:42
-
-
Save Dev-Husnain/86e53b36bc7094166be1e0320cdb01d9 to your computer and use it in GitHub Desktop.
DotsIndicatorView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //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