• 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"/>
    	
    	        
    	        <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自定义控件,可滑动的进度条,记录了从0到1开发的整个过程,加深对自定义View的理解,方便复习。

  • 相关阅读:
    MyBatis-Plus标准数据层开发
    HBase学习笔记(1)—— 知识点总结
    如何制定数据治理策略?做好这7点就够了
    String的理解
    OS之页面置换算法
    .net core 上传文件大小限制
    【算法优选】 滑动窗口专题——贰
    JVM——3.StringTable字符串常量池
    vscode配置vue
    关于.Net 6.0 在Linux ,Docker容器中,不安装任何依赖就生成图形验证码!!!!!!!!!!!
  • 原文地址:https://blog.csdn.net/qq_17470165/article/details/126334645