当前位置: 首页 > news >正文

Android 自定义「阶段进度条(步轴)」实践

动画阶段点型 Step Bar + 连续填充型 Stage Bar

一、背景 & 需求

很多业务不是简单的 0–100% 进度,而是分阶段的流程,比如:

  • 注册流程:填写信息 → 验证 → 完成

  • 工厂/机器人:初始化 → 运行中 → 校验 → 完成

  • 审批流程:发起 → 审核中 → 通过 → 归档

这类 UI 一般叫做阶段进度条 / 步轴(Step Progress Bar)

本文实现并对比两种常用方案,两种都是带动画的

  1. 方案一:动画阶段点型 Step Bar(Node Type + 动画)

  2. 方案二:连续填充型 Stage Bar(Animated + 可配置阶段数 + 文案)


二、方案一:动画阶段点型 Step Bar(Node Type + 动画)

1. 效果说明

  • 一条横向线段

  • N 个等间距的圆点节点

  • 当前步骤之前的线段 & 节点高亮,之后是灰色

  • 切换步骤时,有平滑动画

    • 前景线从上一个节点到下一个节点

    • 节点一个个被“点亮”

示意(逻辑上类似这样):

步骤 1     步骤 2     步骤 3     步骤 4●────────●────────○────────○
↑ 动画时前景线从第一个点平滑滑到第二个点

2. 适用场景

  • 步骤明确、节点含义很强的流程:注册、支付、审批、操作引导

  • 希望用户直观看到“第几步”,同时又有推进动画反馈

3. 思路简述(分享时可以这么讲)

  • 普通 Step Bar:用 currentStep: Int 直接决定前景线长度和节点状态
  • 动画版:增加一个 animatedStep: Float
  • setStep(step, animate=true) 时,用 ValueAnimator 把
    animatedStep 从旧值 平滑过渡到新值(例如 1.0f → 2.0f)

  • 绘制前景线长度:按 animatedStep 来算

  • 节点是否高亮:i <= animatedStep 就算“到达”


4. 完整代码:AnimatedStepProgressBar.kt

package com.example.widgetimport android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import android.view.animation.DecelerateInterpolator
import kotlin.math.min/*** 方案一:动画阶段点型 Step Bar(Node Type + 动画)** 特性:* - totalSteps:总步骤数(>=2),可代码配置* - setStep(step, animate):切换步骤,带平滑动画* - 带前景线、背景线、多节点圆点*/
class AnimatedStepProgressBar @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {/** 总步骤数,至少 2 个 */var totalSteps: Int = 4set(value) {field = value.coerceAtLeast(2)if (currentStep > field) currentStep = fieldanimatedStep = min(animatedStep, field.toFloat())invalidate()}/** 当前目标步骤(0..totalSteps) */var currentStep: Int = 0private set/** 用于绘制的“动画步骤值”,0f ~ totalSteps(允许小数,控制平滑) */private var animatedStep: Float = 0fprivate var animator: ValueAnimator? = null// 颜色可按需调整private val reachedColor = 0xFF4CAF50.toInt()   // 已完成(绿)private val unreachedColor = 0xFFBDBDBD.toInt() // 未完成(灰)private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {style = Paint.Style.STROKEstrokeWidth = dp(3f)strokeCap = Paint.Cap.ROUND}private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {style = Paint.Style.FILL}private val circleRadius = dp(6f)override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {val desiredHeight =(circleRadius * 2 + paddingTop + paddingBottom + dp(8f)).toInt()val width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)val height = resolveSize(desiredHeight, heightMeasureSpec)setMeasuredDimension(width, height)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)if (totalSteps < 2) returnval usableWidth = width - paddingLeft - paddingRightval startX = paddingLeft.toFloat()val endX = (paddingLeft + usableWidth).toFloat()val centerY = height / 2fval spacing = usableWidth / (totalSteps - 1).toFloat()// 1. 背景线(灰色)linePaint.color = unreachedColorcanvas.drawLine(startX, centerY, endX, centerY, linePaint)// 2. 前景线(绿),长度由 animatedStep 决定linePaint.color = reachedColorval reachedEndX = startX + spacing * animatedStepcanvas.drawLine(startX, centerY, reachedEndX, centerY, linePaint)// 3. 节点圆点:索引 <= animatedStep 时视为“到达”for (i in 0 until totalSteps) {val cx = startX + spacing * ival isReached = i.toFloat() <= animatedStep + 1e-3fcirclePaint.color = if (isReached) reachedColor else unreachedColorcanvas.drawCircle(cx, centerY, circleRadius, circlePaint)}}/*** 设置当前步骤(0..totalSteps)* @param step    目标步骤* @param animate 是否使用动画*/fun setStep(step: Int, animate: Boolean = true) {val target = step.coerceIn(0, totalSteps)if (!animate) {animator?.cancel()currentStep = targetanimatedStep = target.toFloat()invalidate()return}if (animator?.isRunning == true && target == currentStep) returncurrentStep = targetval start = animatedStepval end = target.toFloat()animator?.cancel()animator = ValueAnimator.ofFloat(start, end).apply {duration = 350Linterpolator = DecelerateInterpolator()addUpdateListener {animatedStep = it.animatedValue as Floatinvalidate()}start()}}private fun dp(value: Float): Float =value * resources.displayMetrics.density
}

5. 使用示例

XML:

<com.example.widget.AnimatedStepProgressBarandroid:id="@+id/stepBar"android:layout_width="match_parent"android:layout_height="40dp"android:layout_marginHorizontal="16dp"android:layout_marginTop="24dp" />

代码:

val stepBar = findViewById<AnimatedStepProgressBar>(R.id.stepBar)// 配置总步骤数(可根据业务变化)
stepBar.totalSteps = 5// 初始化到第 0 步(无动画)
stepBar.setStep(0, animate = false)// 点击“下一步”按钮时推进
btnNext.setOnClickListener {val next = (stepBar.currentStep + 1).coerceAtMost(stepBar.totalSteps)stepBar.setStep(next, animate = true)
}

三、方案二:连续填充型 Stage Bar

(Animated + 可配置阶段数 + 文案)

1. 效果说明

  • 一条“胶囊形”进度条:黄色底 + 蓝色填充
  • maxStage 控制总阶段数(3、4、6… 任意都行)
  • 每切换阶段,蓝条平滑过渡到新的占比(1/maxStage、2/maxStage …)
  • 顶部可以显示一段文字,例如:
    • 阶段 2 / 6:处理中

    • 初始化 → 连接设备 → 校验 → 完成 等提示文案

相比方案一,这个更偏“整体进度 + 动画体验”,而不是单纯强调节点。

2. 适用场景

  • 任务执行、导入导出、机器人任务、数据处理等连续进度感更强的场景

  • 希望通过文字+动画给用户舒适的反馈


3. 自定义属性 attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="StageProgressBar"><!-- 顶部文案 --><attr name="spb_labelText" format="string" /><!-- 顶部文字颜色 --><attr name="spb_labelTextColor" format="color" /><!-- 顶部文字大小 --><attr name="spb_labelTextSize" format="dimension" /><!-- 最大阶段数(>=1),默认 4 --><attr name="spb_maxStage" format="integer" /></declare-styleable></resources>

4. 完整代码:StageProgressBar.kt

(动画 + 可配置阶段数 + 顶部文案)

package com.example.widgetimport android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.example.myapplication.R   // 换成你的 R 包名/*** 方案二:连续填充型 Stage Bar* - maxStage:阶段总数(XML + 代码可配置)* - setStage(stage, animate):阶段切换动画* - labelText:顶部文案,颜色和字号也可 XML/代码配置*/
class StageProgressBar @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {/** 最大阶段数(>=1) */var maxStage: Int = 4set(value) {val newValue = value.coerceAtLeast(1)if (field == newValue) returnfield = newValueif (currentStage > field) currentStage = fieldprogressFraction = currentStage / field.toFloat()requestLayout()invalidate()}/** 当前阶段 [0..maxStage] */var currentStage: Int = 0private set/** 实际绘制用 0f ~ 1f,由动画驱动 */private var progressFraction: Float = 0fprivate var animator: ValueAnimator? = null// 背景 / 填充颜色private val bgColor = 0xFFF9C54A.toInt()   // 黄色背景private val fgColor = 0xFF1E6CFF.toInt()   // 蓝色进度// 进度条高度private val barHeightDp = 8fprivate val barHeightPx get() = dp(barHeightDp)// 顶部文案var labelText: String = ""set(value) {field = valueinvalidate()}private var labelTextColor: Int = 0xFFFFFFFF.toInt()private var labelTextSizePx: Float = sp(12f)private val textBarSpacing = dp(4f)private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {style = Paint.Style.FILLcolor = bgColor}private val fgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {style = Paint.Style.FILLcolor = fgColor}private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {textAlign = Paint.Align.CENTER}private val rect = RectF()init {if (attrs != null) {val ta = context.obtainStyledAttributes(attrs,R.styleable.StageProgressBar,defStyleAttr,0)maxStage = ta.getInt(R.styleable.StageProgressBar_spb_maxStage,maxStage).coerceAtLeast(1)labelText = ta.getString(R.styleable.StageProgressBar_spb_labelText) ?: ""labelTextColor = ta.getColor(R.styleable.StageProgressBar_spb_labelTextColor,labelTextColor)labelTextSizePx = ta.getDimension(R.styleable.StageProgressBar_spb_labelTextSize,labelTextSizePx)ta.recycle()}textPaint.color = labelTextColortextPaint.textSize = labelTextSizePx}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {val fm = textPaint.fontMetricsval textHeight = fm.bottom - fm.topval desiredHeight =(paddingTop +textHeight +textBarSpacing +barHeightPx +paddingBottom).toInt()val width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)val height = resolveSize(desiredHeight, heightMeasureSpec)setMeasuredDimension(width, height)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)val widthF = width.toFloat()val fm = textPaint.fontMetricsval textHeight = fm.bottom - fm.topval textBaselineY = paddingTop - fm.topval barTop = paddingTop + textHeight + textBarSpacingval barBottom = barTop + barHeightPxval left = paddingLeft.toFloat()val right = widthF - paddingRightval radius = (barBottom - barTop) / 2f// 1. 顶部文字if (labelText.isNotEmpty()) {canvas.drawText(labelText, widthF / 2f, textBaselineY, textPaint)}// 2. 黄色背景条rect.set(left, barTop, right, barBottom)canvas.drawRoundRect(rect, radius, radius, bgPaint)// 3. 蓝色进度条if (progressFraction > 0f) {val totalWidth = right - leftval progressRight = left + totalWidth * progressFractionrect.set(left, barTop, progressRight, barBottom)canvas.drawRoundRect(rect, radius, radius, fgPaint)}}/*** 设置阶段(0..maxStage)*/fun setStage(stage: Int, animate: Boolean = true) {val targetStage = stage.coerceIn(0, maxStage)val targetFraction = targetStage / maxStage.toFloat()if (!animate) {animator?.cancel()currentStage = targetStageprogressFraction = targetFractioninvalidate()return}if (targetStage == currentStage && animator?.isRunning == true) returncurrentStage = targetStageanimator?.cancel()animator = ValueAnimator.ofFloat(progressFraction, targetFraction).apply {duration = 400Linterpolator = DecelerateInterpolator()addUpdateListener { anim ->progressFraction = anim.animatedValue as Floatinvalidate()}start()}}/** 代码动态改文字颜色 */fun setLabelTextColor(color: Int) {labelTextColor = colortextPaint.color = labelTextColorinvalidate()}/** 代码动态改文字大小(px) */fun setLabelTextSizePx(sizePx: Float) {labelTextSizePx = sizePxtextPaint.textSize = labelTextSizePxrequestLayout()invalidate()}private fun dp(value: Float): Float =value * resources.displayMetrics.densityprivate fun sp(value: Float): Float =value * resources.displayMetrics.scaledDensity
}


5. 使用示例

XML:

<com.example.widget.StageProgressBarandroid:id="@+id/stageBar"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginHorizontal="16dp"android:layout_marginTop="24dp"xmlns:app="http://schemas.android.com/apk/res-auto"app:spb_maxStage="6"app:spb_labelText="阶段 1 / 6:初始化中"app:spb_labelTextColor="@android:color/white"app:spb_labelTextSize="14sp" />

代码:

val bar = findViewById<StageProgressBar>(R.id.stageBar)fun updateStage(stage: Int) {bar.setStage(stage, animate = true)bar.labelText = "阶段 $stage / ${bar.maxStage}"
}updateStage(1)btnNext.setOnClickListener {val next = (bar.currentStage + 1).coerceAtMost(bar.maxStage)updateStage(next)
}

四、两种方案对比总结(技术分享可以直接放这一页)

维度方案一:动画阶段点型 Step Bar方案二:连续填充型 Stage Bar
表现形式一条线 + 多个圆点节点胶囊形连续进度条
动画线段和节点随步骤“逐步点亮”整体蓝色填充条按比例平滑变化
强调点“第几步 / 下一个节点是谁”“整体完成了多少 / 当前处于哪一阶段”
阶段数配置totalSteps(代码配置)maxStage(XML + 代码)
文案支持默认无,可额外扩展内置 labelText + 颜色/字号可配置
适用场景注册、审批、分步引导任务执行、数据处理、机器人任务、长流程
UI 风格节点明确、结构清晰更现代、有节奏感的进度展示

一张嘴上的总结:

  • 想突出「这是一个 4 步操作,现在在第 2 步」→ 用 方案一 动画节点 Step Bar

  • 想突出「任务整体完成进度 + 状态文案」→ 用 方案二 连续填充 Stage Bar

http://www.dtcms.com/a/618328.html

相关文章:

  • 【第三阶段-核心功能开发:UI进阶】第七章:主题系统-就像室内设计师
  • discuz 手机网站wordpress 搜索自定义数据表字段
  • CC++链接数据库(MySQL)超级详细指南
  • 苍穹外卖 —— 数据统计和使用Apache_POI库导出Excel报表
  • 昆明好的网站制作网站价格评估 优帮云
  • 如何查询SQL Server数据库服务器的IP地址
  • 开源:开源协议从入门到落地
  • 网站域名要怎样规划佛山做外贸网站案例
  • 网站建设找导师蓝林月租网站空间
  • 2025 IntelliJ IDEA 2025最新免费安装教程
  • Numpy数值分析库实验
  • 游戏常用运行库丨免费纯净丨多系统支持丨自动推荐安装
  • git-拉取代码报错update ref failed ref ‘ORIG HEAD‘
  • 手机网站模板 html5西安搬家公司电话号码大全
  • 资源优化排名网站哈尔滨企业网站模板建站
  • 基于扩散模型与流模型的跨分辨率流场映射方法研究
  • 【Linux日新月异(十)】CentOS 7 文件系统结构深度解剖:从根到叶的完整指南
  • linux服务-rsync+inotify文件同步-ssh
  • 保障房建设网站首页游戏策划
  • 深度学习杂草分割系统1:数据集说明(含下载链接)
  • 超分辨率重建(Super-Resolution, SR)
  • 高端品牌网站建设注意事项制作ppt的基本做法
  • 2025 年 Redis 面试天花板
  • component-富文本实现(WangEditor)
  • 烟台城乡住房建设厅网站网站alt标签
  • win11上使用Workbench备份mysql数据库
  • B站评论数据采集:基于Requests的智能爬虫实战
  • 信息学与容斥
  • 网易云音乐评论数据采集:基于Requests的智能爬虫实战
  • 网站空间登录网站建设模式有哪些内容