当前位置:   article > 正文

Android自定义控件(一) 可滑动的进度条_android进度条控件

android进度条控件

前言

  • 本篇文章记录通过自定义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>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

二、获取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()
		 
	}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

三、绘制元素

重写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)
	       }
	   }
	}
	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

四、处理滑动事件

重写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
	 }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

五、问题点的处理

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


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

进度值的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
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

通过打印出来的值,可以发现文本基线并不是垂直于坐标轴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)
	 }
	   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

六、布局文件中使用

 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" />
        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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

 mBinding.svSize.onProgressChange {
     Log.d("AAAAAA","svSize progress = $it")
 }
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

结尾

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/461094
推荐阅读
相关标签
  

闽ICP备14008679号