最近在参与一个安卓项目,不忙的时候也会接一些开发任务,遇到一个需求,要实现一个折线图,且根据Y轴的值对应区间显示不同颜色。Android应用中各种图标的实现多数都是在用MPAndroidChart,我个人没什么深入的研究,就随大流吧。找了一些折线图实现的例子,都没有满足需求的实现方式,但是确认了图标中的线是由LineChartRenderer实现的。
既然现有方法无法实现需求,只能对原方法进行重写了。我们项目主要使用kotlin,所以下面我也以kotlin的语法进行展示,Android studio好像有kotlin和Java的转换工具,如果刚好有同学的需求和我这个类似,用的又是Java的话,可以用工具转成Java类。
先直接展示重写后的MyLineChartRenderer.kt
package com.example.mykotlinandroid.view
import android.graphics.*
import com.github.mikephil.charting.animation.ChartAnimator
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider
import com.github.mikephil.charting.interfaces.datasets.IDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import com.github.mikephil.charting.renderer.LineChartRenderer
import com.github.mikephil.charting.utils.ColorTemplate
import com.github.mikephil.charting.utils.ViewPortHandler
import java.util.*
class MyLineChartRenderer(
chart: LineDataProvider, animator: ChartAnimator?,
viewPortHandler: ViewPortHandler?
) : LineChartRenderer(chart, animator, viewPortHandler) {
private var mHighlightCirclePaint: Paint? = null
private var isHeart = false
private lateinit var pos: FloatArray
private lateinit var colors: IntArray
private lateinit var range: IntArray
init {
mChart = chart
mCirclePaintInner = Paint(Paint.ANTI_ALIAS_FLAG)
mCirclePaintInner.style = Paint.Style.FILL
mCirclePaintInner.color = Color.WHITE
mHighlightCirclePaint = Paint()
}
private var mLineBuffer = FloatArray(4)
override fun drawLinear(c: Canvas?, dataSet: ILineDataSet) {
val entryCount = dataSet.entryCount
val isDrawSteppedEnabled = dataSet.mode == LineDataSet.Mode.STEPPED
val pointsPerEntryPair = if (isDrawSteppedEnabled) 4 else 2
val trans = mChart.getTransformer(dataSet.axisDependency)
val phaseY = mAnimator.phaseY
mRenderPaint.style = Paint.Style.STROKE
// if the data-set is dashed, draw on bitmap-canvas
val canvas: Canvas? = if (dataSet.isDashedLineEnabled) {
mBitmapCanvas
} else {
c
}
mXBounds[mChart] = dataSet
// if drawing filled is enabled
if (dataSet.isDrawFilledEnabled && entryCount > 0) {
drawLinearFill(c, dataSet, trans, mXBounds)
}
// more than 1 color
if (dataSet.colors.size > 1) {
if (mLineBuffer.size <= pointsPerEntryPair * 2) mLineBuffer =
FloatArray(pointsPerEntryPair * 4)
for (j in mXBounds.min..mXBounds.range mXBounds.min) {
var e: Entry = dataSet.getEntryForIndex(j) ?: continue
if (e.y == 0f) continue
mLineBuffer[0] = e.x
mLineBuffer[1] = e.y * phaseY
if (j < mXBounds.max) {
e = dataSet.getEntryForIndex(j 1)
if (e == null) break
if (e.y == 0f) break
mLineBuffer[2] = e.x
if (isDrawSteppedEnabled) {
mLineBuffer[3] = mLineBuffer[1]
mLineBuffer[4] = mLineBuffer[2]
mLineBuffer[5] = mLineBuffer[3]
mLineBuffer[6] = e.x
mLineBuffer[7] = e.y * phaseY
} else {
mLineBuffer[3] = e.y * phaseY
}
} else {
mLineBuffer[2] = mLineBuffer[0]
mLineBuffer[3] = mLineBuffer[1]
}
trans.pointValuesToPixel(mLineBuffer)
if (!mViewPortHandler.isInBoundsRight(mLineBuffer[0])) break
// make sure the lines don't do shitty things outside
// bounds
if (!mViewPortHandler.isInBoundsLeft(mLineBuffer[2])
|| !mViewPortHandler.isInBoundsTop(mLineBuffer[1]) && !mViewPortHandler
.isInBoundsBottom(mLineBuffer[3])
) continue
// get the color that is set for this line-segment
mRenderPaint.color = dataSet.getColor(j)
canvas!!.drawLines(mLineBuffer, 0, pointsPerEntryPair * 2, mRenderPaint)
}
} else { // only one color per dataset
if (mLineBuffer.size < (entryCount * pointsPerEntryPair).coerceAtLeast(
pointsPerEntryPair
) * 2
) mLineBuffer = FloatArray(
(entryCount * pointsPerEntryPair).coerceAtLeast(pointsPerEntryPair) * 4
)
var e1: Entry?
var e2: Entry
e1 = dataSet.getEntryForIndex(mXBounds.min)
if (e1 != null) {
var j = 0
for (x in mXBounds.min..mXBounds.range mXBounds.min) {
e1 = dataSet.getEntryForIndex(if (x == 0) 0 else x - 1)
e2 = dataSet.getEntryForIndex(x)
if (e1.y == 0f || e2.y == 0f) {
continue
}
mLineBuffer[j ] = e1.x
mLineBuffer[j ] = e1.y * phaseY
if (isDrawSteppedEnabled) {
mLineBuffer[j ] = e2.x
mLineBuffer[j ] = e1.y * phaseY
mLineBuffer[j ] = e2.x
mLineBuffer[j ] = e1.y * phaseY
}
mLineBuffer[j ] = e2.x
mLineBuffer[j ] = e2.y * phaseY
}
if (j > 0) {
trans.pointValuesToPixel(mLineBuffer)
val size =
((mXBounds.range 1) * pointsPerEntryPair).coerceAtLeast(pointsPerEntryPair) * 2
mRenderPaint.color = dataSet.color
if (isHeart) {
mRenderPaint.shader = LinearGradient(
0f,
mViewPortHandler.contentRect.top,
0f,
mViewPortHandler.contentRect.bottom,
colors,
pos,
Shader.TileMode.CLAMP
)
}
canvas!!.drawLines(mLineBuffer, 0, size, mRenderPaint)
}
}
}
mRenderPaint.pathEffect = null
}
/**
* cache for the circle bitmaps of all datasets
*/
private val mImageCaches = HashMap<IDataSet<*>, DataSetImageCache>()
private val mCirclesBuffer = FloatArray(2)
override fun drawCircles(c: Canvas) {
mRenderPaint.style = Paint.Style.FILL
val phaseY = mAnimator.phaseY
mCirclesBuffer[0] = 0f
mCirclesBuffer[1] = 0f
val dataSets = mChart.lineData.dataSets
for (i in dataSets.indices) {
val dataSet = dataSets[i]
if (!dataSet.isVisible || !dataSet.isDrawCirclesEnabled || dataSet.entryCount == 0) continue
mCirclePaintInner.color = dataSet.circleHoleColor
val trans = mChart.getTransformer(dataSet.axisDependency)
mXBounds[mChart] = dataSet
val circleRadius = dataSet.circleRadius
val circleHoleRadius = dataSet.circleHoleRadius
val drawCircleHole =
dataSet.isDrawCircleHoleEnabled && circleHoleRadius < circleRadius && circleHoleRadius > 0f
val drawTransparentCircleHole = drawCircleHole &&
dataSet.circleHoleColor == ColorTemplate.COLOR_NONE
var imageCache: DataSetImageCache?
if (mImageCaches.containsKey(dataSet)) {
imageCache = mImageCaches[dataSet]
} else {
imageCache = DataSetImageCache(mRenderPaint, mCirclePaintInner)
mImageCaches[dataSet] = imageCache
}
val changeRequired = imageCache!!.init(dataSet)
// only fill the cache with new bitmaps if a change is required
if (changeRequired) {
imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole)
}
val boundsRangeCount = mXBounds.range mXBounds.min
for (j in mXBounds.min..boundsRangeCount) {
val e = dataSet.getEntryForIndex(j) ?: break
if (e.y == 0f) continue
mCirclesBuffer[0] = e.x
mCirclesBuffer[1] = e.y * phaseY
trans.pointValuesToPixel(mCirclesBuffer)
if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0])) break
if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) ||
!mViewPortHandler.isInBoundsY(mCirclesBuffer[1])
) continue
val circleBitmap = imageCache.getBitmap(j)
if (circleBitmap != null) {
c.drawBitmap(
circleBitmap,
mCirclesBuffer[0] - circleRadius,
mCirclesBuffer[1] - circleRadius,
null
)
}
}
}
}
private class DataSetImageCache(val mRenderPaint: Paint, val mCirclePaintInner: Paint) {
private val mCirclePathBuffer = Path()
private var circleBitmaps: Array<Bitmap?>? = null
/**
* Sets up the cache, returns true if a change of cache was required.
*
* @param set
* @return
*/
fun init(set: ILineDataSet): Boolean {
val size = set.circleColorCount
var changeRequired = false
if (circleBitmaps == null) {
circleBitmaps = arrayOfNulls(size)
changeRequired = true
} else if (circleBitmaps!!.size != size) {
circleBitmaps = arrayOfNulls(size)
changeRequired = true
}
return changeRequired
}
/**
* Fills the cache with bitmaps for the given dataset.
*
* @param set
* @param drawCircleHole
* @param drawTransparentCircleHole
*/
fun fill(set: ILineDataSet, drawCircleHole: Boolean, drawTransparentCircleHole: Boolean) {
val colorCount = set.circleColorCount
val circleRadius = set.circleRadius
val circleHoleRadius = set.circleHoleRadius
for (i in 0 until colorCount) {
val conf = Bitmap.Config.ARGB_8888
val circleBitmap = Bitmap.createBitmap(
(circleRadius * 2.1).toInt(),
(circleRadius * 2.1).toInt(), conf
)
val canvas = Canvas(circleBitmap)
circleBitmaps!![i] = circleBitmap
mRenderPaint.color = set.getCircleColor(i)
if (drawTransparentCircleHole) {
// Begin path for circle with hole
mCirclePathBuffer.reset()
mCirclePathBuffer.addCircle(
circleRadius,
circleRadius,
circleRadius,
Path.Direction.CW
)
// Cut hole in path
mCirclePathBuffer.addCircle(
circleRadius,
circleRadius,
circleHoleRadius,
Path.Direction.CCW
)
// Fill in-between
canvas.drawPath(mCirclePathBuffer, mRenderPaint)
} else {
canvas.drawCircle(
circleRadius,
circleRadius,
circleRadius,
mRenderPaint
)
if (drawCircleHole) {
canvas.drawCircle(
circleRadius,
circleRadius,
circleHoleRadius,
mCirclePaintInner
)
}
}
}
}
/**
* Returns the cached Bitmap at the given index.
*
* @param index
* @return
*/
fun getBitmap(index: Int): Bitmap? {
return circleBitmaps!![index % circleBitmaps!!.size]
}
}
/***
* 对高亮的值进行显示小圆点 如果没有此需要可删除
*/
override fun drawHighlighted(c: Canvas, indices: Array<Highlight>) {
super.drawHighlighted(c, indices)
val phaseY = mAnimator.phaseY
val lineData = mChart.lineData.getDataSetByIndex(0)
val trans = mChart.getTransformer(lineData.axisDependency)
mCirclesBuffer[0] = 0f
mCirclesBuffer[1] = 0f
for (high in indices) {
val e = lineData.getEntryForXValue(high.x, high.y)
mCirclesBuffer[0] = e.x
mCirclesBuffer[1] = e.y * phaseY
trans.pointValuesToPixel(mCirclesBuffer)
mHighlightCirclePaint!!.color = lineData.highLightColor
//根据不同的区间显示小圆点的颜色
if (isHeart) {
if (e.y >= range[0]) {
mHighlightCirclePaint!!.color = colors[0]
} else if (e.y < range[0] && e.y >= range[1]) {
mHighlightCirclePaint!!.color = colors[2]
} else if (e.y >= range[2] && e.y < range[1]) {
mHighlightCirclePaint!!.color = colors[4]
} else {
mHighlightCirclePaint!!.color = colors[6]
}
}
c.drawCircle(mCirclesBuffer[0], mCirclesBuffer[1], 10f, mHighlightCirclePaint!!)
mHighlightCirclePaint!!.color = Color.WHITE
c.drawCircle(mCirclesBuffer[0], mCirclesBuffer[1], 5f, mHighlightCirclePaint!!)
}
}
override fun drawHorizontalBezier(dataSet: ILineDataSet) {
val phaseY = mAnimator.phaseY
val trans = mChart.getTransformer(dataSet.axisDependency)
mXBounds[mChart] = dataSet
cubicPath.reset()
if (mXBounds.range >= 1) {
var prev = dataSet.getEntryForIndex(mXBounds.min)
var cur = prev
// let the spline start
cubicPath.moveTo(cur.x, cur.y * phaseY)
for (j in mXBounds.min 1..mXBounds.range mXBounds.min) {
prev = cur
cur = dataSet.getEntryForIndex(j)
val cpx = (prev.x
(cur.x - prev.x) / 2.0f)
cubicPath.cubicTo(
cpx, prev.y * phaseY,
cpx, cur.y * phaseY,
cur.x, cur.y * phaseY
)
}
}
// if filled is enabled, close the path
if (dataSet.isDrawFilledEnabled) {
cubicFillPath.reset()
cubicFillPath.addPath(cubicPath)
// create a new path, this is bad for performance
drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds)
}
mRenderPaint.color = dataSet.color
mRenderPaint.style = Paint.Style.STROKE
trans.pathValueToPixel(cubicPath)
if (isHeart) {
mRenderPaint.shader = LinearGradient(
0f, mViewPortHandler.contentRect.top,
0f, mViewPortHandler.contentRect.bottom, colors, pos, Shader.TileMode.CLAMP
)
}
mBitmapCanvas.drawPath(cubicPath, mRenderPaint)
mRenderPaint.pathEffect = null
}
/***
* @param isHeart true 开启分区间显示的颜色
* @param medium 不同层级的判断条件
* @param colors 不同区间的颜色值,从上到下的颜色 我这里是3个值 那么分成四段 colors数组长度就为4
*/
fun setHeartLine(isHeart: Boolean, medium: Int, larger: Int, limit: Int, colors: IntArray) {
this.isHeart = isHeart
range = IntArray(3)
range[0] = limit
range[1] = larger
range[2] = medium
val pos = FloatArray(4)
val yMax = (mChart as LineChart).axisLeft.axisMaximum
val yMin = (mChart as LineChart).axisLeft.axisMinimum
pos[0] = (yMax - limit) / (yMax - yMin)
pos[1] = (limit - larger) / (yMax - yMin) pos[0]
pos[2] = (larger - medium) / (yMax - yMin) pos[1]
pos[3] = 1f
this.pos = FloatArray(pos.size * 2)
this.colors = IntArray(colors.size * 2)
var index = 0
for (i in pos.indices) {
this.colors[index] = colors[i]
this.colors[index 1] = colors[i]
if (i == 0) {
this.pos[index] = 0f
this.pos[index 1] = pos[i]
} else {
this.pos[index] = pos[i - 1]
this.pos[index 1] = pos[i]
}
index = 2
}
}
}
MyLineChartRenderer.kt有了,就需要写一个使用这个的MyLineChart,我们对LineChart进行重写:
package com.example.mykotlinandroid.view
import android.content.Context
import android.util.AttributeSet
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.data.LineData
class MyLineChart: LineChart {
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
)
override fun init() {
super.init()
mRenderer = MyLineChartRenderer(this, mAnimator, mViewPortHandler)
}
override fun getLineData(): LineData {
return mData
}
override fun onDetachedFromWindow() {
// releases the bitmap in the renderer to avoid oom error
if (mRenderer != null && mRenderer is MyLineChartRenderer) {
(mRenderer as MyLineChartRenderer).releaseBitmap()
}
super.onDetachedFromWindow()
}
}
后面就可以直接使用了,在布局文件中直接引用重写的MyLineChart类:
<com.example.mykotlinandroid.view.MyLineChart
android:id="@ id/lc_my_line_chart"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_below="@id/tv_title"
android:layout_marginTop="20dp"/>
把初始化的代码也贴上来:
package com.example.mykotlinandroid.fragment
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.example.mykotlinandroid.R
import com.example.mykotlinandroid.databinding.FragmentLineChartBinding
import com.example.mykotlinandroid.view.MyLineChartRenderer
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
class LineChartFragment(mContext: Context) : Fragment(R.layout.fragment_line_chart) {
private lateinit var binding: FragmentLineChartBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLineChartBinding.inflate(inflater)
initView()
return binding.root
}
private fun initView() {
//准备测试数据
var list = ArrayList<Entry>()
for (i in 1..10) {
if (i % 2 == 0) {
list.add(Entry(i.toFloat() - 1, i.toFloat() * 2))
} else {
list.add(Entry(i.toFloat() - 1, i.toFloat() * -2))
}
}
val dataSet = LineDataSet(list, "lineDataSet")
dataSet.mode = LineDataSet.Mode.HORIZONTAL_BEZIER
val lineData = LineData(dataSet)
binding.lcMyLineChart.data = lineData
binding.lcMyLineChart.legend.isEnabled = false
binding.lcMyLineChart.description.isEnabled = false
//设置每一个分区的颜色,将设置内容传递给MyLineChartRenderer.kt
if (binding.lcMyLineChart.renderer is MyLineChartRenderer) {
val renderer: MyLineChartRenderer =
binding.lcMyLineChart.renderer as MyLineChartRenderer
val medium = 3
val larger = 9
val limit = 17
val colors = IntArray(4)
colors[0] = resources.getColor(R.color.blue, null)
colors[1] = resources.getColor(R.color.black, null)
colors[2] = resources.getColor(R.color.purple_200, null)
colors[3] = resources.getColor(R.color.teal_200, null)
//最关键的逻辑就是下面这一步了
renderer.setHeartLine(true, medium, larger, limit, colors)
}
binding.lcMyLineChart.xAxis.axisMaximum = 10f
binding.lcMyLineChart.xAxis.axisMinimum = 0f
binding.lcMyLineChart.axisLeft.axisMaximum = 22f
binding.lcMyLineChart.axisLeft.axisMinimum = -22f
binding.lcMyLineChart.axisLeft.setDrawGridLines(false)
binding.lcMyLineChart.axisRight.isEnabled = false
// binding.lcMyLineChart.axisRight.setDrawGridLines(false)
binding.lcMyLineChart.setScaleEnabled(false)
binding.lcMyLineChart.xAxis.position=XAxis.XAxisPosition.BOTTOM
}
}
上面这个例子是将小于3归为一个颜色,3到9一个颜色,9到17一个颜色,大于17一个颜色。可以根据自己的需要进行调整,至于要设置更多的颜色区间,就需要去改MyLineChartRenderer.kt里的代码了。
这里有几个遗留问题,第一个是不能开启缩放Y轴,这个实现逻辑是根据Y轴值的初始位置进行判定的,一旦缩放Y轴,颜色不会根据Y轴的值变化而变化,除非在代码中每次缩放都加载一次MyLineChartRenderer;第二个问题是MyLineChartRenderer.kt的方法drawLinear中有一个逻辑判断坐标点Y值是否为0,如果为0就跳过这个点,继续下一个点,这就导致你在使用这种mode时:
dataSet.mode = LineDataSet.Mode.LINEAR
无法显示原点。我还遇到过线条在滑动几下后自动消失,还有没有其他问题就不清楚了,如果你是LINEAR,建议把这个逻辑注销掉。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved