自定义步骤条并结合ViewPager使用【自定义组件】

效果图

自定义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
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用ViewPager,你需要遵循以下步骤: 1. 在你的布局文件中添加ViewPager组件。例如,你可以在XML文件中添加以下代码: ```xml <androidx.viewpager.widget.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 创建适配器(Adapter)类来管理ViewPager的内容。适配器负责提供ViewPager所需的视图项(Fragments或Views)。你可以创建一个继承自FragmentPagerAdapter或FragmentStatePagerAdapter的自定义适配器类,具体取决于你的需求。例如,以下是一个简单的FragmentPagerAdapter示例: ```java public class MyPagerAdapter extends FragmentPagerAdapter { private List<Fragment> fragmentList; public MyPagerAdapter(FragmentManager fragmentManager, List<Fragment> fragments) { super(fragmentManager); fragmentList = fragments; } @NonNull @Override public Fragment getItem(int position) { return fragmentList.get(position); } @Override public int getCount() { return fragmentList.size(); } } ``` 3. 在你的Activity或Fragment中实例化ViewPager和适配器,并将它们连接在一起。例如,在Activity中: ```java ViewPager viewPager = findViewById(R.id.viewPager); MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager(), fragmentList); viewPager.setAdapter(adapter); ``` 请确保将fragmentList替换为你要显示的Fragment列表。 4. 可选:如果你想添加页面指示器(dots或tabs)来显示当前页面的位置,你可以使用TabLayout组件。在布局文件中添加TabLayout: ```xml <com.google.android.material.tabs.TabLayout android:id="@+id/tabLayout" android:layout_width="match_parent" android:layout_height="wrap_content" /> ``` 然后在代码中与ViewPager关联: ```java TabLayout tabLayout = findViewById(R.id.tabLayout); tabLayout.setupWithViewPager(viewPager); ``` 这将在TabLayout中显示与ViewPager页面数量相同的指示器。 这是使用ViewPager的基本步骤。你可以根据你的需求和设计进行更多的自定义和调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值