自定义IndicatorView和ViewPager2+Fragment联动实现

2,146 阅读4分钟

最近又遇到两个界面切换和底部圆点联动的设计,第一眼看到想到的就是ViewPager2+Fragment的联动,那底部的小圆点该怎么联动呢?后来找找,发现去年就写了草稿,是看完鸿洋的文章准备总结一下的,不过是java版本,里面用到的是ViewPager+ImageView使用PagerAdapter适配器和小圆点实现的联动。现在来更改一下,使用ViewPager2+Fragment使用FragmentStateAdapter适配器和小圆点实现联动。

本片主要讲自定义IndicatorView并实现和ViewPager2+Fragment的联动,为了方便实现,以下Demo中的Fragment只放了一张图片

实现效果图

想要两个页面的切换,如下面所示

两个页面切换

自己简单实现的效果

多个页面切换

可以滑动布局,也可以点击小圆点来切换布局,这里的图片是放在Fragment里的,可以根据自己的布局来更改Fragment内的布局内容

1. 绘制多个圆

1.1 先计算多个圆心的坐标

在这里插入图片描述

具体的坐标该如何计算如图所示:

第一个圆心横坐标为:半径加上边的宽度

第二个圆心横坐标为:第一个点横坐标+(radius+mStrokeWidth)*2+mSpace

定义Indicator类来记录圆心坐标

//圆心坐标
inner class Indicator {
    // 圆心x坐标
    var cx = 0f
    // 圆心y 坐标
    var cy = 0f
}
//计算圆心坐标加上的
var mIndicators = mutableListOf<Indicator>()
//border-画笔宽度
private var mStrokeWidth = 0
// 圆之间的间距
private var mSpace = 0
//半径
private var mRadius = 0
//计数各个圆的圆心,放到集合中
private fun measureIndicator() {
    mIndicators.clear()
    //临时变量记录该圆心横坐标
    var cx = 0f
    //[)左闭右开
    for (i in 0 until mCount) {
        val indicator = Indicator()
        if (i == 0) {
            //第一个圆的横坐标为半径+画笔的宽度
            cx = mRadius + mStrokeWidth.toFloat()
        } else {
            cx += (mRadius + mStrokeWidth) * 2 + mSpace.toFloat()
        }
        indicator.cx = cx
        //位于布局中间
        indicator.cy = measuredHeight / 2.toFloat()
        mIndicators.add(indicator)
    }
}

1.2 绘制圆

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    //mIndicators.indices可以得到所得对象的下标值,相当于 i in 0 .. mIndicators.size
    for (i in mIndicators.indices) {
        val indicator = mIndicators[i]
        val x = indicator.cx
        val y = indicator.cy
        //这个点为当前选择的点,设置画笔颜色和风格,画实心圆
        if (mSelectPosition == i) {                  
            mCirclePaint.style = Paint.Style.FILL
            mCirclePaint.color = mSelectColor
        } else {          
            //设置未选中点的画笔颜色和风格,画空心圆
            mCirclePaint.color = mDotNormalColor
            mCirclePaint.style = Paint.Style.STROKE
        }
        canvas?.drawCircle(x, y, mRadius.toFloat(), mCirclePaint)
    }
}

1.3 计算该View的大小

看文章刚开始的那张图,可以看出View的总宽度的计算方法

至于高度多少可以自定义,但是View的高度至少要大于圆的radius*2,

/**
 * 计算View的大小,就是整个自定义控件的大小
 */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val width = (mRadius + mStrokeWidth) * 2 * mCount + mSpace * (mCount - 1)   
    //widthMeasureSpec和heightMeasureSpec是在XML里设置的宽度和高度
    if(heightMeasureSpec<2 * mRadius){
        //决定了当前View的大小
        setMeasuredDimension(width, 2 * mRadius)
    }else{
        //决定了当前View的大小
        setMeasuredDimension(width, heightMeasureSpec)
    }
	//在这里测量每个圆点的位置
    measureIndicator()
}

2. 与ViewPager2联动

2.1 向外提供方法,设置CircleIndicatorView与ViewPager2关联

//要关联的ViewPager2
private var mViewPager: ViewPager2? = null
// indicator 的数量
private var mCount = 0
fun setUpWithViewPager(viewPager: ViewPager2) {
   
    mViewPager = viewPager
	//对ViewPager切换界面做监听
    viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            mSelectPosition = position
            invalidate()
        }
    })
    //获取真实的数值
    val count: Int = (viewPager.adapter as MyAdapter).itemCount
    setCount(count)
}
private fun setCount(count: Int) {
    mCount = count
    invalidate()
}

3. Indicator小圆点的点击事件

目的:实现点击小圆点就能切换ViewPager到相应界面

思路:监听ACTION_DOWN事件,获取点击屏幕的坐标,与所绘制的圆位置作比较,若点击区域在圆的范围内,就点击了该Indicator,点击后切换ViewPager到相应界面。

//处理点击小圆点的点击事件,看点击的位置是否在小圆点内
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)
}

在这里插入图片描述

//处理点击事件
private fun handleActionDown(xDis: Float, yDis: Float) {
    for (i in mIndicators.indices) {
        val indicator = mIndicators[i]
        //圆点的坐标是(cx,cy) 半径是mRadius, 圆边宽是mStrokeWidth
        if ( xDis >= indicator.cx - (mRadius + mStrokeWidth) && xDis < indicator.cx + mRadius + mStrokeWidth
            //                && yDis >= yDis - (indicator.cy + mStrokeWidth) && yDis < indicator.cy + mRadius + mStrokeWidth
            //TODO(这里和参考的文章不同,为啥是它那样)
            && yDis >= indicator.cy - (mRadius   + mStrokeWidth) && yDis < indicator.cy + mRadius + mStrokeWidth) {
            //找到了点击的Indicator
            //切换ViewPager
            mViewPager?.setCurrentItem(i, false)
            //单独点击圆点的回调
            if (mOnIndicatorClickListener != null) {
                mOnIndicatorClickListener!!.onSelected(i)
            }
        }
    }
}
private var mOnIndicatorClickListener: OnIndicatorClickListener? = null

interface OnIndicatorClickListener {
    fun onSelected(position: Int)
}

fun setOnIndicatorClickListener(onIndicatorClickListener: OnIndicatorClickListener) {
    mOnIndicatorClickListener = onIndicatorClickListener
}

如何需要在点击圆点时做一些其它操作,可以写个专门的监听器来做回调

4. 自定义属性

如果想在XML文件中能够直接编辑该控件圆点的一些属性,可以自定义属性。

在values文件夹下新建一个Value resource file

<resources>

    <declare-styleable name="CircleIndicatorView">
        <!--圆半径-->
        <attr name="indicatorRadius" format="dimension" />
        <!--画笔宽度,即圆的边距-->
        <attr name="indicatorBorderWidth" format="dimension" />
        <!--圆之间的间距-->
        <attr name="indicatorSpace" format="dimension" />
        <!--圆未被选择的颜色-->
        <attr name="indicatorColor" format="color" />
        <!--圆被选中的颜色-->
        <attr name="indicatorSelectedColor" format="color" />

    </declare-styleable>
</resources>

然后在CircleIndicatorView的构造方法中来获取属性值赋值

init {
    //画笔设置
    mCirclePaint.isDither = true
    mCirclePaint.isAntiAlias = true
    mCirclePaint.style = Paint.Style.FILL_AND_STROKE
    mCirclePaint.color = mDotNormalColor
    mCirclePaint.strokeWidth = mStrokeWidth.toFloat() //画笔宽度

    getAttr(context, attrs!!)

}
//获取自定义属性
private fun getAttr(
    context: Context,
    attrs: AttributeSet
) {
    val typedArray =
    context.obtainStyledAttributes(attrs, R.styleable.CircleIndicatorView)
    //半径(没设置则用6dp)
    mRadius = typedArray.getDimensionPixelSize(
        R.styleable.CircleIndicatorView_indicatorRadius,
        DisplayUtils.dpToPx(6)
    )
    //画笔宽度(没设置则用2dp)
    mStrokeWidth = typedArray.getDimensionPixelSize(
        R.styleable.CircleIndicatorView_indicatorBorderWidth,
        DisplayUtils.dpToPx(2)
    )
    //圆之间间距(没设置则用5dp)
    mSpace = typedArray.getDimensionPixelSize(
        R.styleable.CircleIndicatorView_indicatorSpace,
        DisplayUtils.dpToPx(5)
    )
    //圆未选中颜色
    mDotNormalColor = typedArray.getColor(
        R.styleable.CircleIndicatorView_indicatorColor,
        Color.BLACK
    )
    //圆选中颜色
    mSelectColor = typedArray.getColor(
        R.styleable.CircleIndicatorView_indicatorSelectedColor,
        Color.WHITE
    )
}

DisplayUtils是工具类,可以将dp转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()
    }
}

5. 使用

在xml布局内加上该控件

<?xml version="1.0" encoding="utf-8"?>
<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>

在Activity内直接绑定即可

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

6. ViewPager2+Fragment的联动使用

这里介绍ViewPager2和Fragment的连用

6.1 建立适配器

适配器继承自FragmentStateAdapter

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

    var fragments = mutableListOf<Fragment>()

    //创建Fragment(这里的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(): Int {
        return fragments.size
    }

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

6.2 绑定适配器

mViewpager.adapter = MyAdapter(this)

更多ViewPager2的使用可参考底部的参考文章

7.总结

其实还是用到自定义View和ViewPager2的使用两个知识点。小圆点点击带动ViewPager联动不过是自定义View的Touch里响应点击事件,然后通过viewPager.setCurrentItem(i, false)方法来实现和ViewPager联动;ViewPager切换带动小圆点联动不过是通过viewPager.registerOnPageChangeCallback()通过实现onPageSelected()方法然后重绘View来实现的。

写过的东西还是要多总结,不然很快就会忘掉。 如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。

Github地址

参考文章

Android自定义View之 实现一个多功能的IndicatorView

学不动也要学!深入了解ViewPager2