咱们知道,大数据时代,折线图是展示数据最重要的工具之一,不管在移动端还是PC端都有着极其繁复的折线图统计产品,比方:

  • 个人中心的根底折线图

Echars-折线图系列

  • 招商银行月度明细的弧度折线图

Echars-折线图系列

  • 支付宝年收入的根底折线图

Echars-折线图系列

  • 除了上面各类移动端折线图,PC端还有各类常用的折线图,如下图,这儿我就不一一列举了。

Echars-折线图系列

经过前面章节的学习,咱们应该对制作和手势以及动画有了必定根底的知道。当然咱们也在前面章节学习了特别炫酷和具有难度的自定义内容,可是Echars作为前后端开发中最常用的统计图,咱们何不来测验挑战一波,本篇将带领咱们完成Echars 里具有代表性的部分折线图,包括:

  • 根底折线图;
  • 根底折线面积图;
  • 折线堆叠图。

这些折线图是许多网站和移动端最常用的折线图,掌握了它们,根本就能够满意咱们日常开发运用了。在工作中,你能够触类旁通,这样在面对产品时,也能够很自傲地说,“只需你们想不到,没有我办不到!”

接下来咱们就逐一来完成这三种折线图。

制作根底折线图

Echars-折线图系列

首要要阐明一下,制作进程中为了更好的让咱们了解原点方位、制作区域等,都会开端采用一些布景色彩或许制作几许区域来帮助咱们了解。

1. 创立制作区域

或许许多同学会问,为什么老要创立制作区域?作为初学者,关于二维坐标系的熟练度和改换知道或许稍加缺少。为了让这部分伙伴在学习进程中能更好了解小册,所以咋们最好是创立制作区域,符号不同的布景色彩。

图片如下,绿色部分是屏幕布景,黄色部分是自定义View制作区域,原点在左上角:

Echars-折线图系列

/*com/example/draw_android/section16_line_chart/a_basic_line_chart/BasicLineChartView.kt*/
package com.example.draw_android.section16_line_chart.a_basic_line_chart
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
/**
 * Created by wangfei44 on 2022/1/29.
 */
class BasicLineChartView constructor(context: Context, attributeSet: AttributeSet) :
    View(context, attributeSet) {
    init {
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(
            0f, 0f, 60f,
            Paint().apply {
                color = Color.BLACK
                style = Paint.Style.FILL
            })
    }
}
2. 创立坐标系和刻度

制作最重要的一个进程是什么呢?当然是创立坐标系。在前文咱们就说过,因为所有的制作都是依赖于坐标系作为参照来进行制作,如果坐标系不符合当时制作的内容,每一个点的制作将会带来大量的核算,每一次不同方位的制作都会带来许多次坐标的改换,参加手势之后,手势坐标系和制作坐标系之间的映射将是极大的挑战,可见创立坐标系有多重要。

此事例,很明显,将圆点放置在左下角,向上向右作为正方向是咱们最明智的创立挑选。坐标系的创立进程也便是咱们画布改换进程。

画布坐标系改换代码如下:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    //画布全体向下平移
    canvas.translate(0f,height.toFloat())
    //画布沿X轴纵向翻转
    canvas.scale(1f,-1f)
    //原点方位画圆圈做为参照物
    canvas.drawCircle(
        0f, 0f, 60f,
        Paint().apply {
            color = Color.BLACK
            style = Paint.Style.FILL
        })
}

Echars-折线图系列

制作X轴刻度时,依据数据最大值和宽度以及刻度个数进行分段。经过循环改换画布制作刻度线。

Echars-折线图系列

private val marginWidth = 60f
private fun drawBottomScaleLine(canvas: Canvas) {
    //制作最底部一条横线
    canvas.drawLine(marginWidth, marginWidth, width - marginWidth, marginWidth,
        Paint().apply {
            color = Color.BLACK
            style = Paint.Style.STROKE
            strokeWidth = 2f
        })
    //制作水平方向刻度线
    //总的宽度= 总的画布宽度-两头的边距
    val widthScale = width - 2 * marginWidth
    //水平方向一共7个天数,8个刻度短线,每一份刻度之间间隔 = widthScale/7
    val eachScale = widthScale / 7f
    //保存当时画布矩阵到仓库
    canvas.save()
    for (index in 0 until 8) {
        canvas.drawLine(marginWidth, marginWidth, marginWidth, marginWidth - 10,
            Paint().apply {
                color = Color.BLACK
                style = Paint.Style.STROKE
                strokeWidth = 2f
            })
        canvas.translate(eachScale, 0f)
    }
    canvas.restore()
}
private fun drawOtherScaleLine(canvas: Canvas) {
    //Y轴方向一共6条线进行逐步制作
    val eachYScale = (height - 2*marginWidth)/6f
    canvas.save()
    for (index in 0 until 6){
        canvas.translate(0f,eachYScale)
        canvas.drawLine(marginWidth, marginWidth, width - marginWidth, marginWidth,
            Paint().apply {
                color = Color.argb(255,215,215,215)
                style = Paint.Style.STROKE
                strokeWidth = 2f
            })
    }
    canvas.restore()
}
2. 制作刻度文字

有必要清楚知道到,制作文字的难点便是画布状况以及画布坐标系改换的运用。学好这一点,任其复杂多变,千姿百态,你都能够制作出它地点的方位和款式。

接下来经过图文阐明进行制作:

X轴最下边的文字和Y轴左面文字的制作,首要咋们剖析一下文字地点方位,文字都显现在刻度中心,也便是咱们文字制作的方位能够是刻度中心的方位-位子长度的一般。如下图 start = a-textWidth/2

Echars-折线图系列

Echars-折线图系列

private fun drawXScaleText(canvas: Canvas) {
    val xTextPaint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 2f
        textSize = 18f
    }
    //总的宽度= 总的画布宽度-两头的边距
    val widthScale = width - 2 * marginWidth
    val eachScale = widthScale / 7f
    //保存当时画布矩阵到仓库
    canvas.save()
    canvas.translate(marginWidth, 20f)
    for (index in 0 until 7) {
        //画布从左往右逐步平移每个刻度,便利制作。
        canvas.save()
        //字体是上下颠倒,这儿能够经过scale进行回转
        canvas.scale(1f, -1f)
        val rect = Rect()
        xTextPaint.getTextBounds(xScaleText[index], 0, xScaleText[index].length, rect)
        canvas.drawText(
            xScaleText[index], eachScale / 2f - rect.width() / 2f,
            0f,
            xTextPaint
        )
        canvas.restore()
        canvas.translate(eachScale, 0f)
    }
    //将上次保存的画布矩阵推出仓库,画布坐标回到左下角
    canvas.restore()
}

接下来是Y轴方向的文字制作,你也能够自己找个草纸操作下。

相同能够剖析,文字开端制作的方位,间隔Y轴左面一个文字宽度稍宽点,而竖直方向间隔刻度线文字高度的一半。

Echars-折线图系列

Echars-折线图系列

private fun drawYScaleText(canvas: Canvas) {
    val xTextPaint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 2f
        textSize = 18f
    }
    //Y轴方向一共6条线进行逐步制作
    val eachYScale = (height - 2 * marginWidth) / 6f
    canvas.save()
    canvas.translate(marginWidth, marginWidth)
    for (index in 0 until 7) {
        //画布从左往右逐步平移每个刻度,便利制作。
        canvas.save()
        //字体是上下颠倒,这儿能够经过scale进行回转
        canvas.scale(1f, -1f)
        val rect = Rect()
        val yScaleText = (50*index).toString()
        xTextPaint.getTextBounds(
            yScaleText,
            0,
            yScaleText.length,
            rect
        )
        canvas.drawText(
            yScaleText, -(rect.width()+20f),
            rect.height()/2f,
            xTextPaint
        )
        canvas.restore()
        canvas.translate(0f, eachYScale)
    }
    canvas.restore()
}
3.制作数据

坐标系创立和刻度制作之后,就该进行制作数据了,这是这是最重要的一步了。

制作数据进程中最重要的点是什么呢?自定义过的开发者应该都明白,将数据映射到屏幕画布坐标系内才是要点,那咱们应该如何做好映射联系呢?接下来咋们一步步进行剖析。

咱们能够看到最大刻度scaleMax=300,而画布高度mHeight=height-marginWidth*2,那么每一单位刻度的实践像素高度 scale = mHeight / scaleMax,那每个数据的实践像素高度咱们也能够核算 = data * scale

Echars-折线图系列

private fun drawData(canvas: Canvas) {
    val widthScale = width - 2 * marginWidth
    //水平方向一共7个天数,8个刻度短线,每一份刻度之间间隔 = widthScale/7
    val eachScale = widthScale / 7f
    //每一刻度所占的实践像素
    val scale = (height - marginWidth * 2) / 300f
    canvas.save()
    canvas.translate(marginWidth, marginWidth)
    val path = Path()
    //制作起点开端
    path.moveTo(eachScale / 2, dataArray[0] * scale)
    for (index in 1 until dataArray.size) {
        path.lineTo(eachScale / 2 + eachScale * index, dataArray[index] * scale)
    }
    canvas.drawPath(path, Paint().apply {
        color = Color.argb(255, 103, 123, 211)
        style = Paint.Style.STROKE
        strokeWidth = 6f
    })
    for (index in 0 until dataArray.size){
        //制作圆圈
        canvas.drawCircle(
            eachScale / 2 + eachScale * index,
            dataArray[index] * scale,
            6f,
            Paint().apply {
                color = Color.WHITE
                style = Paint.Style.FILL
                strokeWidth = 6f
            })
         //制作圆的边   
        canvas.drawCircle(
            eachScale / 2 + eachScale * index,
            dataArray[index] * scale,
            8f,
            Paint().apply {
                color = Color.argb(255, 103, 123, 211)
                style = Paint.Style.STROKE
                strokeWidth = 6f
            })
    }
    canvas.restore()
}
4.增加动画

动画是UI中引起人们留意力,也是产品美感设计中极其重要的元素。一个产品中的动画,或许更让其更受欢迎,更具有生机和魅力。

咱们平常是否留意过,自定义中的动画一般都是呈现在哪里呢?据我调查和开发经验来说,大多都是增加到其途径或许制作区域,制作途径的动画当然脱离不了途径的丈量了。接下来咋们会持续知道和巩固之前学过的内容。

动画作用咱们能够看到途径从左往右逐渐呈现。途径的丈量PathMeasure以及getSegment获取任意两方位间的途径,附加一个动画就能够搞定,具体API介绍能够看前面章节。

Echars-折线图系列

val mPathMeasure=PathMeasure(path, false)
val length:Float= mPathMeasure.length
val stop: Float = length
val start = 0f
//用于截取整个path中某个片段,经过参数startD和stopD来操控截取的长度,
// 并将截取后的path保存到参数dst中,最终一个参数表示开始点是否运用moveTo将途径的新开始点移到成果path的开始点中,
// 通常设置为true
val resultGreenPath=Path()
mPathMeasure.getSegment(start, stop*(mCurAnimValue), resultGreenPath, true)
5.布局自适应

判别一个产品的兼容性,我想都离不开布局的适配了。当你自定义一个精巧的View,是否能够成功适配不同的手机分辨率?不同的平板分辨率?不同的小窗形式?不同的浮窗形式?等。那屏幕适配需求留意哪些环节呢?

我的答复是:

  1. 处理好数据和屏幕坐标系之间的映射。
  1. 关于画布改换进程中制作考虑的细节。例如,边距的考虑,画布改换时候的关于全体坐标的影响,以及画布状况的保存和开释。

咱们经过平板电脑形式,来看看不同尺寸是否适配,好的自定义必定具有很好的屏幕适配才能。由于上面咋们都是经过数据和画布的宽高进行核算,所以猜测适配应该还能够的。下面经过录制GIF看看适配才能。

Echars-折线图系列

制作根底折线面积图

二、制作根底折线面积图

Echars-折线图系列

接下来咋们来制作Echars事例里边的面积图。

1.制作坐标系和刻度

经过前面的章节咱们已经熟练掌握了画布的改换,所以坐标系和刻度的制作有许多方式能够完成。不能以我的事例代码为唯一写法。

能够看到此事例和事例一的坐标系以及刻度线简直共同,竖直方向有6个刻度,水平方向有7个刻度。不同在于x轴刻度文字在每个刻度下方,并不在刻度中心,且坐标方位和画布区域有margin大小的空白区域来制作文字刻度。

Echars-折线图系列

经过上图剖析咋们最好是将坐标系改换到左下角才会更好的进行制作。从左上角改换到左下角。
canvas.translate(0f, height.toFloat())
canvas.scale(1f, -1f)
canvas.translate(margin, margin)
如下图是画布改换进程1-2-3-4完成。

Echars-折线图系列

咱们核算刻度间隔,然后经过canvas.translate进行平移画布经过canvas.drawLine制作刻度线。

Echars-折线图系列

package com.example.draw_android.section16_line_chart.b_basic_area_chart
/*app/src/main/java/com/example/draw_android/section16_line_chart/b_basic_area_chart/BasicAreaChartView.kt*/
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
/**
 * Created by wangfei44 on 2022/2/4.
 */
class BasicAreaChartView constructor(context: Context, attributeSet: AttributeSet) :
    View(context, attributeSet) {
    private val margin = 150f
    private val data = arrayListOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
    private val dataY = arrayListOf(820, 932, 901, 934, 1290, 1330, 1320)
    private val linePaint = Paint().apply {
        color = Color.GRAY
        style = Paint.Style.STROKE
        strokeWidth = 4f
    }
    init {
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawScaleLine(canvas)
    }
  //制作刻度
  private fun drawScaleLine(canvas: Canvas) {
    canvas.translate(0f, height.toFloat())
    canvas.scale(1f, -1f)
    canvas.translate(margin, margin)
    //制作X轴的刻度
    canvas.save()
    val spaceWidth = (width - margin * 2) / (data.size-1)
    for (index in 0 until data.size) {
        //制作X轴上刻度线
        canvas.drawLine(0f, 0f, 0f, -lineLength, linePaint)
        //经过水平改换移动画布来循环制作刻度
        canvas.translate(spaceWidth, 0f)
    }
    //康复画布坐标系到左下角。
    canvas.restore()
    //制作竖直方向的线
    canvas.save()
    val spaceHeight = (height - margin * 2) / dataY.size
    for (index in 0..5) {
        //制作平行于X轴线
        canvas.drawLine(0f, 0f, width.toFloat() - margin * 2, 0f, linePaint)
        canvas.translate(0f, spaceHeight)
    }
    canvas.restore()
   }
}

Echars-折线图系列

2.制作文字

不知道小伙伴们换记住默许坐标系制作文字默许在那个方位呢?咱们能够记忆或许猜测一下。

下面代码咋们要不先来看看?

//制作文字
private fun drawScaleText(canvas: Canvas) {
    canvas.save()
    canvas.drawText("Mon",0f,0f,textPaint)
    canvas.restore()
}

想必咱们也清楚,一个明晰的制作思路和方案是一个美好的开端,万事前做好剖析,依据原理和提出的方案分步进行。

学习了前面章节,咱们知道默许字体制作如下图1显现,在x轴正方向上方,y轴负方向。因为咱们画布坐标系是图二向上Y正,向右X正,所以文字制作看起来是颠倒的。所以制作时候咱们只需求将坐标系沿x轴镜像回转。接下来咱们能够经过字体的宽高进行平移画布到文字具体方位。如下图不难看出文字向下移动间隔=刻度线长度+文字高度,向左移动文字宽度/2。

Echars-折线图系列

Echars-折线图系列

//制作文字
private fun drawScaleText(canvas: Canvas) {
    canvas.save()
    val spaceWidth = (width - margin * 2) / (data.size-1)
    for (index in 0 until data.size) {
        val str = data[index]
        val rect = Rect()
        textPaint.getTextBounds(str,0,str.length,rect)
        canvas.save()
        canvas.scale(1f, -1f)
        canvas.translate(-rect.width()/2f,rect.height().toFloat()+lineLength)
        canvas.drawText(data[index], 0f, 0f, textPaint)
        canvas.restore()
        canvas.translate(spaceWidth,0f)
    }
    canvas.restore()
}

Echars-折线图系列

Y轴方向同理自己剖析,画布向左移动文字的宽度+必定的间隔(lineLength)。向下移动文字高度的二分之一即可。

canvas.save()
//每个刻度线之间的实践高度像素
val spaceHeight = (height - margin * 2) / dataY.size
for (index in 0 until 6) {
    val str = (300*index).toString()
    //丈量文字的rect容器
    val rect = Rect()
    textPaint.getTextBounds(str,0,str.length,rect)
    canvas.save()
    canvas.scale(1f, -1f)
    //向左向下移动画布方位
    canvas.translate(-rect.width().toFloat()- lineLength,rect.height()/2f)
    canvas.drawText(str, 0f, 0f, textPaint)
    canvas.restore()
    canvas.translate(0f,spaceHeight)
}
canvas.restore()

Echars-折线图系列

2.制作数据

能够看到数据刻度在Y轴最大数值yMax=1500,而实践的刻度高度mH= height-2*margin,能够核算出每一刻度所占的像素为scaleH= mH/yMax。

private fun drawData(canvas: Canvas) {
    val spaceWidth = (width - margin * 2) / (data.size - 1)
    val scaleYH = (height - margin * 2) / 1500
    val path = Path()
    path.moveTo(0f, scaleYH * dataY[0])
    for (index in 1 until dataY.size) {
        path.lineTo(spaceWidth * index, scaleYH * dataY[index].toFloat())
    }
    canvas.drawPath(path, lineDataPaint)
    //圆圈内白色部分
    for (index in 1 until dataY.size) {
        canvas.drawCircle(
            spaceWidth * index,
            scaleYH * dataY[index].toFloat(),
            6f,
            circleDataPaint
        )
    }
    //圆圈外部蓝色部分
    for (index in 0 until dataY.size) {
        canvas.drawCircle(
            spaceWidth * index,
            scaleYH * dataY[index].toFloat(),
            6f,
            lineDataPaint
        )
    }
}

Echars-折线图系列

制作填充部分,之前课程都具体讲过,只需求将折线部分闭合,经过画布填充款式就能够搞定。
style = Paint.Style.FILL

Echars-折线图系列

//制作闭合区域
val innerPath = Path()
innerPath.moveTo(0f, scaleYH * dataY[0])
for (index in 1 until dataY.size) {
    innerPath.lineTo(spaceWidth * index, scaleYH * dataY[index].toFloat())
}
innerPath.lineTo(spaceWidth * (dataY.size-1),0f)
innerPath.lineTo(0f,0f)
innerPath.close()
canvas.drawPath(innerPath, innerPaint)

Echars-折线图系列

3.增加动画
private fun drawData(canvas: Canvas) {
    val spaceWidth = (width - margin * 2) / (data.size - 1)
    val scaleYH = (height - margin * 2) / 1500
    //制作闭合区域
    val innerPath = Path()
    innerPath.moveTo(0f, scaleYH * dataY[0])
    for (index in 1 until dataY.size) {
        innerPath.lineTo(spaceWidth * index, scaleYH * dataY[index].toFloat())
    }
    val mPathMeasure=PathMeasure(innerPath, false)
    val length:Float= mPathMeasure.length
    val stop: Float = length
    val start = 0f
    //用于截取整个path中某个片段,经过参数startD和stopD来操控截取的长度,
    // 并将截取后的path保存到参数dst中,最终一个参数表示开始点是否运用moveTo将途径的新开始点移到成果path的开始点中,
    // 通常设置为true
    val resultGreenPath=Path()
    mPathMeasure.getSegment(start, stop*(mCurAnimValue), resultGreenPath, true)
    mPathMeasure.setPath(resultGreenPath, false)
    val pos = FloatArray(2)
    mPathMeasure.getPosTan(length - 1, pos, null)
    resultGreenPath.lineTo(pos[0],0f)
    resultGreenPath.lineTo(0f,0f)
    resultGreenPath.close()
    canvas.drawPath(resultGreenPath, innerPaint)
    val path = Path()
    path.moveTo(0f, scaleYH * dataY[0])
    for (index in 1 until dataY.size) {
        path.lineTo(spaceWidth * index, scaleYH * dataY[index].toFloat())
    }
    val mPathMeasure1=PathMeasure(innerPath, false)
    val length1:Float= mPathMeasure1.length
    val stop1: Float = length1
    val start1 = 0f
    val resultGreenPath1=Path()
    mPathMeasure.getSegment(start1, stop1*(mCurAnimValue), resultGreenPath1, true)
    mPathMeasure.setPath(resultGreenPath1, false)
    val pos1 = FloatArray(2)
    mPathMeasure.getPosTan(length1 - 1, pos1, null)
    resultGreenPath1.lineTo(pos1[0],0f)
    canvas.drawPath(resultGreenPath1, lineDataPaint)
    //圆圈内白色部分
    for (index in 1 until dataY.size) {
        canvas.drawCircle(
            spaceWidth * index,
            scaleYH * dataY[index].toFloat(),
            6f,
            circleDataPaint
        )
    }
    //圆圈外部蓝色部分
    for (index in 0 until dataY.size) {
        canvas.drawCircle(
            spaceWidth * index,
            scaleYH * dataY[index].toFloat(),
            6f,
            lineDataPaint
        )
    }
}

Echars-折线图系列

最终测验屏幕适配是否可行

Echars-折线图系列

三、制作折线堆叠图

Echars-折线图系列

接下来咋们来制作Echars事例里边的折线堆叠图。

1.制作坐标系和刻度

制作如上面简直共同,不做详解。可是制作进程中咱们能够试着在不同的坐标系下进行制作,锻炼自己关于平面坐标系的了解。

package com.example.draw_android.section16_line_chart.c_stached_line_chart
/*app/src/main/java/com/example/draw_android/section16_line_chart/c_stached_line_chart/StackedLineChartView.kt*/
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
/**
 * Created by wangfei44 on 2022/2/6.
 */
class StackedLineChartView constructor(context: Context, attributeSet: AttributeSet) :
    View(context, attributeSet) {
    private val lineLength = 20f
    private val margin = 150f
    private val data = arrayListOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
    private val linePaint = Paint().apply {
        color = Color.argb(99, 123, 123, 123)
        style = Paint.Style.STROKE
        strokeWidth = 3f
        isAntiAlias = true
    }
    private val lineDataPaint = Paint().apply {
        color = Color.argb(222, 123, 123, 223)
        style = Paint.Style.STROKE
        strokeWidth = 5f
        isAntiAlias = true
    }
    private val circleDataPaint = Paint().apply {
        color = Color.WHITE
        style = Paint.Style.FILL
        strokeWidth = 5f
        isAntiAlias = true
    }
    private val textPaint = Paint().apply {
        color = Color.argb(242, 123, 123, 123)
        style = Paint.Style.FILL
        strokeWidth = 4f
        textSize = 24f
        isAntiAlias = true
    }
    init {
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawScaleLine(canvas)
        drawScaleText(canvas)
    }
    //制作刻度
    private fun drawScaleLine(canvas: Canvas) {
        canvas.translate(0f, height.toFloat())
        canvas.scale(1f, -1f)
        canvas.translate(margin, margin)
        //制作X轴的刻度
        canvas.save()
        val spaceWidth = (width - margin * 2) / (data.size - 1)
        for (index in 0 until data.size) {
            //制作X轴上刻度线
            canvas.drawLine(0f, 0f, 0f, -lineLength, linePaint)
            //经过水平改换移动画布来循环制作刻度
            canvas.translate(spaceWidth, 0f)
        }
        //康复画布坐标系到左下角。
        canvas.restore()
        //制作竖直方向的线
        canvas.save()
        val spaceHeight = (height - margin * 2) / 5
        for (index in 0..5) {
            //制作平行于X轴线
            canvas.drawLine(0f, 0f, width.toFloat() - margin * 2, 0f, linePaint)
            canvas.translate(0f, spaceHeight)
        }
        canvas.restore()
    }
    //制作文字
    private fun drawScaleText(canvas: Canvas) {
        canvas.save()
        val spaceWidth = (width - margin * 2) / (data.size - 1)
        for (index in 0 until data.size) {
            val str = data[index]
            val rect = Rect()
            textPaint.getTextBounds(str, 0, str.length, rect)
            canvas.save()
            canvas.scale(1f, -1f)
            canvas.translate(-rect.width() / 2f, rect.height().toFloat() + lineLength)
            canvas.drawText(data[index], 0f, 0f, textPaint)
            canvas.restore()
            canvas.translate(spaceWidth, 0f)
        }
        canvas.restore()
        canvas.save()
        val spaceHeight = (height - margin * 2) / 6
        for (index in 0 until 7) {
            val str = (500 * index).toString()
            val rect = Rect()
            textPaint.getTextBounds(str, 0, str.length, rect)
            canvas.save()
            canvas.scale(1f, -1f)
            canvas.translate(-rect.width().toFloat() - lineLength, rect.height() / 2f)
            canvas.drawText(str, 0f, 0f, textPaint)
            canvas.restore()
            canvas.translate(0f, spaceHeight)
        }
        canvas.restore()
    }
}

Echars-折线图系列

2.制作数据

数据是模拟的,不必纠结数据共同,当然这儿色彩能够依据高度来设置,我这儿为了便利数据源默许给了色彩。

模拟数据:
private val data = arrayListOf("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
private val dataArray = arrayListOf(
    Series(
        "Email",
        "line",
        "Total",
        arrayListOf(120, 132, 101, 134, 90, 230, 210),
        Color.argb(255, 141, 192, 240)
    ),
    Series(
        "Union Ads",
        "line",
        "Total",
        arrayListOf(350, 332, 201, 294, 290, 330, 410),
        Color.argb(255, 217, 131, 131)
    ),
    Series(
        "Video Ads",
        "line",
        "Total",
        arrayListOf(820, 890, 790, 800, 900, 1300, 1400),
        Color.argb(255, 237, 189, 96)
    ),
    Series(
        "Direct",
        "line",
        "Total",
        arrayListOf(500, 560, 500, 540, 530, 800, 910),
        Color.argb(255, 181, 213, 194)
    ),
    Series(
        "Search Engine",
        "line",
        "Total",
        arrayListOf(1640, 1860, 1802, 1868, 2580, 2660, 2640),
        Color.argb(255, 119, 135, 194)
    )
)
data class Series(
    val name: String,
    val type: String,
    val stack: String,
    val data: ArrayList<Int>,
    val color: Int
)

制作进程

private fun drawData(canvas: Canvas) {
    //水平每个刻度之间的间隔
    val spaceWidth = (width - margin * 2) / (data.size - 1)
    //核算每一个单位真实的实践像素高度
    val scaleHeight = (height - margin * 2) / 3000
    //循环制作数据线条
    for (index in 0 until dataArray.size) {
        val dataList = dataArray[index].data
        val path = Path()
        for (ind in 0 until dataList.size) {
            if (ind == 0) {
                path.moveTo(ind * spaceWidth, dataList[ind] * scaleHeight)
            } else {
                path.lineTo(ind * spaceWidth, dataList[ind] * scaleHeight)
            }
        }
        canvas.drawPath(path, linePaint.apply {
            color = dataArray[index].color
        })
    }
    //这儿进行制作线上的点圆圈,和画线分隔确保覆盖在线上面。
    for (index in 0 until dataArray.size) {
        val dataList = dataArray[index].data
        for (ind in 0 until dataList.size) {
            if (ind == 0) {
                canvas.drawCircle(
                    ind * spaceWidth,
                    dataList[ind] * scaleHeight,
                    6f,
                    innerCirclePaint.apply {
                        color = Color.WHITE
                    })
                canvas.drawCircle(
                    ind * spaceWidth,
                    dataList[ind] * scaleHeight,
                    4f,
                    circlePaint.apply {
                        color = dataArray[index].color
                    })
            } else {
                canvas.drawCircle(
                    ind * spaceWidth,
                    dataList[ind] * scaleHeight,
                    6f,
                    innerCirclePaint.apply {
                        color = Color.WHITE
                    })
                canvas.drawCircle(
                    ind * spaceWidth,
                    dataList[ind] * scaleHeight,
                    4f,
                    circlePaint.apply {
                        color = dataArray[index].color
                    })
            }
        }
    }
}

Echars-折线图系列

3.增加手势交互
由下图剖析:
1.当鼠标移动进程间隔最近的折线转折点方向连成一条竖直虚线。
2.在移动进程中鼠标右下方有一个显现概况的小框。
3.移动竖直方向和折线交汇的圆点有缩放动画。

Echars中的动画部分

Echars-折线图系列

前面课程关于手势,手势坐标系与画布坐标系映射改换都讲过了。所以这儿咋们直接写代码,进程会随同反复解说过的图文描述解说。

配合下图剖析进程:
手势移动进程中,咱们知道手势坐标系是默许屏幕左上角为圆点。而咱们的制作坐标系在左下角,且向右边进行了平移margin的
画布改换。
1.咱们配合下图来再次解说手势坐标系与画布坐标系映射,图中X,Y坐标系代表手势坐标系,X1,Y1坐标系代表画布制作坐标系。
咱们能够核算手势坐标映射到画布坐标系,图中圆点width = event.x-marginheight = 画布的高度-event.y-margin2.咱们能够看到手势移动进程中,依据判别丈量最近方位的折线折点来进行制作竖直方向的竖线。咱们能够循环遍历每根刻度间隔
手势当时的方位,取最近的刻度进行制作竖直方向的虚线。

Echars-折线图系列

    private var minValue: Float = 0f
    private var recentIndex: Int = 0
    private var visible: Boolean = false
    private val recentPaint = Paint().apply {
        color = Color.argb(200, 123, 223, 136)
        style = Paint.Style.STROKE
        strokeWidth = 3f
        isAntiAlias = true
    }
    private fun drawLine(canvas: Canvas) {
        val spaceWidth = (width - margin * 2) / (data.size - 1)
        val minMarginLeft = recentIndex * spaceWidth
        if (visible) {
            canvas.drawLine(minMarginLeft, 0f, minMarginLeft, height - margin * 2, recentPaint)
        }
    }
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                visible = true
                invalidate()
            }
            MotionEvent.ACTION_MOVE -> {
                //水平方向每个刻度之间的实践像素间隔
                val spaceWidth = (width - margin * 2) / (data.size - 1)
                //默许初始化最小间隔折线点的间隔为手势映射到画布坐标系的间隔
                minValue = event.x - margin
                //遍历每个刻度比较那个间隔手势最近。获取最近间隔的折线点
                for (index in 0 until data.size) {
                    //event.x - margin为手势坐标映射到画布坐标系之后的方位。这儿取绝对值,咱们依据调查在最近的左右只需最近就会呈现线。
                    if (abs(event.x - margin - spaceWidth * index) <= minValue) {
                        minValue = abs(event.x - margin - spaceWidth * index)
                        recentIndex = index
                    }
                }
                //循环获取来间隔最近的刻度,然后经过invalidate取刷新当然这儿为了便利全局刷新了。能够局部刷新为了性能。
                invalidate()
            }
            MotionEvent.ACTION_UP -> {
                visible = false
                invalidate()
            }
        }
        return true
    }

咱们来看看作用再持续

Echars-折线图系列

4.完善动画和适配测验

经过visible和recentIndex来完善当时手势竖线上的折点圆动画。能够经过制作圆的半径来完成。

//这儿进行制作线上的点圆圈,和画线分隔确保覆盖在线上面。
for (index in 0 until dataArray.size) {
    val dataList = dataArray[index].data
    for (ind in 0 until dataList.size) {
        if (ind == 0) {
            canvas.drawCircle(
                ind * spaceWidth,
                dataList[ind] * scaleHeight,
                if (visible&&ind == recentIndex) {
                    8f
                } else {
                    6f
                },
                innerCirclePaint.apply {
                    color = Color.WHITE
                })
            canvas.drawCircle(
                ind * spaceWidth,
                dataList[ind] * scaleHeight,
                if (visible&&ind == recentIndex) {
                    6f
                } else {
                    4f
                },
                circlePaint.apply {
                    color = dataArray[index].color
                })
        } else {
            canvas.drawCircle(
                ind * spaceWidth,
                dataList[ind] * scaleHeight,
                if (visible&&ind == recentIndex) {
                    8f
                } else {
                    6f
                },
                innerCirclePaint.apply {
                    color = Color.WHITE
                })
            canvas.drawCircle(
                ind * spaceWidth,
                dataList[ind] * scaleHeight,
                if (visible&&ind == recentIndex) {
                    6f
                } else {
                    4f
                },
                circlePaint.apply {
                    color = dataArray[index].color
                })
        }
    }
}

Echars-折线图系列

四、总结

前面章节学过了画布的改换画布状况的仓库保存,也学过了手势坐标系画布坐标系的相互映射联系,剩余的便是诲人不倦的不断的操练,不断的熟悉API的运用,才能熟能生巧,胸有成竹,构成自己自定义制作的架构知识体系。

折线图系列在Echars里有许多事例,当然后边有时间都会补充,或许咱们能够留言,上图,我会挑选一些比较有代表性和难度的进行制作。这篇计划多写几个的,可是篇幅太长了,怕影响阅读。