前语

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

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

嗯,尽管我是一颗大韭菜,但同时我也是一名程序员,站在代码的角度,至少我能学习一下它的成绩走势图时怎么完成的呀,从而练习一下我的自定义View技术。

所以,今日咱就先不考虑是加仓死扛仍是换回割肉了。今日!就让咱们来测验复刻一下这个基金的成绩走势图吧。

就复刻这个基金吧

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

作用展示

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

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

获取数据

想要制作出相同的作用,条件得有数据。这边咱们能够抓取一下接口数据,获取到近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

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

有了区域分布图今后,咱们就能够经过代码来完成区域分配了。

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,非常感谢。

引荐阅览

  • 为何引荐在自定义View中抽Drawable

    假如你不是很熟悉本文涉及的自定义Drawable相关知识点,引荐阅览该文。

  • 本文apk下载地址

    apk大小为4M左右,欢迎下载安装体验。

  • 本文项目一切源码地址

    一切源码都已开源,托管在GitHub上,创作共享不易,假如有协助到你,期望能够给我点个Star,非常感谢。


到此文章就完毕啦~

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

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