在Android折线图中实现不同Y值区间显示不同颜色

在Android折线图中实现不同Y值区间显示不同颜色

首页休闲益智one color更新时间:2024-06-23

最近在参与一个安卓项目,不忙的时候也会接一些开发任务,遇到一个需求,要实现一个折线图,且根据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