Recently, I encountered two interface switches and the linkage of the dot at the bottom of the design, the first thing I think of is the linkage of ViewPager2+Fragment, then how to link the dot at the bottom? Later find, found last year to write a draft, is after reading hongyang’s article ready to summarize, but is the Java version, which is used in ViewPager+ImageView using PagerAdapter adapter and small dot linkage. Now to change that, use the ViewPager2+Fragment to use the FragmentStateAdapter adapter to interconnect with the dot.

This video is mainly about customizing IndicatorView and realizing linkage with ViewPager2+Fragment. In order to facilitate realization, only one picture is included in the Fragment in the following Demo

Realize the effect drawing

You want to switch between the two pages, as shown below

Their simple implementation of the effect

You can slide the layout, or click on the dot to switch the layout. Here, the image is placed in the Fragment, and you can change the layout content in the Fragment according to your own layout

1. Draw multiple circles

1.1 First calculate the coordinates of multiple centers

How to calculate the specific coordinates is shown in the figure:

The abscissa of the first center is: the radius plus the width of the edge

The abscess of the second center is: the abscess of the first point +(RADIUS +mStrokeWidth)*2+mSpace

Define Indicator class to record center coordinates

// Center coordinates
inner class Indicator {
    // Center x coordinates
    var cx = 0f
    // The center of the circle is y
    var cy = 0f
}
Copy the code
// Calculate the coordinates of the center of the circle plus
var mIndicators = mutableListOf<Indicator>()
//border- brush width
private var mStrokeWidth = 0
// The spacing between circles
private var mSpace = 0
/ / radius
private var mRadius = 0
Copy the code
// Count the center of each circle and place it in the set
private fun measureIndicator(a) {
    mIndicators.clear()
    // The temporary variable records the abscissa of the center of the circle
    var cx = 0f
    //[) left closed right open
    for (i in 0 until mCount) {
        val indicator = Indicator()
        if (i == 0) {
            // The abscissa of the first circle is the radius + the brush width
            cx = mRadius + mStrokeWidth.toFloat()
        } else {
            cx += (mRadius + mStrokeWidth) * 2 + mSpace.toFloat()
        }
        indicator.cx = cx
        // In the middle of the layout
        indicator.cy = measuredHeight / 2.toFloat()
        mIndicators.add(indicator)
    }
}
Copy the code

1.2 draw round

override fun onDraw(canvas: Canvas?). {
    super.onDraw(canvas)
    // mindicators. indices = I in 0... // mindicators. indices = I in 0... mIndicators.size
    for (i in mIndicators.indices) {
        val indicator = mIndicators[i]
        val x = indicator.cx
        val y = indicator.cy
        // This point is the currently selected point, set the brush color and style, draw a solid circle
        if (mSelectPosition == i) {                  
            mCirclePaint.style = Paint.Style.FILL
            mCirclePaint.color = mSelectColor
        } else {          
            // Set the brush color and style for the unselected point and draw a hollow circlemCirclePaint.color = mDotNormalColor mCirclePaint.style = Paint.Style.STROKE } canvas? .drawCircle(x, y, mRadius.toFloat(), mCirclePaint) } }Copy the code

1.3 Calculate the size of the View

Look at the figure at the beginning of this article to see how the total width of the View is calculated

You can customize the height, but the height of the View should be at least greater than radius*2 of the circle,

/** * Calculates the size of the View, which is the size of the entire custom control */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val width = (mRadius + mStrokeWidth) * 2 * mCount + mSpace * (mCount - 1)   
    //widthMeasureSpec and heightMeasureSpec are widths and heights set in XML
    if(heightMeasureSpec<2 * mRadius){
        // Determines the current View size
        setMeasuredDimension(width, 2 * mRadius)
    }else{
        // Determines the current View size
        setMeasuredDimension(width, heightMeasureSpec)
    }
	// Measure the position of each dot here
    measureIndicator()
}
Copy the code

2. Associate with ViewPager2

2.1 Setting the association between CircleIndicatorView and ViewPager2

// The ViewPager2 to associate
private var mViewPager: ViewPager2? = null
// Indicator number
private var mCount = 0
Copy the code
fun setUpWithViewPager(viewPager: ViewPager2) {
   
    mViewPager = viewPager
	// Listen on the ViewPager switch interface
    viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            mSelectPosition = position
            invalidate()
        }
    })
    // Get the real value
    val count: Int = (viewPager.adapter as MyAdapter).itemCount
    setCount(count)
}
Copy the code
private fun setCount(count: Int) {
    mCount = count
    invalidate()
}
Copy the code

3 Indicator Click events

Objective: To switch the ViewPager to the corresponding interface by clicking the dot

Monitor ACTION_DOWN event to get the coordinates of the click screen and compare with the position of the drawn circle. If the click area is within the range of the circle, click the Indicator. After clicking, switch ViewPager to the corresponding interface.

// Handle the click event to see if the click position is within the dot
override fun onTouchEvent(event: MotionEvent?).: Boolean {
    var xPoint = 0f
    var yPoint = 0f
    when(event? .action) { MotionEvent.ACTION_DOWN -> { xPoint = event.x yPoint = event.y handleActionDown(xPoint, yPoint) } }return super.onTouchEvent(event)
}
Copy the code

// Handle the click event
private fun handleActionDown(xDis: Float, yDis: Float) {
    for (i in mIndicators.indices) {
        val indicator = mIndicators[i]
        // The dot's coordinates are (cx,cy) and its radius is mRadius and its edge width is mStrokeWidth
        if ( xDis >= indicator.cx - (mRadius + mStrokeWidth) && xDis < indicator.cx + mRadius + mStrokeWidth
            // && yDis >= yDis - (indicator.cy + mStrokeWidth) && yDis < indicator.cy + mRadius + mStrokeWidth
            //TODO(this is different from the reference article, why it is so)
            && yDis >= indicator.cy - (mRadius   + mStrokeWidth) && yDis < indicator.cy + mRadius + mStrokeWidth) {
            // Click Indicator is found
            / / switch ViewPagermViewPager? .setCurrentItem(i,false)
            // Click the dot's callback alone
            if(mOnIndicatorClickListener ! =null) { mOnIndicatorClickListener!! .onSelected(i) } } } }Copy the code
private var mOnIndicatorClickListener: OnIndicatorClickListener? = null

interface OnIndicatorClickListener {
    fun onSelected(position: Int)
}

fun setOnIndicatorClickListener(onIndicatorClickListener: OnIndicatorClickListener) {
    mOnIndicatorClickListener = onIndicatorClickListener
}
Copy the code

How do I need to do something else when I click on the dot? I can write a special listener to do the callback

4. Customize attributes

If you want to be able to edit some of the properties of the control dot directly in an XML file, you can customize the properties.

Create a Value Resource file in the VALUES folder

<resources>

    <declare-styleable name="CircleIndicatorView">
        <! -- Circle radius -->
        <attr name="indicatorRadius" format="dimension" />
        <! Brush width, i.e. the edge of the circle -->
        <attr name="indicatorBorderWidth" format="dimension" />
        <! -- Spacing between circles -->
        <attr name="indicatorSpace" format="dimension" />
        <! -- Unselected color of the circle -->
        <attr name="indicatorColor" format="color" />
        <! -- circle selected color -->
        <attr name="indicatorSelectedColor" format="color" />

    </declare-styleable>
</resources>
Copy the code

Then obtain the attribute value assignment in the constructor of CircleIndicatorView

init {
    // Brush Settings
    mCirclePaint.isDither = true
    mCirclePaint.isAntiAlias = true
    mCirclePaint.style = Paint.Style.FILL_AND_STROKE
    mCirclePaint.color = mDotNormalColor
    mCirclePaint.strokeWidth = mStrokeWidth.toFloat() // Brush width

    getAttr(context, attrs!!)

}
Copy the code
// Get custom attributes
private fun getAttr(
    context: Context,
    attrs: AttributeSet
) {
    val typedArray =
    context.obtainStyledAttributes(attrs, R.styleable.CircleIndicatorView)
    // Radius (6dp if not set)
    mRadius = typedArray.getDimensionPixelSize(
        R.styleable.CircleIndicatorView_indicatorRadius,
        DisplayUtils.dpToPx(6))// Brush width (2dp if not set)
    mStrokeWidth = typedArray.getDimensionPixelSize(
        R.styleable.CircleIndicatorView_indicatorBorderWidth,
        DisplayUtils.dpToPx(2))// Circle spacing (5dp if not set)
    mSpace = typedArray.getDimensionPixelSize(
        R.styleable.CircleIndicatorView_indicatorSpace,
        DisplayUtils.dpToPx(5))// The color of the circle is not selected
    mDotNormalColor = typedArray.getColor(
        R.styleable.CircleIndicatorView_indicatorColor,
        Color.BLACK
    )
    // circle the color
    mSelectColor = typedArray.getColor(
        R.styleable.CircleIndicatorView_indicatorSelectedColor,
        Color.WHITE
    )
}
Copy the code

DisplayUtils is a utility class that converts dp to PX

object DisplayUtils {
    fun dpToPx(dp: Int): Int {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            dp.toFloat(),
            Resources.getSystem().displayMetrics
        ).toInt()
    }

    fun pxToDp(px: Float): Int {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_PX,
            px,
            Resources.getSystem().displayMetrics
        ).toInt()
    }
}
Copy the code

5. Use

Add the control to the XML layout


      
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

   <androidx.viewpager2.widget.ViewPager2
       android:id="@+id/mViewpager"
       android:layout_width="match_parent"
       android:layout_height="200dp"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
   <com.myfittinglife.viewpager2demo.CircleIndicatorView
       android:id="@+id/circleIndicatorView"
       android:layout_width="wrap_content"
       android:layout_height="40dp"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toBottomOf="@id/mViewpager"
       app:indicatorSpace="10dp"
       app:indicatorColor="@color/colorAccent"
       app:indicatorSelectedColor="@color/colorPrimary"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Bind directly within the Activity

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?). {
        mViewpager.adapter = MyAdapter(this)
        circleIndicatorView.setUpWithViewPager(mViewpager)
    }
}   
Copy the code

6. Interworking with ViewPager2+Fragment

Here’s how to use ViewPager2 with fragments

6.1 Setting up an Adapter

The adapter inherits from FragmentStateAdapter

class MyAdapter(fragment:FragmentActivity): FragmentStateAdapter(fragment) {

    var fragments = mutableListOf<Fragment>()

    // Create a Fragment.
    init {
        fragments.add(MyFragment.newInstance("1"))
        fragments.add(MyFragment.newInstance("2"))
        fragments.add(MyFragment.newInstance("3"))
        fragments.add(MyFragment.newInstance("4"))}override fun getItemCount(a): Int {
        return fragments.size
    }

    override fun createFragment(position: Int): Fragment {
        return fragments[position]
    }
}
Copy the code

6.2 Binding an Adapter

mViewpager.adapter = MyAdapter(this)
Copy the code

See the reference article at the bottom for more usage of ViewPager2

7. To summarize

So we’re going to use custom View and ViewPager2. SetCurrentItem (I, false) ¶ The Touch of a custom View responds to the click event and uses the viewPager.setCurrentItem(I, false) method to link with the ViewPager. ViewPager switch drive dots linkage. But is through the ViewPager registerOnPageChangeCallback () by implementing onPageSelected () method and then redraw the View.

Summarize what you have written, or you will soon forget it. If this article has helped you, please don’t forget the triple link, and if there are inappropriate places, please mention them. See you in the next article.

Making the address

Refer to the article

Android custom View implementation of a multi-functional IndicatorView

Learn if you can’t! Take a closer look at ViewPager2