MPAndroid源码分析

作者: freelifes | 来源:发表于2022-02-09 16:37 被阅读0次

    MPAndroid是开源的图标绘制框架。能够绘制 ,支持绘制折线图,柱状图,组合图,radar图等。

    本文主要解决: Y轴绘制出现了负数,且不能取整。
    Y轴数据.png

    如:左侧出现-18

    类图结构

    MPAndroid.png
    1. 通过类图可知,MPAndroid的Chart继承自ViewGroup. 重写了onMeasure, onLayout,onDraw等方法。
    2. 绘制分为坐标轴AxisRenderer 和 数据区域DataRender.

    AxisRenderer

    AxisRenderer 就是绘制X轴和Y轴。直接子类是YAxisRenderer 和 XAxisRenderer

    class AxisRenderer{
       //通过最大值和最小值得到Entry[]
       protected void computeAxisValues(float min, float max) 
       //绘制Label(就是数值)
       public abstract void renderAxisLabels(Canvas c);
       //绘制网格线
       public abstract void renderGridLines(Canvas c);
       //绘制坐标轴
       public abstract void renderAxisLine(Canvas c);
       //绘制limit
        public abstract void renderLimitLines(Canvas c);
    }
    

    DataRender

    DataRender 绘制折线,柱状图,贝塞尔曲线,数值,其他的如MakerView等

    class DataRenderer{
       //绘制折线,柱状图
        public abstract void drawData(Canvas c);
        //绘制Values
        public abstract void drawValues(Canvas c);
    }
    

    绘制Y轴label的计算规则

    protected void computeAxisValues(float min, float max) {
    
            float yMin = min;
            float yMax = max;
    
            int labelCount = mAxis.getLabelCount();
            double range = Math.abs(yMax - yMin);
    
            double rawInterval = range / labelCount;
            double interval = Utils.roundToNextSignificant(rawInterval);//61-->60  111 =>100  165=>200
    
            // Normalize interval //65 ==》 10   200 =》 100
            //interval 超过50,就是100的间隔  低于50值不变
            double intervalMagnitude = Utils.roundToNextSignificant(Math.pow(10, (int) Math.log10(interval)));
            int intervalSigDigit = (int) (interval / intervalMagnitude);
            if (intervalSigDigit > 5) {
                // Use one order of magnitude higher, to avoid intervals like 0.9 or 90
                // if it's 0.0 after floor(), we use the old value
                interval = Math.floor(10.0 * intervalMagnitude) == 0.0
                        ? interval
                        : Math.floor(10.0 * intervalMagnitude);  //65 => 10 =>10*10 = 100
            }
    
            int n = mAxis.isCenterAxisLabelsEnabled() ? 1 : 0;
            // force label count
            if (mAxis.isForceLabelsEnabled()) {
               //labelcount 默认为6 ,此处interval重新赋值
                interval = (float) range / (float) (labelCount - 1);
                mAxis.mEntryCount = labelCount;
    
                if (mAxis.mEntries.length < labelCount) {
                    // Ensure stops contains at least numStops elements.
                    mAxis.mEntries = new float[labelCount];
                }
                float v = min;
                for (int i = 0; i < labelCount; i++) {
                    mAxis.mEntries[i] = v;
                    v += interval; 
                }
                n = labelCount;
                // no forced count
            } else {
              //mEntry[]的第一个值是和ymin有关,不一定从0开始的。
                double first = interval == 0.0 ? 0.0 : Math.ceil( yMin/ interval) * interval;
                double last = interval == 0.0 ? 0.0 : Utils.nextUp(Math.floor(yMax / interval) * interval);
                double f;
                int i;
                if (interval != 0.0 && last != first) {
                   //依据interval 动态计算label的数量
                    for (f = first; f <= last; f += interval) {
                        ++n;
                    }
                }
                else if (last == first && n == 0) {
                    n = 1;
                }
                mAxis.mEntryCount = n;
                if (mAxis.mEntries.length < n) {
                    // Ensure stops contains at least numStops elements.
                    mAxis.mEntries = new float[n];
                }
                for (f = first, i = 0; i < n; f += interval, ++i) {
    
                    if (f == 0.0) // Fix for negative zero case (Where value == -0.0, and 0.0 == -0.0)
                        f = 0.0;
    
                    mAxis.mEntries[i] = (float) f;
                }
            }
        }
    

    上述分为2种情况,通过mAxis.isForceLabelsEnabled() 判断采用哪种计算规则。这个值是通过 yAxis.setLabelCount(6, true) 传递的。
    简单描述一下2种算法,都是为了确定mEntry[],存储的数据就是要绘制在Y轴上的label,也就是Y轴上的数字:

    1. 如果labelIsEnable() = true
      interval = (max - min) / (labelcount -1)
      mEntry[0] = min ; mEntry[1] = mEntry[0] + interval
      缺点: interval 任意,mEntry[0] 的值也是任意的。不是我们想要10 或者 5 的倍数的数字。
    2. 如果labelIsEnable() = false
      interval计算分为2步
      第一步: interval = (max - min) / labelcount
      第二步: interval = 65 转换成100 ; interval = 44 转换成40
      mEntry[0] = math.ceil(ymin/interval)*interval ;
      mEntry[1] = mEntry[0] + interval;
      缺点:比如65的interval 转成了100 ,导致我们期望6个label,最终可能是5个,间隔过大问题。
      这2种算法,如果不满足我们的需求,可以自定义Y轴。

    自定义Y轴算法

    写一种算法,保证lablecount = 6 , 同时间距也是5的倍数,保证mEntry[0] 也是10的整数。

    public class HyYAxisRenderer extends YAxisRenderer {
       @Override
       protected void computeAxisValues(float min, float max) {
           if (mAxis.isForceLabelsEnabled()) {
                if(mAxis.mEntries != null) {
                int length = mAxis.mEntries.length;
                if (length > 0) {
                   if (max == mAxis.mEntries[length-1]){
                       return;
                     }
                  }
               }
               // interval = (float) range / (float) (labelCount - 1);
               double rangeLength = Math.floor(Math.log10(range / (labelCount - 1)));
               double pow = Math.pow(10, rangeLength);
               float floorMin = (float) (Math.floor(min / pow) * pow);
               if (floorMin > 0) {
                   floorMin = 0;
               }
               interval = getInterval(max, floorMin, (labelCount - 1));
               mAxis.mEntryCount = labelCount;
               if (mAxis.mEntries.length < labelCount) {
                   // Ensure stops contains at least numStops elements.
                   mAxis.mEntries = new float[labelCount];
               }
               float v = floorMin;
               for (int i = 0; i < labelCount; i++) {
                   mAxis.mEntries[i] = v;
                   v += interval;
               }
               n = labelCount;
               // no forced count
           }else { }
           mAxis.mAxisMinimum = mAxis.mEntries[0];
           mAxis.mAxisMaximum = mAxis.mEntries[n - 1];
           mAxis.mAxisRange = Math.abs(mAxis.mAxisMaximum - mAxis.mAxisMinimum);
    }
    public double getInterval(float max, float min, int count) {
           float range2 = (max - min) / count;
           double rangeLength2 = Math.floor(Math.log10(range2));
           double pow = Math.pow(10, rangeLength2);
           float floorMax = (float) (Math.ceil(max / pow) * pow);
           int interval = (int) Math.ceil((floorMax - min) / count);
    
           double finalInterval = interval;
           if (interval > 5 && interval < 100) { //
               double aa = Math.floor(interval / 5);
               if (aa * 5 * count < floorMax) {
                   finalInterval = (aa + 1) * 5;
               } else {
                   finalInterval = aa * 5;
               }
           }
           return finalInterval;
       }
    } 
    

    主要是getInterval()方法,处理isForceLabelsEnabled() 为true的情况,计算最终的Interval,不会出现任意数字,label为5或者10的倍数。

    使用自定义的Y轴

    HyYAxisRenderer hyYAxisRenderer = new HyYAxisRenderer(chart.getViewPortHandler(),yAxis,chart.getTransformer(YAxis.AxisDependency.LEFT));
    chart.setRendererLeftYAxis(hyYAxisRenderer);
    yAxis.setLabelCount(6, true);
    

    其他类图

    MpAndroid_dataSet.png

    其他问题

    1.Y轴坐标的mAxisMinimum 和 mAxisMaximum 在计算的时候,默认会增加SpaceBottom和SpaceTop距离,可能是为了方便绘制贝塞尔曲线,防止越界问题,不使用,可以设置为0。

    public class YAxis extends AxisBase {
     this.mAxisMinimum = mCustomAxisMin ? this.mAxisMinimum : min - (range / 100f) * getSpaceBottom();
     this.mAxisMaximum = mCustomAxisMax ? this.mAxisMaximum : max + (range / 100f) * getSpaceTop();
     this.mAxisRange = Math.abs(this.mAxisMinimum - this.mAxisMaximum); // 912
    
    1. xAxis.setCenterAxisLabels(true); 设置数字居中,默认只有X坐标会生效。Y轴通过设置offsetX,offsetY实现偏移。
    public class XAxisRenderer extends AxisRenderer {
     protected void drawLabels(Canvas c, float pos, MPPointF anchor) {
            boolean centeringEnabled = mXAxis.isCenterAxisLabelsEnabled();
    
            float[] positions = new float[mXAxis.mEntryCount * 2];
    
            for (int i = 0; i < positions.length; i += 2) {
    
                // only fill x values
                if (centeringEnabled) {
                    positions[i] = mXAxis.mCenteredEntries[i / 2];
                } else {
                    positions[i] = mXAxis.mEntries[i / 2];
                }
            }
    

    效果

    图一.png
    图二.png
    图三

    图三对比图二就是去掉默认padding, 坐标轴也不是负数了。

    相关文章

      网友评论

        本文标题:MPAndroid源码分析

        本文链接:https://www.haomeiwen.com/subject/qkdbkrtx.html