android 自定义view: 矩形图表

Posted 史大拿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 自定义view: 矩形图表相关的知识,希望对你有一定的参考价值。

本系列自定义View全部采用kt

系统mac

android studio: 4.1.3

kotlin version1.5.0

gradle: gradle-6.5-bin.zip

本篇效果:

tips: 本篇是在上一篇的基础上来绘制的,背景表格,和左侧文字都是上一篇的东西, 如果不清楚可以先学习上一篇!

绘制网格和文字

这是上一篇的代码,我把它封装成了一下,如果需要请下载看看吧 !

class E2RectChartBlogView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BaseChartView(context, attrs, defStyleAttr) 

    init 
        // 初始化数据
        originList = arrayListOf(
            148, 120, 21, 60, 10,
            70, 80, 100, 222, 74,
        )
    

    override fun onDraw(canvas: Canvas) 
        canvas.scale(0.8f, 0.8f, width / 2f, width / 2f)
        // 绘制网格
        super.drawGrid(canvas)
        // 绘制文字
        super.drawLeftText(canvas)
    

绘制矩形

首先绘制矩形,先得确定要绘制到什么地方:

如图:

我们需要将矩形绘制到每一格的正中心位置,

假设我们需要绘制矩形高度为 90

我们知道

  • 每一格的宽度 eachWidth

  • 需要绘制的矩形宽度(rectWidth) = eachWidth * 0.6

  • 每一格的矩形高度 = View#height / 数组最大值

  • 对应的高度 = 每一个的矩形高度 * 90

那么最终矩形的坐标为

  • left = (eachWidth / 2f ) - (eachWidth * 0.6) / 2f
  • right = left + rectWidth
  • top = 每一个的矩形高度 * 高度对应的值
  • bottom = height

来看看代码:

 // 每一格矩形的宽
    private val eachWidth: Float
        get() 
            return width / verticalCount.toFloat()
         

override fun onDraw(canvas: Canvas) 
   
   // 每一格的矩形宽度
   val eachWidth = eachWidth
   // 矩形的宽度
   val rectWidth = eachWidth * 0.6f

   // 计算出每一格的高度
   val eachGrid = height.toFloat() / originMax

   // 绘制矩形
   drawRect(canvas, eachWidth, rectWidth, eachGrid)
 

 /*
  * 作者:史大拿
	* 创建时间: 9/26/22 3:28 PM
	* @param eachWidth: 每一格的矩形宽度
	* @param rectWidth: 每一个矩形的宽
  * @param eachGridHeight: 每一格矩形的高度
	*/
private fun drawRect(
  canvas: Canvas,
  eachWidth: Float,
  rectWidth: Float,
  eachGridHeight: Float
) 
  var tempLeft = eachWidth / 2f - rectWidth / 2f
  originList.forEach 

    val left = tempLeft
    val top = height - eachGridHeight * it
    val right = left + rectWidth
    val bottom = height.toFloat()

    canvas.drawRoundRect(left, top, right, bottom, 0f, 0f, paint)
    tempLeft += eachWidth
  

可以看出,绘制成功了,但是绘制出表格外面了,这种情况是不允许的,

和上一篇的套路一样,只保留屏幕内的数据(裁剪一下)

override fun onDraw(canvas: Canvas) 
   canvas.withSave 
     // 裁剪
     canvas.clipRect(0, 0, width, height)
     // 绘制矩形
     drawRect(canvas, eachWidth, rectWidth, eachGrid)
   

手势监听

手势监听还是用到了 GestureDetectorCompat

如果手势监听使用有点模糊 建议看这一篇 android 图解 PhotoView,从‘百草园’到‘三味书屋’!

首先先让他滚动起来

private var offsetX = 0f
private var downX = 0f
private var originX = 0f

private inner class E2RectGesture : GestureDetector.OnGestureListener 
    override fun onDown(event: MotionEvent): Boolean 
				// 记录按下位置
        downX = event.x
        // 记录上一次偏移量
        originX = offsetX
        return true
    
    override fun onScroll(
        e1: MotionEvent,// down按下的位置
        event: MotionEvent, // 当前滑动的事件
        distanceX: Float, // event - e1 的X距离
        distanceY: Float // event - e2 的Y距离
    ): Boolean 
        when (event.action) 
            MotionEvent.ACTION_MOVE -> 
                // 当前偏移位置 = 当前位置 - 按压位置 + 原始偏移量
                offsetX = event.x - downX + originX

                // 修复X轴距离 保证offsetX 不会滑出屏幕
                repairOffSetX()
            
        

        invalidate()
        return false
    

  ....


private fun repairOffSetX() 
  // 限制滑动距离
  if (offsetX > 0) 
    offsetX = 0f
  

  if (offsetX <= -(originList.size * eachWidth - width)) 
    	offsetX = -(originList.size * eachWidth - width)
  

使用GestureDetectorCompat

// 手势监听
private val gestureDetectorCompat = GestureDetectorCompat(context, E2RectBlogGesture())

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean 
    gestureDetectorCompat.onTouchEvent(event)
    return true

记得设置offsetX 用来滑动

/*
     * 作者:史大拿
     * 创建时间: 9/26/22 3:28 PM
     * @param eachWidth: 每一格的矩形宽度
     * @param rectWidth: 每一个矩形的宽
     * @param eachGridHeight: 每一格矩形的高度
     */
    private fun drawRect(
        canvas: Canvas,
        eachWidth: Float,
        rectWidth: Float,
        eachGridHeight: Float
    ) 
        var tempLeft = eachWidth / 2f - rectWidth / 2f
        originList.forEach 
            val left = tempLeft + offsetX // TODO 设置偏移量
            val top = height - eachGridHeight * it
            val right = left + rectWidth
            val bottom = height.toFloat()

            /// 绘制矩形
           ....
        
    

设置fling事件

因为只可以左右滑动,所以只需要关心x轴即可

fling事件还是用到了OverScroller

private inner class E2RectGesture : GestureDetector.OnGestureListener 
		...
  	
  	 override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent?,
            velocityX: Float,
            velocityY: Float
        ): Boolean 
            val startX = offsetX.toInt() // 只需要关心x轴即可
            val startY = 0

            val vX = velocityX.toInt()
            val vY = 0

            val minX = -(originList.size * eachWidth - width).toInt()
            val maxX = 0

            val minY = 0
            val maxY = 0

            overScroll.fling(
                startX, startY,
                vX, vY,
                minX, maxX,
                minY, maxY,
                0, 0
            )

            postOnAnimation(this@E2RectChartView)
            return true
        


 override fun run() 
   // 计算偏移量 返回是否完成
   val isOver = overScroll.computeScrollOffset()
   if (isOver) 
     offsetX = overScroll.currX.toFloat()
     repairOffSetX() // 修复X轴距离
     invalidate()
     postOnAnimation(this) //
   
 

这里代码也比较简单,就不过多赘述了.

设置动画

动画其实就是在2秒内一直的变换高度即可

变换进度从0到1变换

只需要高度 * 变换进度即可

先来定义一个动画

private var currentFraction = 0f

private val admissionAnimation by lazy 
    val animator = ObjectAnimator.ofFloat(0f, 1f)
    animator.duration = 2000 // 动画时间
    animator.addUpdateListener 
        currentFraction = it.animatedValue as Float
        invalidate()
    
    animator

currentFraction 会在两秒内,从0 变换到1

现在只需要将currentFraction设置给矩形的高度即可

private fun drawRect(
        canvas: Canvas,
        eachWidth: Float,
        rectWidth: Float,
        eachGridHeight: Float
    ) 
        var tempLeft = eachWidth / 2f - rectWidth / 2f
        originList.forEach 

            val left = tempLeft + offsetX // 偏移量
            val top = height - eachGridHeight * it * currentFraction // 将当前进度设置给top
            val right = left + rectWidth
            val bottom = height.toFloat()

            canvas.drawRoundRect(left, top, right, bottom, 0f, 0f, paint)
            tempLeft += eachWidth
        
    

添加文字

文字的x,y位置和矩形的top位置是一样的

 override fun onDraw(canvas: Canvas) 
   
   canvas.withSave 
     // 裁剪
     canvas.clipRect(0, 0, width, height)
     // 绘制矩形
     drawRect(canvas, eachWidth, rectWidth, eachGrid)
   
		// 绘制文字 
   drawText(canvas, eachWidth, eachGrid)
 

 /*
	* 作者:史大拿
	* 创建时间: 9/26/22 4:04 PM
	* @param eachWidth: 每一格的矩形宽度
	* @param eachGridHeight: 每一格矩形的高度
	*/
private fun drawText(canvas: Canvas, eachWidth: Float, eachGridHeight: Float) 
        var tempX = eachWidth / 2f
        originList.forEachIndexed  _, value ->
            val text = "$value"

            // 文字的宽
            val textWidth = paint.measureText(text)

            val x = tempX - textWidth / 2f + offsetX
            val y = (height - eachGridHeight * value) - paint.fontMetrics.bottom

            // 如果在范围内才绘制
            if (x >= 0 && x <= width) 
                canvas.drawText(text, x, y, paint)
            

            tempX += eachWidth
        
    

可以看出,文字绘制成功了,但是我们想要的是和矩形一样一直变换

那么肯定和currentFraction有关系

同样的套路,y轴 * currentFraction即可

  private fun drawText(canvas: Canvas, eachWidth: Float, eachGridHeight: Float) 
        var tempX = eachWidth / 2f
        originList.forEachIndexed  _, value ->
            val text = String.format("%.0f", value * currentFraction)

            // 文字的宽
            val textWidth = paint.measureText(text)

            val x = tempX - textWidth / 2f + offsetX
            val y = (height - eachGridHeight * value * currentFraction) - paint.fontMetrics.bottom


            // 如果在范围内才绘制
            if (x >= 0 && x <= width) 
                canvas.drawText(text, x, y, paint)
            

            tempX += eachWidth
        
    

完整代码

原创不易,您的点赞就是对我最大的帮助!

以上是关于android 自定义view: 矩形图表的主要内容,如果未能解决你的问题,请参考以下文章

android 自定义view: 矩形图表

android自定义View: 绘制图表

android自定义View: 绘制图表

android自定义View: 绘制图表

Android自定义圆角矩形进度条2

Android自定义View实现可拖拽的进度条