Tears_fg

导航

使用MPAndroidChart实现心跳图

简介

这篇文章主要介绍如何使用MPAndroidChart实现心跳图的效果。

需求分析

之前考虑过用2个linechart上下叠起来,坐标轴上下设置了默认空格,数据需要处理坐标轴为0的情况,多个数据处理比较复杂,数据处理和UI效果不尽如意,最终考虑使用单个linechartview来实现效果,在数据方面我们主要考虑怎么求斜率,在UI层面通用的lineChartView并不能满足我们的需求,我们需要考虑自定义矩形背景,自定义网格线,自定义坐标轴,并且能动态修改坐标轴满足进行中状态的数据变更,还要做到上下2块数据颜色的区分。

三方库介绍

MPAndroidChart 是一款功能强大的 Android 图表开源库,主要功能和特点如下:
1. 支持各种图表类型:线形图、柱状图、饼图、散点图、气泡图、雷达图等。
2. 支持在同一图表内显示多种类型的数据集。
3. 支持手势操作如拖拽、缩放各轴、设置可见范围等。
4. 支持高度自定义图表样式,包括颜色、字体、描述文本等。
5. 支持实时图表,可以动态更新数据。
MPAndroidChart开发文档:https://www.wenjiangs.com/doc/b2k2uz90i

实现

引入三方库

  1. 添加 MPAndroidChart 库的依赖,三方库地址:https://github.com/PhilJay/MPAndroidChart
repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}

初始化linechart

binding.apply {
         lineChart.setDescription(null)
         lineChart.setDrawGridBackground(false) //是否启用网格背景
         lineChart.setDrawMarkers(false)
         lineChart.setDrawBorders(false) //绘制边框
         lineChart.setTouchEnabled(false)
         lineChart.setDragEnabled(false)
         lineChart.setScaleEnabled(false)

        //左边坐标轴
        //隐藏左边坐标轴横网格线
         lineChart.getAxisLeft().setDrawGridLines(false)
         lineChart.getAxisLeft().setDrawLabels(false)
         lineChart.getAxisLeft().setDrawGridLinesBehindData(false)
         lineChart.getAxisLeft().setDrawTopYLabelEntry(false)
         lineChart.getAxisLeft().setDrawZeroLine(false)
         lineChart.axisLeft.isEnabled = true
          
          //右边坐标轴
        //隐藏右边坐标轴横网格线
         lineChart.getAxisRight().setDrawGridLines(false)
         lineChart.getAxisRight().setDrawLabels(false) //右边的刻度
         lineChart.getAxisRight().setDrawGridLinesBehindData(false)
         lineChart.getAxisRight().setDrawTopYLabelEntry(false)
         lineChart.getAxisRight().setDrawZeroLine(false)
         lineChart.axisRight.isEnabled = false

        //x轴
         //隐藏X轴竖网格线
         lineChart.getXAxis().setDrawGridLines(true)
         lineChart.getXAxis().setDrawLabels(true)
         lineChart.getXAxis().setDrawAxisLine(false)
         lineChart.getXAxis().setDrawGridLinesBehindData(false)
         lineChart.xAxis.isEnabled = true
         lineChart.xAxis.gridColor = ColorUtil.xAxisGridColor//垂直的线
         lineChart.xAxis.gridLineWidth = 0.5f //垂直的线宽
         lineChart.xAxis.setLabelCount(7, true)//标签数量 0 - 90 count = 7

         //设置X轴范围
         lineChart.getXAxis().setAxisMinimum(0f);
         lineChart.getXAxis().setAxisMaximum(90f)//只能显示100
         lineChart.setNoDataText("")

        //设置Y轴范围
         lineChart.getAxisLeft().setAxisMinimum(-150f) //数据的最小值
         lineChart.getAxisLeft().setAxisMaximum(150f)
      }

数据填充

  1. 首先我们需要对接口数据进行解析。
      接口数据大致如下:
    "0,0,0,0,100,-48,70,-42,50,10,-30,66,24,100,-10,40,10,54,0,0,-100,-48,-46,26,0,62,-44,0,26,-20,-20,38,-100,-60,0,8,100,-6,100,0,38,60,-100,22,38,26"
  2. 定义2个list存储上下2块数据,并计算斜率,将斜率值存储到2个集合中。
    private fun addData(heartRate: MutableList<Int>) {
       topList.clear()
       bottomList.clear()
       heartRate.add(0, 0)
       var previousEntry: Entry? = null //前一个点的值
       for (i in 0 until heartRate.size) {
          val value = heartRate.get(i)
          val currentEntry = Entry(i.toFloat(), heartRate.get(i).toFloat())
          if (previousEntry != null) {
             if (value > 0 && previousEntry.y < 0) {
                //计算斜率 X轴的坐标点
                val xAxis = CustomLineChartRenderer.getXAxis(previousEntry, currentEntry)
    
                //先添加X轴的交点坐标
                topList.add(Entry(xAxis, 0f))
                topList.add(Entry(i.toFloat(), heartRate.get(i).toFloat()))
                bottomList.add(Entry(xAxis, 0f))
                bottomList.add(Entry(i.toFloat(), 0f))
             } else if (value < 0 && previousEntry.y > 0) {
                //计算斜率 X轴的坐标点
                val xAxis = CustomLineChartRenderer.getXAxis(previousEntry, currentEntry)
    
                //先添加X轴的交点坐标
                bottomList.add(Entry(xAxis, 0f))
                bottomList.add(Entry(i.toFloat(), heartRate.get(i).toFloat()))
                topList.add(Entry(xAxis, 0f))
                topList.add(Entry(i.toFloat(), 0f))
             } else {
                if (value > 0) {
                   topList.add(Entry(i.toFloat(), heartRate.get(i).toFloat()))
                   bottomList.add(Entry(i.toFloat(), 0f))
                } else if (value < 0) {
                   bottomList.add(Entry(i.toFloat(), heartRate.get(i).toFloat()))
                   topList.add(Entry(i.toFloat(), 0f))
                } else {
                   topList.add(Entry(i.toFloat(), 0f))
                   bottomList.add(Entry(i.toFloat(), 0f))
                }
             }
          } else {
             //第一个点
             if (value > 0) {
                topList.add(Entry(i.toFloat(), heartRate.get(i).toFloat()))
                bottomList.add(Entry(i.toFloat(), 0f))
             } else if (value < 0) {
                bottomList.add(Entry(i.toFloat(), heartRate.get(i).toFloat()))
                topList.add(Entry(i.toFloat(), 0f))
             } else {
                topList.add(Entry(i.toFloat(), 0f))
                bottomList.add(Entry(i.toFloat(), 0f))
             }
          }
    
          previousEntry = currentEntry
       }
    
    }
  3. 计算斜率。
    /**
     * 如果已知两点的坐标(x1, y1)和(x2, y2),要计算经过这两点并且与横轴相交的点的坐标(x, 0),可以使用以下公式:
     * <p>
     * 求斜率:k=(y2-y1)/(x2-x1)
     * 直线方程 y-y1=k(x-x1)
     * y = 0, 求x ?
     */
    public static float getXAxis(Entry previousEntry, Entry currentEntry) {
        float k = (currentEntry.getY() - previousEntry.getY()) / (currentEntry.getX() - previousEntry.getX());
        float x = (0 - previousEntry.getY()) / k + previousEntry.getX();
        return x;
    }
  4. 数据填充。
      定义 2个lineDataSet,设置上下2块数据的属性,设置填充模式。
    private fun bindData(values: ArrayList<Int>) {
       var heartRateList = arrayListOf<Int>()
    
       heartRateList.addAll(values)
       addData(heartRateList)
    
       //第一部分
       val set = LineDataSet(topList, null)
       set.setDrawCircles(false) //不显示圆点
       set.setDrawValues(false) //不显示数值
       set.color = Color.TRANSPARENT //线条颜色
       set.setDrawFilled(true);
       set.setDrawCircles(false)
       set.setMode(LineDataSet.Mode.LINEAR);//填充模式
       set.fillColor = ColorUtil.topFillColor
       set.fillAlpha = 225
    
       //第2部分
       val setTwo = LineDataSet(bottomList, null)
       setTwo.setDrawCircles(false) //不显示圆点
       setTwo.setDrawValues(false) //不显示数值
       setTwo.color = Color.TRANSPARENT //线条颜色
       setTwo.setDrawFilled(true);
       setTwo.setMode(LineDataSet.Mode.LINEAR);
       setTwo.fillColor = ColorUtil.bottomFillColor
       setTwo.fillAlpha = 225
       binding.lineChart.setData(LineData(set, setTwo))
       //lineChart.invalidate()// 仅刷新LineChart的显示,不会改变数据和样式等设置。
       //lineChart.notifyDataSetChanged()//会彻底重置并重新绘制LineChart,数据、样式以及属性设置都会生效。
    
    
    }
  5. 刷新数据。
    binding.lineChart.notifyDataSetChanged()     // 通知数据集变化
    binding.lineChart.invalidate()

自定义坐标轴、网格线

  1. 代码中设置坐标轴显示的文本。
    binding.lineChart.xAxis.valueFormatter = object : DefaultAxisValueFormatter(0) {
       override fun getFormattedValue(value: Float): String {
          val formatValue = super.getFormattedValue(value)
          //mXAxisRenderer
          if (formatValue == "45") {
             return "HT"
          }
          return "$formatValue" + "'"
       }
  2. 自定义类CustomXAxisRenderer继承XAxisRenderer。
  3. 初始化画笔。
    private Paint dashPaint;
    
    public CustomXAxisRenderer(Context context, ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans) {
        super(viewPortHandler, xAxis, trans);
        this.context = context;
        bitmap = BitmapFactory.decodeResource(context.getResources(), android.R.drawable.arrow_down_float);
    
        dashPaint = new Paint();
        dashPaint.setColor(ColorUtil.dashColor);
        dashPaint.setStyle ( Paint.Style.STROKE ) ;
        dashPaint.setStrokeWidth(0.5f);
        dashPaint.setAntiAlias(true);
        float[] dash = {2,7};  // 虚线段长和间隙
        dashPaint.setPathEffect(new DashPathEffect(dash, 0));
        //单个网格背景的高度
        xAxisGradBgHeight = mViewPortHandler.contentHeight() / 2f / 3f ;
        bgRectBottomPaint = new Paint();
        bgRectBottomPaint.setColor(ColorUtil.BG_Rect_BOTTOM);
        bgRectBottomPaint.setStyle(Paint.Style.FILL);
    
    }
  4. 重写renderGridLines方法,绘制自定义的y轴网格线。
      坐标轴的数量在初始化的时候已经定好了。
    /**
     * 处理绘制X轴的 label 文本绘制
     * @param c
     */
    @Override
    public void renderGridLines(Canvas c) {
    
        if (!mXAxis.isDrawGridLinesEnabled() || !mXAxis.isEnabled())
            return;
    
        int clipRestoreCount = c.save();
        c.clipRect(getGridClippingRect());
    
        if(mRenderGridLinesBuffer.length != mAxis.mEntryCount * 2){
            mRenderGridLinesBuffer = new float[mXAxis.mEntryCount * 2];
        }
        float[] positions = mRenderGridLinesBuffer;
    
        int htPosition = -1;
        for (int i = 0; i < positions.length; i += 2) {
            positions[i] = mXAxis.mEntries[i / 2]; //自己 定义的X轴 标签,7个
            positions[i + 1] = mXAxis.mEntries[i / 2];
    
            if (positions[i] == 45) {
                htPosition = i;
            }
        }
    
        mTrans.pointValuesToPixel(positions); //时间点 转 像素坐标
    
        setupGridPaint();
    
        Path gridLinePath = mRenderGridLinesPath;
        gridLinePath.reset();
    
        for (int i = 0; i < positions.length; i += 2) {
            if (htPosition == i) {
                //绘制HT
                mGridPaint.setColor(ColorUtil.xAxisGridLineColor_HT);
                drawGridLine(c, positions[i], positions[i + 1], gridLinePath);
            } else {
                mGridPaint.setColor(ColorUtil.xAxisGridLineColor);
                drawGridLine(c, positions[i], positions[i + 1], gridLinePath);
            }
        }
        //还原
        mGridPaint.setColor(ColorUtil.xAxisGridLineColor);
    
        c.restoreToCount(clipRestoreCount);
    }
  5. 坐标转换。
    /**
     * X轴 坐标转化
     * @param xAxisValue X轴的时间点, 比如 30,代表30分钟的X轴坐标点
     * @return 转化后的坐标
     */
    private float[] pointToPixel(float xAxisValue) {
        float[] positions = new float[2];
        positions[0] = xAxisValue;
        positions[1] = 0f;// 0代表离X轴的间距为0, 即代表: 文本距离X轴的高度
        mTrans.pointValuesToPixel(positions); //时间点 转 像素坐标
        return positions;
    }
  6. 重写drawLabel方法,修改坐标轴的颜色值。
      这里也可以处理不同时间点的事件,绘制点球、黄牌、换人、红牌等事件。
     @Override
        protected void drawLabel(Canvas c, String formattedLabel, float x, float y, MPPointF anchor, float angleDegrees) {
            if ("HT".equals(formattedLabel)) {
                mAxisLabelPaint.setColor(ColorUtil.Lable_Color_HT);
                mGridPaint.setColor(ColorUtil.Lable_Color_HT);
                super.drawLabel(c, formattedLabel, x, y, anchor, angleDegrees);
            } else {
                mAxisLabelPaint.setColor(ColorUtil.Lable_Color_other);
                mGridPaint.setColor(ColorUtil.Lable_Color_other);
                super.drawLabel(c, formattedLabel, x, y, anchor, angleDegrees);
            }
            if( onCustomXAxisRendererListener != null){
            List<EventData> eventDataList = onCustomXAxisRendererListener.getDrawEventList();
            if (eventDataList != null) {
                //绘制事件、黄牌、换人、红牌
                float lineWidth = dp2px(0.5f);
                float spacing = 0f; //间距
                for (EventData eventDatum : eventDataList) {
                    float[] positions = pointToPixel(eventDatum.getTime() / 1f);
    //                position 0中立 1 主队 2 客队
                    var res = getEventIcon(eventDatum);
                    bitmap = BitmapFactory.decodeResource(context.getResources(), res);
                    newBitmap = createScaledBitmap(bitmap);
                    if (eventDatum.getPosition() == 1) {
                        //正数
    //                    c.drawLine(positions[0] - lineWidth / 2f, positions[1] / 2, positions[0] + lineWidth / 2f, positions[1], dashPaint);
                        c.drawBitmap(newBitmap, positions[0] - newBitmap.getWidth() / 2f, positions[1] / 2 - newBitmap.getHeight() - spacing, null);
                    } else if (eventDatum.getPosition() == 2) {
                        //负数
    //                    c.drawLine(positions[0] - lineWidth / 2f, positions[1] + positions[1] / 2, positions[0] + lineWidth / 2f, positions[1], dashPaint);
                        //绘制球
                        c.drawBitmap(newBitmap, positions[0] - newBitmap.getWidth() / 2f, positions[1] + positions[1] / 2 + spacing, null);
    
                    }
                    if(bitmap != null) {
                        bitmap.recycle();
                    }
                    if(newBitmap != null) {
                        newBitmap.recycle();
                    }
                }
            }
                 }
        }

自定义背景

  1. 自定义CustomLineChartRenderer 实现LineChartRenderer。
  2. 初始化CustomLineChartRenderer。
    public CustomLineChartRenderer(Context context, LineDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
        this.context = context;
        mChart = chart;
    
        paintProgress = new Paint();
        paintProgress.setColor(ColorUtil.xAxisGridLineColor_HT);
        paintProgress.setStyle(Paint.Style.FILL);
    
    
        textPaint = new Paint();
        textPaint.setColor(ColorUtil.xAxisGridLineColor_HT);
        textPaint.setTextSize(dp2px(10f));
        textPaint.setAntiAlias(true);
    
        paintGrey = new Paint();
        paintGrey.setColor(ColorUtil.penaltyKickColor);
        paintGrey.setStyle(Paint.Style.FILL);
    
        bgRectTopPaint = new Paint();
        bgRectTopPaint.setColor(ColorUtil.BG_Rect_TOP);
        bgRectTopPaint.setStyle(Paint.Style.FILL);
    
        bgRectBottomPaint = new Paint();
        bgRectBottomPaint.setColor(ColorUtil.BG_Rect_BOTTOM);
        bgRectBottomPaint.setStyle(Paint.Style.FILL);
    
        dashPaint = new Paint();
        dashPaint.setColor(ColorUtil.dashColor);
        dashPaint.setStyle(Paint.Style.STROKE);
        dashPaint.setStrokeWidth(1f);
        dashPaint.setAntiAlias(true);
        float[] dash = {2, 7};  // 虚线段长和间隙
        dashPaint.setPathEffect(new DashPathEffect(dash, 0));
    
    }
  3. 设置单个网格背景的高度。
     @Override
        protected void drawDataSet(Canvas c, ILineDataSet dataSet) {
            //四个角的空间 偏移量 + 内容区域,就是整个LineChart的区域
    //        Log.e("tag", "path offsetLeft = " + mViewPortHandler.offsetLeft());
    //        Log.e("tag", "path offsetTop = " + mViewPortHandler.offsetTop());
    //        Log.e("tag", "path offsetRight = " + mViewPortHandler.offsetRight());
    //        Log.e("tag", "path offsetBottom = " + mViewPortHandler.offsetBottom());
    //        Log.e("tag", "path contentTop = " + mViewPortHandler.contentTop());
            xAxisGradBgHeight = mViewPortHandler.contentHeight() / 2f / 3f;
            super.drawDataSet(c, dataSet);
    
    
        }
  4. 重写drawFillPath方法绘制背景。
        @Override
        protected void drawFilledPath(Canvas c, Path filledPath, int fillColor, int fillAlpha) {
            bounds = new RectF();
            //获取填充颜色的path的矩形范围
            filledPath.computeBounds(bounds, false);
            if (onCustomLineChartRendererListener != null) {
                penaltyData = onCustomLineChartRendererListener.drawPenaltyData();
            }
            //数据为0,绘制底部灰色区块
            if (penaltyData != null && penaltyData.getTotal() == 0) {
                bgRectBottomPaint.setColor(ColorUtil.BG_Rect_BOTTOM);
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 2, getRightDrawRegion(penaltyData), mViewPortHandler.offsetTop() + xAxisGradBgHeight * 4), bgRectBottomPaint);
                return;
            }
            //x轴的下方
            if (floatEqual(bounds.top,mViewPortHandler.contentHeight() / 2 + mViewPortHandler.offsetTop())) {
                //绘制底部灰色区块(所有区域)
                bgRectBottomPaint.setColor(ColorUtil.BG_Rect_BOTTOM);
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3, getRightDrawRegion(penaltyData), mViewPortHandler.offsetTop() + xAxisGradBgHeight * 4), bgRectBottomPaint);
                //绘制底部灰色区域(内容部分)
                bgRectBottomPaint.setColor(ColorUtil.BG_Rect_BOTTOM_2);
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3, bounds.right, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 5), bgRectBottomPaint);
                //绘制底部灰色区域(第二内容部分)
                bgRectBottomPaint.setColor(ColorUtil.BG_Rect_BOTTOM);
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3, bounds.right, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 4), bgRectBottomPaint);
    //
            } else {
                //x轴的上方
                bgRectTopPaint.setColor(ColorUtil.BG_Rect_TOP);
                //绘制第一部分绿色矩形(仅仅内容区域)
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 1, bounds.right, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 2), bgRectTopPaint);
                bgRectTopPaint.setColor(ColorUtil.BG_Rect_BOTTOM);
                //绘制第二部分所有底部灰色区块(所有区域)
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 2, getRightDrawRegion(penaltyData), mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3), bgRectTopPaint);
                //绘制第二部分底部内容区域绿色区块(内容区域)
                bgRectTopPaint.setColor(ColorUtil.BG_Rect_TOP_2);
                c.drawRect(new RectF(bounds.left, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 2, bounds.right, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3), bgRectTopPaint);
    
            }
    
            super.drawFilledPath(c, filledPath, fillColor, fillAlpha);
    
    
        }
    
       public boolean floatEqual(float a, float b){
           return Math.abs(Double.parseDouble(df.format(a))-Double.parseDouble(df.format(b))) < 0.5f;
       };
drawFilledPath这步是绘制贝塞尔曲线,所以我们自定义背景需要在这步之前。

呼吸线实现

LineChartRenderer的父类onDraw方法中有个drawExtras()方法,这步在填充完数据后,这样我们可以添加我们额外的功能,如呼吸线、点球,自定义坐标轴等。
  @Override
    public void drawExtras(Canvas c) {
        super.drawExtras(c);
        //绘制最右边的深色线 压在填充色上面
        c.drawRect(new RectF(bounds.right - gridLineWidth / 2f, xAxisGradBgHeight * 1 + mViewPortHandler.offsetTop(), bounds.right + gridLineWidth / 2f, xAxisGradBgHeight * 5 + mViewPortHandler.offsetTop()), paintProgress);

        if (onCustomLineChartRendererListener != null) {
            penaltyData = onCustomLineChartRendererListener.drawPenaltyData();
        }
         //绘制坐标轴
        textPaint.setColor(ColorUtil.axisColor);
        c.drawRect(new RectF(bounds.right, mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3, mViewPortHandler.contentRight(), mViewPortHandler.offsetTop() + xAxisGradBgHeight * 3 + axisHeight), paintGrey);

    }
如何让我们的线实现呼吸效果呢,这里我使用了渐变动画,并且通过view的postInvalidata实现了局部刷新。
ValueAnimator anim;
Handler handler = new Handler() ;
Runnable runnable;
public void startAnimation() {
    if(anim == null) {
        anim = ValueAnimator.ofFloat(1, 0);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAlpha = (float) animation.getAnimatedValue();

            }
        });
        anim.setDuration(800);
        anim.setRepeatCount(ValueAnimator.INFINITE);
        anim.setRepeatMode(ValueAnimator.REVERSE);

    }
    anim.start();
    if(runnable == null) {
        runnable = new Runnable() {
            @Override
            public void run() {
                if (mChart instanceof LineChart && bounds != null) {
                    ((LineChart) mChart).postInvalidate((int)(bounds.right - gridLineWidth / 2f), (int)(xAxisGradBgHeight * 1 + mViewPortHandler.offsetTop()), (int)(bounds.right + gridLineWidth / 2f), (int)(xAxisGradBgHeight * 5 + mViewPortHandler.offsetTop()));

                }
                handler.postDelayed(this, 400);
            }
        };
    }
    handler.postDelayed(runnable,0);

}
动态变更paint。
@Override
public void drawData(Canvas c) {
    paintProgress.setAlpha((int) (255 * mAlpha));  // 设置透明度.setAlpha((int) (255 * mAlpha));  // 设置透明度
    super.drawData(c);
}
注意在适当的时候进行动画的取消。
public void cancelAnim(){
    if (anim != null && anim.isRunning()) anim.cancel();
        handler.removeCallbacksAndMessages(null);
}
除此之外我们还可以动态变更坐标轴。
binding.lineChart.xAxis.axisMaximum = 120f
binding.lineChart.notifyDataSetChanged()     // 通知数据集变化
binding.lineChart.invalidate()

优化

由于项目中有视频直播,视频播放的时候会产生卡顿,分析发现,心跳图的绘制会发生掉帧的情况,因此考虑在linechatView同级布局中增加一个view,通过linechatview数据位置来动态更新view的位置。

增加长度为1dp的呼吸线view


<FrameLayout
   android:id="@+id/flContainer"
   android:layout_width="0dp"
   android:layout_height="100dp"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toEndOf="@id/llLayout"
   app:layout_constraintTop_toTopOf="parent">

   <com.github.mikephil.charting.charts.LineChart
      android:id="@+id/lineChart"
      android:layout_width="match_parent"
      android:layout_height="match_parent"

      />
   <View
      android:id="@+id/vLine"
      android:layout_width="1dp"
      android:layout_height="wrap_content"
      android:visibility="gone"
      />
</FrameLayout>
根据比赛状态来开启或者取消动画。这里创建了一个定时器,1s通知外部view更新位置。TimerTask每次取消后都需要新建,否则会报错。

开启、取消定时器

 ValueAnimator anim;
    Timer timer =  new Timer();
    TimerTask timerTask;

    public void startAnimation() {
        cancelAnim();    
        timerTask=  new TimerTask() {
            @Override
            public void run() {
                // 执行动画
                if (mChart instanceof LineChart && bounds != null) {
//                        LogUtils.e("linechart----(int)bounds.right   }-"+(int)bounds.right+"---"+(int) (xAxisGradBgHeight * 1 + mViewPortHandler.offsetTop()));
                    onCustomLineChartRendererListener.drawLine((int)bounds.right,(int) (xAxisGradBgHeight * 1 + mViewPortHandler.offsetTop()),(int)(xAxisGradBgHeight * 4));
                }
            }
        }; //首次0毫秒后执行,之后每1秒更新一次位置
    timer.schedule(timerTask, 0, 1000);


    }

    public void cancelAnim() {
        if (anim != null && anim.isRunning()) anim.cancel();
        if(timerTask != null) {
            timerTask.cancel();
        }

    }

通知view执行呼吸动画

override fun drawLine(marginStart: Int, marginTop: Int,height:Int) {
   job?.cancel()
   job = lifecycleScope.launch {
      withContext(Dispatchers.Main) {
         val layoutParams = FrameLayout.LayoutParams(1.dp, height)
         layoutParams.marginStart = marginStart
         layoutParams.topMargin = marginTop
         binding.vLine.layoutParams = layoutParams
         if (alphaAnimation == null) {
            alphaAnimation = AnimationUtils.loadAnimation(context, R.anim.anim_breathe)
         }
         if(binding.vLine.isGone) binding.vLine.isShow(true)
         if (binding.vLine.animation == null) {
            binding.vLine.startAnimation(alphaAnimation)
         }
      }
   }


}
anim_breathe.xml文件
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="500"
        android:fromAlpha="1"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:repeatCount="infinite"
        android:fillAfter="true"
        android:repeatMode="reverse"
        android:toAlpha="0" />
</set>

问题及解决

问题:
当2个球队只有一个球队有数据,会出现坐标轴从左下角开始的情况。
解决:
DefaultFillFormatter 是一个用于配置图表填充区域样式的类。我们可以发现当max或者min为正数和负数的时候,最小填充位置为0,其他时候位置则为最小值或者最大值。
public float getFillLinePosition(ILineDataSet dataSet, LineDataProvider dataProvider) {

    float fillMin = 0f;
    float chartMaxY = dataProvider.getYChartMax();
    float chartMinY = dataProvider.getYChartMin();

    LineData data = dataProvider.getLineData();

    if (dataSet.getYMax() > 0 && dataSet.getYMin() < 0) {
        fillMin = 0f;
    } else {

        float max, min;

        if (data.getYMax() > 0)
            max = 0f;
        else
            max = chartMaxY;
        if (data.getYMin() < 0)
            min = 0f;
        else
            min = chartMinY;

        fillMin = dataSet.getYMin() >= 0 ? min : max;
    }

    return fillMin;
}
根据实际情况修改:
//下面的数据
set.fillFormatter =
    IFillFormatter { dataSet, dataProvider ->
        var fillMin = 0f
        if (dataSet.yMax > 0 && dataSet.yMin < 0) {
            fillMin = 0.0f
        } else {
            fillMin = dataSet.yMax
        }

         fillMin }
  //上面的数据
  setTwo.fillFormatter =
    IFillFormatter { dataSet, dataProvider -> 0.0f }

实现效果

posted on 2023-10-08 14:30  Tears_fg  阅读(353)  评论(0编辑  收藏  举报