效果图
自定义StepProgress
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.abs
import kotlin.math.pow
class StepProgress @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 点击圆时监听,返回当前阶段
var onStepClick:((Int)->Unit) = {}
private val textSize = 30f
// 完成后圆、路径颜色
private val doneColor = Color.rgb(68, 129, 254)
private var currentStep: Int = 1 // 当前步骤,从1开始
private val stepRadius = 30f // 圆圈半径
private val paddingHorizontal = 30f // 水平内边距
private val paddingVertical = stepRadius // 垂直内边距
private var stepSpacing = 200f // 圆圈之间的间距(两圆之间连线长度),将动态计算
private var totalSteps: List<String> = listOf() // 所有步骤
private val visibleSteps = mutableListOf<Step>() // 可见步骤的列表
private val paint = Paint().apply {
isAntiAlias = true // 抗锯齿
style = Paint.Style.FILL // 填充样式
textAlign = Paint.Align.CENTER // 文本居中对齐
textSize = 40f // 文本大小
}
private val textMarginTop = 30f // 阶段名称距离圆的间距
init {
// 初始数据,可以通过方法设置实际的步骤数
totalSteps = listOf("阶段1", "阶段2", "阶段3", "阶段4", "阶段5", "阶段6")
currentStep = 1
}
// 设置步骤列表
fun setSteps(steps: List<String>) {
this.totalSteps = steps
invalidate() // 重新绘制视图
}
// 设置当前步骤
fun setCurStep(step: Int) {
if (step in 1..totalSteps.size) {
currentStep = step
invalidate() // 重新绘制视图
}
}
// 下一步
fun nextStep() {
if (currentStep < totalSteps.size) {
currentStep++
invalidate() // 重新绘制视图
}
}
// 上一步
fun previousStep() {
if (currentStep > 1) {
currentStep--
invalidate() // 重新绘制视图
}
}
// 绘制视图
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawSteps(canvas) // 调用绘制步骤条和阶段名称的方法
}
// 绘制步骤条及阶段名称
private fun drawSteps(canvas: Canvas) {
visibleSteps.clear() // 清除旧的步骤数据
val centerX = width / 2f // 视图中心的X坐标
val visibleSteps = calculateVisibleSteps() // 计算当前显示的步骤
// 动态计算圆圈之间的间距
stepSpacing = if (visibleSteps.size > 1) {
(width - 2 * stepRadius * visibleSteps.size) / (visibleSteps.size - 1).toFloat() - paddingHorizontal * 2
} else {
0f
}
// 计算各个步骤的位置
when (visibleSteps.size) {
1 -> {
// 如果只有一个步骤,显示在中心
this.visibleSteps.add(Step(centerX, paddingVertical + stepRadius, visibleSteps[0]))
}
2 -> {
// 如果有两个步骤,均匀分布在中心左右
this.visibleSteps.add(
Step(
centerX - stepSpacing / 2,
paddingVertical + stepRadius,
visibleSteps[0]
)
)
this.visibleSteps.add(
Step(
centerX + stepSpacing / 2,
paddingVertical + stepRadius,
visibleSteps[1]
)
)
}
else -> {
// 如果有三个及以上步骤,当前步骤居中,前后各一个步骤
this.visibleSteps.add(
Step(
centerX - stepSpacing,
paddingVertical + stepRadius,
visibleSteps[0]
)
)
this.visibleSteps.add(Step(centerX, paddingVertical + stepRadius, visibleSteps[1]))
this.visibleSteps.add(
Step(
centerX + stepSpacing,
paddingVertical + stepRadius,
visibleSteps[2]
)
)
}
}
// 绘制连线
for (i in 0 until this.visibleSteps.size - 1) {
paint.color = if (totalSteps.indexOf(this.visibleSteps[i].text) + 1 < currentStep) {
// 当前步骤之前的连线为蓝色
doneColor
} else {
// 当前步骤之后的连线为灰色
Color.GRAY
}
paint.strokeWidth = 5f
canvas.drawLine(
this.visibleSteps[i].x + stepRadius,
paddingVertical + stepRadius,
this.visibleSteps[i + 1].x - stepRadius,
paddingVertical + stepRadius,
paint
)
}
// 绘制步骤和阶段名称
this.visibleSteps.forEachIndexed { index, step ->
// 绘制圆圈
paint.color = if (totalSteps.indexOf(step.text) + 1 <= currentStep) {
// 当前步骤及之前的圆圈为蓝色
doneColor
} else {
// 当前步骤之后的圆圈为灰色
Color.GRAY
}
canvas.drawCircle(step.x, step.y, stepRadius, paint)
// 在圆圈内绘制当前步骤编号
paint.color = Color.WHITE
paint.textSize = textSize// 圆内文字固定大小为30f
val stepIndex = totalSteps.indexOf(step.text) + 1
canvas.drawText(stepIndex.toString(), step.x, step.y + (paint.textSize / 4), paint)
// 在圆圈下方绘制阶段名称
paint.color = if (stepIndex == currentStep) Color.BLUE else Color.BLACK
if (stepIndex == currentStep) {
paint.color = doneColor
paint.textSize = textSize
} else {
paint.color = Color.BLACK
paint.textSize = 20f
}
canvas.drawText(
step.text,
step.x,
step.y + stepRadius + textMarginTop + (paint.textSize / 4),
paint
)
}
}
// 计算当前显示的步骤
private fun calculateVisibleSteps(): List<String> {
return when {
// 如果总步骤数小于等于3,显示所有步骤
totalSteps.size <= 3 -> totalSteps
// 如果当前步骤是第1步,显示前3个步骤
currentStep == 1 -> totalSteps.subList(0, 3)
// 如果当前步骤是最后一步,显示最后3个步骤
currentStep == totalSteps.size -> totalSteps.subList(
totalSteps.size - 3,
totalSteps.size
)
// 否则,显示当前步骤及其前后各一个步骤
else -> totalSteps.subList(currentStep - 2, currentStep + 1)
}
}
private var downX = 0f
private var downY = 0f
// 有效点击范围
private val touchSlop = stepRadius * 2 + textMarginTop + textSize
// 处理点击事件
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x // 记录按下的X坐标
downY = event.y // 记录按下的Y坐标
}
MotionEvent.ACTION_UP -> {
val upX = event.x // 获取抬起的X坐标
val upY = event.y // 获取抬起的Y坐标
// 判断按下和抬起的坐标差距是否在合理范围内
if (abs(upX - downX) <= touchSlop && abs(upY - downY) <= touchSlop) {
visibleSteps.forEach { step ->
if ((upX - step.x).toDouble().pow(2.0) + (upY - step.y).toDouble()
.pow(2.0) <= touchSlop.toDouble().pow(2.0)
) {
// 判断点击位置是否在某个步骤的范围内
currentStep = totalSteps.indexOf(step.text) + 1 // 更新当前步骤
invalidate() // 重新绘制视图
onStepClick.invoke(currentStep) // 通知步骤点击
return true
}
}
}
}
}
return true
}
// 默认宽高
private val defaultWidth = 300
// 圆直径 + 2 * 垂直内边距 + 文字和圆顶部外边距 +文字大小
private val defaultHeight =
stepRadius * 2 + textMarginTop + textSize + paddingVertical * 2// 默认高度
// 重新测量视图的宽高
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec) // 获取宽度测量模式
val widthSize = MeasureSpec.getSize(widthMeasureSpec) // 获取宽度测量值
val heightMode = MeasureSpec.getMode(heightMeasureSpec) // 获取高度测量模式
val heightSize = MeasureSpec.getSize(heightMeasureSpec) // 获取高度测量值
// 根据测量模式和测量值确定宽度
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> defaultWidth.coerceAtMost(widthSize)
MeasureSpec.UNSPECIFIED -> defaultWidth
else -> defaultWidth
}
// 根据测量模式和测量值确定高度
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> defaultHeight.coerceAtMost(heightSize.toFloat())
MeasureSpec.UNSPECIFIED -> defaultHeight
else -> defaultHeight
}
setMeasuredDimension(width, height.toInt()) // 设置视图的宽高
}
/**
* 步骤数据类
* @param x x坐标
* @param y y坐标
* @param text 步骤名
*/
data class Step(val x: Float, val y: Float, val text: String)
}
在xml中使用
<androidx.appcompat.widget.LinearLayoutCompat 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"
android:orientation="vertical"
tools:context=".ui.activity.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.example.customview.widget.flow.StepProgress
android:id="@+id/stepProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.appcompat.widget.LinearLayoutCompat>
activity代码
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var pagerAdapter: TabPagerAdapter
private val fragments = mutableListOf<Fragment>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
fragments.add(FirstFragment())
fragments.add(SecondFragment())
fragments.add(ThirdFragment())
fragments.add(FourthFragment())
pagerAdapter = TabPagerAdapter(fragments, this)
// 设置步骤
binding.stepProgress.setSteps(listOf(
"阶段一",
"阶段二",
"阶段三",
"阶段四"
))
binding.viewPager.adapter = pagerAdapter
binding.viewPager.isSaveEnabled = false
binding.viewPager.offscreenPageLimit = fragments.size - 1
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
binding.stepProgress.setCurStep(binding.viewPager.currentItem + 1)
}
})
// 设置步骤条的点击监听器
binding.stepProgress.onStepClick = {
binding.viewPager.setCurrentItem(it - 1, true)//点击返回步骤是从1开始,viewpager下标从0开始,所以需要减一
}
}
}
适配器代码:
class TabPagerAdapter(private val fragments: List<Fragment>, fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
override fun createFragment(position: Int): Fragment = fragments[position]
override fun getItemCount(): Int = fragments.size
fun getFragments() = fragments
}