微北洋-代码读后感(1)

如何绘制一条优秀的GPA曲线

(1)准备工作

在布局文件中我们发现了很多自定义View,找到GPA曲线的自定义GpaLineChartView

//
<xyz.rickygao.gpa2.view.GpaLineChartView
    android:id="@+id/cv_gpa_line"
    android:layout_width="match_parent"
    android:layout_height="320dp"
    android:background="@color/gpa2_color_secondary"
    android:paddingBottom="160dp"
    android:paddingTop="40dp"
    app:fillColor="@color/gpa2_color_primary"
    app:lineColor="@color/gpa2_color_primary_dark"
    app:pointColor="@color/gpa2_color_primary_dark" />

//

 

在APP中对应红线部分

 

下面是该View的布局说明,除去四周的padding,实际的画布大小为图中红线部分

对应APP中位置,实际设置中Left和Right padding均为0dp

(2)绘制思路

  • 创建画笔
  • 确定上述基准线
  • 确定起点,t0,t1… … 终点坐标
  • 计算每两点之间三阶贝塞尔曲线路径

 

(3)代码实现

存放点信息的数据类和对应List

//
data class DataWithDetail(val data: Double, val detail: String)
var dataWithDetail: List<DataWithDetail> = emptyList()
//

 

计算画布大小,并按照点的个数+1将画布均分,计算出widthStep

//
val contentWidth = width - paddingLeft – paddingRight
val contentHeight = height - paddingTop – paddingBottom
val widthStep = contentWidth.toFloat() / (dataWithDetail.size + 1)
//

 

找出最大数据与最小数据,计算数据跨度,对应的max基准线为maxdata向上偏移1/4dataspan,对应的min基准线为mindata向下偏移1/4dataspan,这样可以保证最后的GPA曲线在出发和结束时一定是上升状态。

要注意的是,max和minDataExtended并不是实际的线,而是一个基准数值,或者说是一个相对值,当data==minDataExtended时点会位于画布的底端,当data==maxDataExtended时,点会位于画布的顶端,我们通过data和dataSpanExtended计算出一个百分比,来确定他在画布内的位置,由于我们向上和向下都延伸了dataspan的1/4,所以我们的点在竖直方向都在画布的1/65/6范围内

所以对于data既可以是绩点,也可以是加权,因为我们最后只是计算一个百分比

//
val minData = dataWithDetail.minBy(DataWithDetail::data)?.data ?: 0.0
val maxData = dataWithDetail.maxBy(DataWithDetail::data)?.data ?: 1.0
val dataSpan = if (maxData != minData) maxData - minData else 1.0
val minDataExtended = minData - dataSpan / 4F
val maxDataExtended = maxData + dataSpan / 4F
val dataSpanExtended = maxDataExtended - minDataExtended
//

 

计算数据点X坐标,间隔为widthStep

//
(0 until dataWithDetail.size).mapTo(pointsX.apply { clear() }) {
    paddingLeft + widthStep * (it + 1)
}
//

 

计算Y坐标,通过占比计算

//
dataWithDetail.mapTo(pointsY.apply { clear() }) {
paddingTop + ((1 - ((it.data - minDataExtended) / dataSpanExtended)) * contentHeight).toFloat()
}
//

 

由于我只有一学期的成绩,所以计算后结果为X = contentWidth / 2 ;Y = contentHeight / 2  计算起点Y坐标,画笔移动到起点

//
var py = (paddingTop + contentHeight).toFloat()
moveTo(0F, py)
//

 

对于每一条贝塞尔曲线我们要设置两个控制点,横坐标X为两点水平中央(我们只需要向左平移widthStep/2),第一个控制点Y坐标与出发点相同,第二个控制点Y坐标与终点相同

 

 

把最后一个数据点与终点连起来,控制点相对位置不变

//
val cx = width - widthStep / 2F
cubicTo(cx, py, cx, paddingTop.toFloat(), width.toFloat(), paddingTop.toFloat())
//

 

在onDraw中调用drawPath

//
drawPath(linePath, linePaint)
//

哎呀woc太tm完美了,完全契合

 

曲线下面的填充path,画了上右下三条线,close闭合,fill填色

//
fillPath.apply {
reset()
    addPath(linePath)
    lineTo(width.toFloat(), height.toFloat())
    lineTo(0F, height.toFloat())
    close()
}
//

 

 

画点,集合操作filter过滤出未选中的点,因为有阴影为了效果,微调圆心,选中的点同理,只是有白色描边

//
pointPath.apply {
reset()
    if (dataWithDetail.isEmpty())
        return@apply // no need to draw
    (0 until dataWithDetail.size)
            .filter { it != selectedIndex }
            .forEach {
                addCircle(
                        pointsX[it] - LINE_STROKE / 4F,
                        pointsY[it] - LINE_STROKE / 4F,
                        POINT_RADIUS,
                        Path.Direction.CCW
                )
            }
 }
//

 

(4)细节

Kotlin优秀的语法,极简迭代+集合操作+优秀的lamada,能写1行就不写5行

//
(0 until dataWithDetail.size).mapTo(pointsX.apply { clear() }) 
//

 

比Builder模式用着更爽

//
fillPath.apply {
reset()
    addPath(linePath)
    lineTo(width.toFloat(), height.toFloat())
    lineTo(0F, height.toFloat())
    close()
}
//

 

虽然说不出来哪里好,但就是感觉挺奇妙

//
var lineColor
    get() = linePaint.color
    set(value) {
        linePaint.color = value
    }
//

 

微北洋-代码读后感(1)》有108个想法

发表评论

邮箱地址不会被公开。 必填项已用*标注