最近又遇到两个界面切换和底部圆点联动的设计,第一眼看到想到的就是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来实现的。
写过的东西还是要多总结,不然很快就会忘掉。 如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。