【Android自定义控件】实现可滑动的进度条

本文介绍如何在Android中通过自定义View实现一个可滑动的进度条,包括定义属性、绘制元素、处理滑动事件等关键步骤。

前言

  • 本篇文章记录通过自定义View实现Android下可滑动的进度条
  • 学习巩固自定义View知识

说明

1、实现效果

文中实现的效果都是未加抗锯齿

自定义可滑动进度条

2、View绘制解析

上图自定义View中有文本(大小)背景条(灰色)进度条(绿色)滑动区域(白色内圆)外框圆(绿色)、进度文字等元素,分析清晰元素的属性,代码更容易的去实现。

  • 背景条属性: 起点坐标(startX)、长度(backgroundTotalLen)、颜色(backgroundColor)、线宽(backgroundStrokeW)

  • 进度条属性: 起点坐标(startX)、进度(progress)为0-100、颜色
    (progressColor)、线宽(progressStrokeW)

  • 滑块内圆: 半径(handleRadius)

  • 外框圆: 这里设置半径比内圆大2f ,颜色和进度条颜色一致(progressColor)

  • 文本(进度值): 是否显示(showProgressText)

注意:

1、进度条的进度对应的坐标和滑动区域(内圆的坐标中心)、外框圆(坐标中心)一致

2、外框圆的颜色和进度条的颜色一致

3、绘制进度值文本如何与背景条与进度条保持垂直居中?


实现

上面分析了自定义View元素的属性值,下面代码进行实现

一、声明属性文件,在values下新建 attrs.xml文件

新建标签为 declare-styleable类型的xml,名字SlideView可自定义,一般和自定义View名保持一致,声明标签和名和对应的类型。


	<resources>
	    <declare-styleable name="SlideView">
	        <!--进度背景颜色-->
	        <attr name="backgroundColor" format="color"/>
	
	        <!--背景的线宽-->
	        <attr name="backgroundStrokeW" format="float"/>
	
	        <!--背景总长度-->
	        <attr name="backgroundTotalLen" format="integer"/>
	
	        <!--起点x坐标-->
	        <attr name="startX" format="integer"/>
	
	        <!--进度-->
	        <attr name="progress" format="integer"/>
	
	        <!--进度颜色-->
	        <attr name="progressColor" format="color"/>
	
	        <!--进度线宽-->
	        <attr name="progressStrokeW" format="float"/>
	
	        <!--手柄圆半径-->
	        <attr name="handleRadius" format="float"/>
	
	        <!--是否显示文字进度-->
	        <attr name="showProgressText" format="boolean"/>
	
	    </declare-styleable>
	</resources>


二、获取xml文件上的属性值,并给画笔设置属性

attributeSet为Activity布局文件中声明的属性,R.styleable.SlideViewattrs.xml中声明的属性

	
	/**
     * 进度条监听,回调到外面
     */
    private lateinit var listener:(progress:Int) -> Unit
	
    fun onProgressChange(l:(progress:Int) ->Unit){
        this.listener = l
    }

    /**
     * 背景条
     */
    private val backgroundPaint = Paint().apply {
        style = Paint.Style.FILL
        strokeCap = Paint.Cap.ROUND
    }

    /**
     * 进度条画笔
     */
    private val progressPaint = Paint().apply {
        style = Paint.Style.FILL
        strokeCap = Paint.Cap.ROUND
    }

    /**
     * 内实心圆画笔
     */
    private val innerCirclePaint = Paint().apply {
        style = Paint.Style.FILL
        strokeWidth = 10f
        color = Color.WHITE
    }

    /**
     * 外圆画笔
     */
    private val outerCirclePaint = Paint().apply {
        style = Paint.Style.STROKE
        strokeWidth = 2f
    }

    /**
     * 文字画笔
     */
    private val textPaint = Paint().apply {
        style = Paint.Style.FILL
        textSize = 40f
        color = Color.BLACK
    }


	init{
	
		 val ta = context.obtainStyledAttributes(attributeSet,`R.styleable.SlideView`)
		 //获取背景条颜色
		 backgroundColor = ta.getColor(R.styleable.SlideView_backgroundColor,context.getColor(R.color.colorEC))
		 //获取背景条线宽
		 backgroundStrokeW = ta.getFloat(R.styleable.SlideView_backgroundStrokeW,18f)
		 //获取背景条总长
		 totalLen = ta.getInt(R.styleable.SlideView_backgroundTotalLen,100)
		 //获取进度条颜色
		 progressColor = ta.getColor(R.styleable.SlideView_progressColor,context.getColor(R.color.colorGrassGreen))
		 //获取进度条线宽
		 progressStrokeW = ta.getFloat(R.styleable.SlideView_progressStrokeW,18f)
		 //获取进度条的进度
		 progress = ta.getInt(R.styleable.SlideView_progress,0)
		 //获取内圆半径
		 radius = ta.getFloat(R.styleable.SlideView_handleRadius,30.toFloat())
		 //获取绘制起点
		 startX = DeviceUtils.dp2px(context,ta.getInt(R.styleable.SlideView_startX,0).toFloat() + DeviceUtils.px2dp(context,radius) + 2f)
		 //是否显示进度值
		 showProgressText = ta.getBoolean(R.styleable.SlideView_showProgressText,true)
		 ta.recycle()
		 
	}


三、绘制元素

重写onDraw方法,绘制背景条进度条滑动区域(内圆)外圆


	/**
	* 绘制元素
	*/
	override fun onDraw(canvas: Canvas) {
	   super.onDraw(canvas)
	   canvas.apply {
	   	//绘制背景条
	       drawLine(startX.toFloat(),endY ,endX.toFloat(),endY,backgroundPaint)
	       //绘制进度条
	       drawLine(startX.toFloat(),endY, progressValue,endY,progressPaint)
	       //绘制内圆
	       drawCircle(progressValue,endY, radius,innerCirclePaint)
	       //绘制外圆
	       drawCircle(progressValue,endY ,radius + 2f,outerCirclePaint)
	       //是否绘制进度值
	       if(showProgressText){
	           drawText("${(((progressValue - startX) / totalLen) * 100).toInt()} %", endX + 40f ,radius + 2f - baseLine,textPaint)
	       }
	   }
	}
	

四、处理滑动事件

重写ViewOnTouchEvent方法,需要判断手指按下的区域在外圆的坐标值内,滑动的范围要限制在startXendX之间


	/**
	 * 处理拖动事件
	 */
	 @SuppressLint("ClickableViewAccessibility")
	 override fun onTouchEvent(event: MotionEvent): Boolean {
	     var cx = event.x
	     val cy = event.y
	     when(event.action){
	         MotionEvent.ACTION_DOWN ->{
	             //判断手指按下区域是否在句柄圆上, 左右和上下有效触摸区域扩大各40f
	             isOnTouch = (cx > progressValue - radius - 20f  && cx < progressValue + radius + 20f  && cy > -20f && cy < 2 * radius + 20f)
	         }
	         MotionEvent.ACTION_MOVE ->{
	             if (isOnTouch){
	             	 //限制最小值为起点startX
	                 if(cx < startX){ cx = startX.toFloat() }
	                 //限制最大值为终点endX
	                 else if(cx > endX) { cx = endX.toFloat() }
	                 progressValue = cx
	                 //重新绘制
	                 invalidate()
	                 //将进度回调出去
	                 listener.invoke(((progressValue / (endX  - startX)) * 100).toInt())
	             }
	         }
	         MotionEvent.ACTION_UP ->{
	             isOnTouch = false
	         }
	     }
	     return true
	 }


五、问题点的处理

绘制进度文字时遇到一个问题,就是文字和背景条和进度条无法居中对齐。


 if(showProgressText){
    drawText("${(((progressValue - startX) / totalLen) * 100).toInt()} %", endX + 40f ,radius + 2f,textPaint)
   }
   

进度值的X坐标在背景条后面,距离为40F,Y坐标和内圆半径raduis + 外圆半径2F,按理说应该是和滑动区域是垂直居中的。然后显示起来并没有居中,猜想文本在坐标系中的绘制较其有特殊。

文本为居中
那我们看下文本是怎么绘制在坐标系中的? 在Android中,提供了方法getTextBounds查看文本的绘制区域。


  /**
    * 文字画笔
    */
 private val textPaint = Paint().apply {
    style = Paint.Style.FILL
    textSize = 40f
    color = Color.BLACK
  }

  rect = Rect()
  textPaint.getTextBounds("100%",0,"100%".length,rect)
  Log.d("AAAAAA","left = $left , top = ${rect?.top!!} , right = $right , bottom = ${rect?.bottom}")

  输出:left = 0 , top = -29 , right = 0 , bottom = 2
  

通过打印出来的值,可以发现文本基线并不是垂直于坐标轴Y轴的,当前Paint和文本获取到绘制区域的 topbottom值如下图所示,这就造成为了绘制后不垂直的原因。因为文本绘制基线涉及需要大量篇幅去说明,这里就不详细解释。那要如何处理呢?其实很简单,让文本的基线
垂直于Y轴即可。
Text BaseLine

解决方法:

把绘制文本的topbottom取中间值作为垂直于Y轴的基线带入计算即可


   //文字绘制的基线
	va baseLine = rect?.let {
	    (rect?.top!! + rect?.bottom!!/ 2
	 }!!
	Log.d("AAAAAA","baseLine = $baseLine")
	  
	输出: baseLine = 13
	 
	if(showProgressText){
		drawText("${(((progressValue - startX) / totalLen) * 100).toInt()} %", endX + 40f ,radius + 2f  - baseLine ,textPaint)
	 }
	   

六、布局文件中使用

 Xml中:
 
  <com.xn.customview.widget.SlideView
     android:id="@+id/svAlpha"
     android:layout_width="@dimen/px_906"
     android:layout_height="@dimen/px_72"
     android:layout_gravity="center_vertical"
     android:layout_marginStart="10dp"
     app:backgroundColor="@color/colorEC"
     app:backgroundStrokeW="18"
     app:backgroundTotalLen="500"
     app:handleRadius="30"
     app:progress="50"
     app:progressColor="@color/colorGrassGreen"
     app:progressStrokeW="18"
     app:showProgressText="true"
     app:startX="0" />
        

 Activity中:
 
 //获取进度回调
 mBinding.svAlpha.onProgressChange {
     Log.d("AAAAAA","svAlpha progress = $it")
 }

 mBinding.svSize.onProgressChange {
     Log.d("AAAAAA","svSize progress = $it")
 }
  

结尾

文章中对文字位置处理不够完善,正确的处理方式请查看文章Android自定义控件(六) Andriod仿iOS控件Switch开关

下载代码方式:https://pan.quark.cn/s/a4b39357ea24 在Android平台的应用过程中,"Curve 贝塞尔曲线的滑动条"被视为一种个性化的视图元素,它融合了贝塞尔曲线的理论基础与常规滑动条的功能特性。 贝塞尔曲线作为一种在数字图形领域中普遍应用的数学工具,主要用于生成平滑且连贯的曲线轨迹。 此类滑动条借助贝塞尔曲线来展现其滑动路径,从而为用户界面注入了别致的视觉表现力和互动感受。 贝塞尔曲线的形态由控制点的位置决定,并可分为线性、二次、三次以及更高阶的形式。 在Android框架内,通常采用的是二次或三次贝塞尔曲线,因为它们能够构成更为复杂的图形形态。 二次贝塞尔曲线通过初始点、终止点以及两个控制点来构建,而三次贝塞尔曲线则通过增加一个额外的控制点来提升曲线的灵活性。 构建此类滑动条的第一步是创建一个自定义View类别,并重写onDraw()函数来绘制贝塞尔曲线。 在函数内部,可以利用Path对象来构建曲线的路径,借助QuadTo()(适用于二次贝塞尔曲线)或CubicTo()(适用于三次贝塞尔曲线)函数来设定曲线的关键点位置。 随后,通过调用canvas.drawPath()函数来呈现路径。 滑动条的滑块部分需要独立设计,使其能够沿着贝塞尔曲线进行移动。 通过监控滑动事件,如MotionEvent.ACTION_DOWN、ACTION_MOVE和ACTION_UP,可以计算出滑块的新位置并刷新视图界面。 计算新位置的过程中,可能需要将显示器的坐标转换为曲线参数空间中的数值,这一过程涉及到曲线参数化的概念,需要对贝塞尔曲线的数学特性有一定的掌握。 为了让滑动条具备实际功能,还必须实现onProgressChanged()函数,该函数会在滑动条的进度发...
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值