图表CanvasChartView(三):手势滑动方案二

前言

之前我们使用了第一种滑动方案,虽然滑动的效果有了,但是和我们期望的还是有很大差距,例如滑动的时候,坐标轴跟着滑动图标一起滑动了,导致我们向右滑动一段距离之后,就看到不坐标轴了。

但是我们也找到了解决办法:

不移动View的scrollX,而是对画布Canvas进行偏移。

今天我们就来实现这种方案。

正文

整体改造的思路:

1、手动记录偏移值,相当于之前的ScrollX

2、computeScroll只用来计算新的偏移值,然后重绘

3、修改绘制onDraw方法,根据偏移值绘制图表

首先实现第一个修改点:

 
 
    /**
     * 记录手指划过的距离
     * */
    protected var offsetX: Float = 0f

    /**
     * 手指滑动距离的备份,用于判断是否手指移动了
     * */
    protected var offsetXTemp: Float = 0f
 /**
     * 重写手势
     * */
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        // 如果不能滑动,不处理手势滑动
        if (!canScroll) {
            return false
        }
        // 计算滑动的速度
        createVelocityTracker(event)
        when (event.action) {
        // 记录手指按下的坐标
            MotionEvent.ACTION_DOWN -> {
                xDown = event.rawX
            }
        // 手势滑动
            MotionEvent.ACTION_MOVE -> {
                // 更新xDown的坐标
                if (xMove != -1f) {
                    xDown = xMove
                }
                // 备份偏移的位置
                offsetXTemp = offsetX
                // 记录当前的x坐标
                xMove = event.rawX
                // 计算移动的位置
                offsetX += (xDown - xMove)
                // 对移动的位置进行范围检查
                // 如果小于0,那么等于0
                if (offsetX < 0) {
                    offsetX = 0f
                }
                // 如果已经大于了最右边界
                else if (offsetX > maxWidth - width) {
                    offsetX = maxWidth - width.toFloat()
                }
                // 检查偏移值是否发生了改变
                if (offsetX != offsetXTemp) {
                    // 重绘
                    invalidate()
                }
            }
        // 手势抬起
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                val dx = calculateFlingDistance()
                // startScroll()方法来初始化滚动数据并刷新界面
                scroller.startScroll(offsetX.toInt(), 0, dx, 0)
                invalidate()
                recycleVelocityTracker()
                // 重置配置信息
                reset()
            }
        }
        return true
    }

先定义了两个变量,offsetX记录X方向上的偏移值,offsetTemp主要是了为了不必要的重绘,如果偏移值没有发生变化,重绘也是没有必要的。

同样要滑动边界的检查,用offsetX替换掉之前的scrollX,这样就完成了我们的第一步。

第二步,修改computeScroll方法,也是非常的简单:

override fun computeScroll() {
        // 第二步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        if (scroller.computeScrollOffset()) {
            offsetX = scroller.currX.toFloat()
            Log.e("lzp", "currX is :${scroller.currX}")
            invalidate()
        }
    }

我们把scroller计算的结果和offsetX进行计算,得到新的offsetX。

接下来是最后一步,也是修改中最复杂的一步:

我们希望图表可以滑动,就需要考虑绘制的效率,内存等等问题,例如RecyclerView,如果没有对item进行复用和缓存,无限制的创建View,内存肯定就要飞速的上涨,最终程序崩溃。

如果我们的图表有几千条几万条数据,把所有的数据都绘制出来,那内存肯定就起飞了,并且绘制的速度也会变慢,用户体验也会非常的差,所以首要任务,是找到绘制的范围:开始位置和结束位置。

/**
     * 根据偏移值,计算绘制的数据的开始位置
     * */
    protected fun getDataStartIndex(): Int {
        // 计算每一个刻度的宽度
        val markWidth = width / xLineMarkCount
        // 计算已经偏移了几个刻度
        val index = (offsetX / markWidth).toInt()
        // 为了绘制第一条能够和前一条有连线,所以我们要减1
        return Math.max(0, index - 1)
    }

    /**
     * 根据偏移值,计算绘制的数据的结束位置
     * */
    protected fun getDataEndIndex(startIndex: Int): Int {
        // 如果绘制的是第一个,直接返回偏移值
        return Math.min(startIndex + xLineMarkCount + 2, adapter!!.maxDataCount)
    }

    /**
     * 计算canvas绘制的偏移值
     *
     * 偏移值 - 刻度值宽度 * 开始位置,相当于对刻度值宽度取模
     * */
    protected fun getCanvasOffset(): Float {
        val markWidth = width / xLineMarkCount
        // 计算已经偏移了几个刻度
        val index = (offsetX / markWidth).toInt()
        // 如果绘制的是第一个,直接返回偏移值
        return if (index == 0) {
            -offsetX % markWidth
        }
        // // 为了绘制第一条能够和前一条有连线,所以我们要减去刻度值的宽度
        else {
            -offsetX % markWidth - markWidth
        }
    }

首先计算绘制开始的位置,用offsetX与一个的刻度的宽度相除,得到了已经滑过的刻度个数,就是数据列表的索引,超出屏幕的数据就不需要绘制了,然后需要绘制当前位置与前一个位置的数据连线,所以这里就简单粗暴的对第一个应该绘制的数据的索引-1,如果是第一个,就直接返回0。

计算绘制结束的刻度,用开始的index加上需要绘制的刻度,因为要可能要绘制与之后的数据连线,并且开始位置已经-1,所以这里先简单的+2,不要忘了和数据的长度进行比较,否则就会出现越界异常。

最后计算canvas应该偏移的位置,滑动的刻度已经不再考虑范围之内,但是这里会有两种情况:

1、如果是第一条数据,他没有之前的连线,所以直接用偏移值就可以了

2、如果不是第一条,因为-1的缘故,所以还要-markWidth。

ok,最关键的三个值我们已经都准备好了,就可以放心的修改onDraw方法了:

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 保存一下canvas的状态
        canvas.save()
        // 绘制X轴和Y轴
        drawXYLine(canvas)

        // 从这里开始,我们要对canvas进行偏移
        canvas.translate(getCanvasOffset(), 0f)

        // 绘制每一条数据之间的间隔虚线
        drawDashLine(canvas)
        // 绘制数据
        drawData(canvas)
        // 恢复一下canvas的状态
        canvas.restore()
    }   /**
     * 绘制数据之间
     *
     * 根据偏移值计算要绘制的区域
     * */
    private fun drawDashLine(canvas: Canvas) {
        // 通过x轴的刻度间隔,计算x轴坐标
        val xItemSpace = width / xLineMarkCount.toFloat()
        // 设置画笔的效果
        paint.color = dashLineColor
        paint.strokeWidth = dashLineWidth
        paint.pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 1f)
        // 画条目之间的间隔虚线,从Data的开始位置绘制到结束位置
        val startIndex = getDataStartIndex()
        val endIndex = getDataEndIndex(startIndex)
        var index = startIndex
        while (index < endIndex) {
            val startY = xItemSpace * (index - startIndex)
            val path = Path()
            path.moveTo(startY, 0f)
            path.lineTo(startY, height.toFloat())
            canvas.drawPath(path, paint)
            index++
        }
    }
/**
     * 绘制数据曲线
     * */
    private fun drawData(canvas: Canvas) {
        // 设置画笔样式
        paint.pathEffect = null
        // 得到数据列表, 如果是null,取消绘制
        val dataList = adapter?.getData() ?: return
        // 绘制每一条数据列表
        for (item in dataList) {
            drawItemData(canvas, item)
        }
    }
   /**
     * 绘制一条数据曲线
     * */
    private fun drawItemData(canvas: Canvas, data: List<ChartBean>) {
        // 通过x轴的刻度间隔,计算x轴坐标
        val xItemSpace = width / xLineMarkCount
        val path = Path()
        val dotPath = Path()
        // 绘制开始位置到结束位置的数据
        val startIndex = getDataStartIndex()
        val endIndex = getDataEndIndex(startIndex)
        var index = startIndex
        while (index < endIndex) {
            // 因为数据的长度不统一,所以这里要做数据的场地检查
            if (index >= data.size){
                break
            }
            // 计算每一个点的位置
            val item = data[index]
            // 计算绘制的x坐标
            val xPos = (xItemSpace / 2 + (index - startIndex) * xItemSpace).toFloat()
            // 计算绘制的y坐标
            val yPos = calculateYPosition(item)
            // 设置Path路径
            if (index == startIndex) {
                path.moveTo(xPos, yPos)
            } else {
                path.lineTo(xPos, yPos)
            }
            dotPath.addCircle(xPos, yPos, dotWidth, Path.Direction.CW)
            // 绘制文字
            drawText(canvas, item, xPos, yPos)
            index++
        }
        // 绘制曲线
        paint.style = Paint.Style.STROKE
        paint.color = chartLineColor
        paint.strokeWidth = chartLineWidth
        canvas.drawPath(path, paint)
        // 绘制圆点
        paint.color = dotColor
        paint.style = Paint.Style.FILL
        canvas.drawPath(dotPath, paint)
    }

首先直接绘制XY坐标轴,然后对Canvas绘制的位置进行偏移,绘制虚线和连线,这里使用了刚刚计算的三个值,只绘制了展示的位置的图表,而不是全部画出来。

OK,运行程序欣赏一下我们的劳动成果:


总结

终于看到了我们期望的效果,到现在为止图表绘制的核心功能我们已经攻破了,但是作为程序员还是要有追求的,我们的demo还是有很大的不足,例如:

  1. 滑动不够流畅
  2. 创建了很多的Path对象,内存浪费
  3. 一些其他的计算,例如文字的宽度,刻度宽度等等

下一篇的内容就是对刚刚完成的CanvasChartView进行优化。

github下载地址(本文的内容请看view2包中的类)

补充:最近工作比较忙,所以更新的慢了一点,现在优化后的CanvasChartView已经在代码中了:

view1:对应第一种方案的View

view2:对应第二种方案,也就是今天的Demo

view3:对view2优化后的View。

心急的小伙伴可以先直接看代码~

猜你喜欢

转载自blog.csdn.net/u011315960/article/details/80486535
今日推荐