5分钟带你复刻蚂蚁基金业绩走势图

1,956 阅读7分钟

前言

都说你不理财,财不理你,现在我理财了,财却直接离我而去了,我成为了绿油油的大韭菜🐶。

望着毫无起色的基金,我陷入了沉思,基金到底可以带给我什么呢?我又能从中学习到什么呢?

嗯,虽然我是一颗大韭菜,但同时我也是一名程序员,站在代码的角度,至少我能学习一下它的业绩走势图时如何实现的呀,从而练习一下我的自定义View技能🐶。

所以,今天咱就先不考虑是加仓死扛还是赎回割肉了。今天!就让我们来尝试复刻一下这个基金的业绩走势图吧。

就复刻这个基金吧👇

复刻蚂蚁基金走势图.jpeg

成果展示

直接先展示一下复刻的成果吧,下载apk体验更佳,下载地址传送门点这里

fund_chart_result.gif

获取数据

想要绘制出相同的效果,前提得有数据。这边我们可以抓取一下接口数据,获取到近1月、近3月、近1年等JSON数据。

内容格式如下:

{
  "data": [
    {
      "date": "2023-03-01",
      "yield": "-0.23",
      "indexYield": "1.41",
      "fundTypeYield": "0.86",
      "benchQuote": "0.01"
    },
    {
      "date": "2023-03-02",
      "yield": "-1.17",
      "indexYield": "1.19",
      "fundTypeYield": "0.17",
      "benchQuote": "-0.64"
    },
    ... 省略 ...
  ],
  "total": {
    "totalYield": "-5.10",
    "totalIndexYield": "-0.46",
    "totalFundTypeYield": "-1.46",
    "totalBenchQuote": "-2.31"
  },
  "success": true,
  "totalCount": 24,
  "name": "保密!无奖竞猜~"
}

主要分为两块内容。

  • 一组包含每天收益率的列表。

    • date:表示日期。
    • yield:表示本基金当日的收益率。
    • indexYield:表示沪深300当日的收益率。
    • fundTypeYield:表示同类平均当日的收益率。
  • 以及这段时间总的收益率数据。

    • totalYield:表示本基金总收益率。
    • totalIndexYield:表示沪深300总收益率。
    • totalFundTypeYield:表示同类平均总收益率。

接着将JSON转为对应的Bean文件。

data class FundReturnRateBean(
    @SerializedName("data")
    var dayRateList: List<DayRateDetail>,
    @SerializedName("total")
    var totalReturnRate: TotalReturnRate,
    var name: String,
    var success: Boolean,
    var totalCount: Int
)

data class DayRateDetail(
    var benchQuote: String,
    var fundTypeYield: String,//同类平均收益率
    var indexYield: String,//沪深300收益率
    var pdate: String,//日期
    var yield: String//本基金收益率
)

data class TotalReturnRate(
    var totalYield: String,//本基金总收益率
    var totalIndexYield: String,//沪深300总收益率
    var totalFundTypeYield: String,//同类平均总收益率
    var totalBenchQuote: String
)

有了数据,接下来就可以动手实现该功能啦🧑‍💻。

具体实现

需求分析

分析一下图表中存在的元素,有:

  • 横坐标:日期。
  • 纵坐标:收益率。
  • 走势线:分别代表着本基金、同类平均、沪深300的收益率走势线。
  • 标签:蚂蚁基金。

这里我们可以使用自定义Drawable来将整个图表进行拆分,我们可以分为3层:

  • FundGridDrawable:用于绘制横坐标与纵坐标。
  • RateLineDrawable:用于绘制本基金、同类平均、沪深300的收益率走势线。
  • FundLabelDrawable:用于绘制标签。

然后按照绘制顺序进行逐层绘制。

  1. 绘制fundGridDrawable
  2. 绘制rateLineDrawable
  3. 绘制fundLabelDrawable

接着我们逐层进行实现。

区域划分

因为我们是分层绘制,所以我们需要按需求进行区域划分。

我们可以先确认好核心区域,也就是绘制走势线的区域,即rateLineDrawable.bounds。(这个核心区域,下文会用 lineChartRect 表示。)。确认好了核心区域后,我们就可以利用它来确认fundGridDrawable.boundsfundLabelDrawable.bounds

rect-distribution.png

有了区域分布图以后,我们就可以通过代码来实现区域分配了。

private var chartRect = Rect()
private val defaultPadding = 5f.px.toInt()
private val paddingTop = 15f.px.toInt()
private val paddingBottom = 25f.px.toInt()
private val paddingStart = 60f.px.toInt()
private val paddingEnd = 20f.px.toInt()
private val labelWidth = 35f.px.toInt()
private val labelHeight = 100f.px.toInt()

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    chartRect = Rect(0, 0, w, h)
    rateLineDrawable.bounds = Rect(
        paddingStart,
        chartRect.top + paddingTop,
        chartRect.right - paddingEnd,
        chartRect.bottom - paddingBottom
    )
    fundGridDrawable.bounds = Rect(
        chartRect.left + defaultPadding,
        rateLineDrawable.bounds.top - defaultPadding,
        rateLineDrawable.bounds.right,
        chartRect.bottom - defaultPadding
    )
    fundGridDrawable.lineChartRect = rateLineDrawable.bounds

    fundLabelDrawable.bounds = Rect(
        rateLineDrawable.bounds.left,
        rateLineDrawable.bounds.bottom - labelWidth,
        rateLineDrawable.bounds.left + labelHeight,
        rateLineDrawable.bounds.bottom
    )
}

区域分配好之后,我们就要来聊聊具体的绘制工作了。

绘制坐标轴

坐标轴这边分为横轴与竖轴,分别表示日期与收益率。

我们先来看横轴。

横轴

横轴表示日期,由三部分构成,分别是:

  • 三个”yyyy-MM-dd”格式的日期:分别代表着该段周期基金的初始日期、末尾日期以及居中日期。位于 lineChartRect 最左边、中间以及最右边位置。
  • 三根短小的竖线:以 lineChartRect.bottom 为起点,向下延伸5dp。同样位于 lineChartRect 最左边、中间以及最右边位置。
  • 一根灰色的实线:跨度刚好是 lineChartRect 的宽度。
FundGridDrawable.kt

private fun drawDateTimeText(canvas: Canvas) {
    textPaint.getTextBounds(minDateTime, 0, minDateTime.length, textBoundsRect)

    dateTimeTextPxY = lineChartRect.bottom + textBoundsRect.height() + paddingBottom
    textPaint.textAlign = Paint.Align.LEFT
    //绘制初始日期
    canvas.drawText(
        minDateTime,
        lineChartRect.left.toFloat(),
        dateTimeTextPxY,
        textPaint
    )
    //绘制最左边竖线
    canvas.drawLine(
        lineChartRect.left.toFloat(),
        lineChartRect.bottom.toFloat(),
        lineChartRect.left.toFloat(),
        lineChartRect.bottom.toFloat() + paddingBottom / 2,
        bottomLinePaint
    )
		...省略代码...

    //绘制横线
    canvas.drawLine(
        lineChartRect.left.toFloat(),
        lineChartRect.bottom.toFloat(),
        lineChartRect.right.toFloat(),
        lineChartRect.bottom.toFloat(),
        bottomLinePaint
    )
}

竖轴

竖轴表示收益率,由两部分构成,分别是:

  • 收益率百分比文字:位于 lineChartRect 左边。
  • 收益率虚线:跨度刚好是 lineChartRect 的宽度。

仔细观察了蚂蚁基金,我发现其将竖轴上的收益率等分成4份,也就是画5条线,且除了最上方的最高收益率线与最下方的最低收益率线,中间还要有一条0收益率线。还发现,虽然收益率百分比保留了两位小数,但都是0,也就是说等分的间距其实取整了。

所以在拿到接口返回的List<DayRateDetail>后,我们还应进行一番处理,从而得出绘制所需的真实收益率数据。

theRateRangeInterval = (maxRate - minRate).div(3).roundToInt()
private fun calcRateAbscissa(): MutableList<Int> {
    val rateAbscissaLines = mutableListOf<Int>()

    if (theRateRangeInterval == 0) {
        return rateAbscissaLines
    }

    rateAbscissaLines.clear()
    rateAbscissaLines.add(0)

    for (i in 1..5) {
        rateAbscissaLines.add(theRateRangeInterval * i)
        if (theRateRangeInterval * i > maxRate) {
            break
        }
    }

    for (i in 1..5) {
        rateAbscissaLines.add(-theRateRangeInterval * i)
        if (-theRateRangeInterval * i < minRate) {
            break
        }
    }

    rateAbscissaLines.sort()
    Log.e(TAG, "calcRateAbscissa: after sort rateAbscissaLines = $rateAbscissaLines")
    return rateAbscissaLines
}

maxRateminRate 分别表示真实接口返回的最大收益率与最小收益率,利用其差额来计算出theRateRangeInterval。接着以0位起点,遍历循环,向上向下添加收益率,以maxRateminRate作为边界终止条件。

计算出真实的rateAbscissaLines后,我们就可以进行绘制了。

private var yPx = 0f
private fun drawRateTextAndLines(canvas: Canvas) {
    rateAbscissaLines.forEach {

        yPx = lineChartRect.top + (maxRate - it).div(yPxSpec).toFloat()

				//绘制收益率虚线
        canvas.drawLine(
            lineChartRect.left.toFloat(),
            yPx,
            lineChartRect.right.toFloat(),
            yPx,
            rateLinePaint
        )

				//绘制收益率百分比文字
        textPaint.textAlign = Paint.Align.RIGHT
        canvas.drawText(
            "${it.toStringAsFixed(2)}%",
            lineChartRect.left.toFloat() - 10f.px,
            yPx + 5f.px,
            textPaint
        )
    }
}

绘制走势线

走势线一共有三条,分别是本基金走势线、同类平均走势线以及沪深300走势线。

线其实是由很多的点组成的,接口返回的List<DayRateDetail>列表中,每个元素其实就是线上的一个点。而点的定位,正是核心之处了🤔。

如果你阅读该文章是从上往下一步一步看下来的,那此刻你就知道,我们现在已经拿到了lineChartRect收益率差额这两个数据了。通过这两个数据,我们就可以计算出竖轴的像素规格了。

yPxSpec = (maxRate - minRate).div(lineChartRect.bounds.height())

而横轴的像素规格就更加简单了。

xPxSpec = lineChartRect.bounds.width().div(dayRateDetailList.size.toDouble())

通过 xPxSpecyPxSpec 就可以很方便的完成点的定位啦。再通过Path将点连成线,就构成了走势线。

以本基金走势线为例。

private var x = 0f
private var yYield = 0f
private fun calcData() {
    yieldLinePath.reset()

    dayRateDetailList.forEachIndexed { index, dayRateDetail ->
        x = bounds.left + index.times(xPxSpec).toFloat()
        yYield = bounds.top + (maxRate - dayRateDetail.yield.toDouble()).div(yPxSpec).toFloat()

        if (index == 0) {
            yieldLinePath.moveTo(x, yYield)
        } else {
            yieldLinePath.lineTo(x, yYield)
        }
    }
}

绘制标签

标签的绘制就很简单了,这里我们取代“蚂蚁基金”,改为“JC基金复刻”

private fun drawLabelTag(canvas: Canvas) {
    canvas.drawText(
        "JC基金复刻",
        bounds.left.toFloat(),
        bounds.bottom - paddingBottom,
        labelTextPaint
    )
}

总结

其实本文最主要的目的是练习自定义View,实现基金的业绩走势线不算复杂,但你想实现相同的效果,其实也不简单。我也只是起了个开头,如果你有兴趣,后期可以添加上动画,添加下单标记点,还可进一步进行自定义。

文章主要分享了实现原理,如果你想查看文本的所有代码,请查看我的GitHub 项目AntFundChart。创作分享不易,如果本文有帮助到你,希望可以给我点个Star,十分感谢🙏。

推荐阅读


到此文章就结束啦~

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。

另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~✌️!