美文网首页Android技术知识Android开发经验谈Android开发
图表CanvasChartView(三):手势滑动方案二

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

作者: 珠穆朗玛小王子 | 来源:发表于2018-05-29 18:52 被阅读3次

    前言

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

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

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

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

    正文

    整体改造的思路:

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

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

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

    首先实现第一个修改点:

        /**
         * 记录手指划过的距离
         * */
        protected var offsetX: Float = 0f
    
        /**
         * 手指滑动距离的备份,用于判断是否手指移动了
         * */
        protected var offsetXTemp: Float = 0f</pre>
    
        /**
         * 重写手势
         * */
        @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,运行程序欣赏一下我们的劳动成果:

    image

    总结

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

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

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

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

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

    view1:对应第一种方案的View

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

    view3:对view2优化后的View。

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

    相关文章

      网友评论

        本文标题:图表CanvasChartView(三):手势滑动方案二

        本文链接:https://www.haomeiwen.com/subject/qrgajftx.html