Android 曲线图的绘制示例代码

本文介绍了Android 曲线图的绘制示例代码,分享给大家,具体如下:

效果展示

效果展示.gif

使用方式

// 初始化数据表格相关
with(mTableView) {
  // 配置坐标系
  setupCoordinator("日", "人", /*这里是横坐标的值*/0f, 5f, 10f, 15f, 20f, 25f, 30f)
  // 添加曲线, 确保纵坐标的数值位数相等
  addWave(ContextCompat.getColor(this@MainActivity, R.color.colorYellow), false,
      0f, 10f, 30f, 54f, 30f, 100f, 10f)
  addWave(ContextCompat.getColor(this@MainActivity, R.color.colorGreen), false,
      0f, 30f, 20f, 20f, 46f, 25f, 5f)
  addWave(ContextCompat.getColor(this@MainActivity, R.color.colorPink), false,
      0f, 30f, 20f, 50f, 46f, 30f, 30f)
  addWave(Color.parseColor("#8596dee9"), true,
      0f, 15f, 10f, 10f, 40f, 20f, 5f)
}

实现思路

  1. 横坐标是固定的, 纵坐标需要跟随曲线传入的数值去动态的调整
  2. 绘制坐标轴: 纵横交错的网格
  3. 根据用户传入坐标数值去绘制坐标轴上的数值
  4. 给X轴和Y轴添加单位信息
  5. 根据用户传入的具体的数值绘制曲线(这里不采用Bezier, 不容易精确的控制顶点的位置)
  6. 绘制填充效果
  7. 添加属性动画

代码实现

/**
 * Created by FrankChoo on 2017/12/29.
 * Email: frankchoochina@gmail.com
 * Version: 1.0
 * Description: 表格自定义View
 */
public class TableView extends View {

  private List<WaveConfigData> mWaves;// 数值集合
  // 坐标轴的数值
  private int mCoordinateYCount = 8;
  private float[] mCoordinateXValues;// 外界传入
  private float[] mCoordinateYValues;// 动态计算
  // 坐标的单位
  private String mXUnit;
  private String mYUnit;
  // 所有曲线中所有数据中的最大值
  private float mGlobalMaxValue;// 用于确认是否需要调整坐标系
  private Paint mCoordinatorPaint;
  private Paint mTextPaint;
  private Paint mWrapPaint;
  // 坐标轴上描述性文字的空间大小
  private int mTopUnitHeight;// 顶部Y轴单位高度
  private int mBottomTextHeight;
  private int mLeftTextWidth;
  // 网格尺寸
  private int mGridWidth, mGridHeight;
  private float mAnimProgress;

  public TableView(Context context) {
    this(context, null);
  }

  public TableView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public TableView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
    post(new Runnable() {
      @Override
      public void run() {
        showAnimator();
      }
    });
  }

  private void init() {
    // 初始化数据集合的容器
    mWaves = new ArrayList<>();
    // 坐标系的单位
    mBottomTextHeight = dp2px(40);// X轴底部字体的高度
    mLeftTextWidth = mBottomTextHeight;// Y轴左边字体的宽度
    mTopUnitHeight = dp2px(30);// 顶部Y轴的单位
    // 初始化坐标轴Paint
    mCoordinatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    mCoordinatorPaint.setColor(Color.LTGRAY);
    // 初始化文本Paint
    mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    mTextPaint.setColor(Color.GRAY);
    mTextPaint.setTextSize(sp2px(12));
    // 初始化曲线Paint
    mWrapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    mWrapPaint.setPathEffect(new CornerPathEffect(200f));
  }

  /**
   * 配置坐标轴信息
   *
   * @param xUnit       X 轴的单位
   * @param yUnit       Y 轴的单位
   * @param coordinateXValues X 坐标轴上的数值
   */
  public void setupCoordinator(String xUnit, String yUnit, float... coordinateXValues) {
    mXUnit = xUnit;
    mYUnit = yUnit;
    mCoordinateXValues = coordinateXValues;
  }

  /**
   * 添加一条曲线, 确保与横坐标的数值对应
   *
   * @param color
   * @param isCoverRegion
   * @param values
   */
  public void addWave(int color, boolean isCoverRegion, float... values) {
    mWaves.add(new WaveConfigData(color, isCoverRegion, values));
    // 根据value的值去计算纵坐标的数值
    float maxValue = 0;
    for (float value : values) {
      maxValue = Math.max(maxValue, value);
    }
    if (maxValue < mGlobalMaxValue) return;
    mGlobalMaxValue = maxValue;
    // 保证网格的数值都为 5 的倍数
    float gridValue = mGlobalMaxValue / (mCoordinateYCount - 1);
    if (gridValue % 5 != 0) {
      gridValue += 5 - (gridValue % 5);
    }
    // 给纵坐标的数值赋值
    mCoordinateYValues = new float[mCoordinateYCount];
    for (int i = 0; i < mCoordinateYCount; i++) {
      mCoordinateYValues[i] = i * gridValue;
    }
    invalidate();
  }

  @Override
  protected void onDraw(Canvas canvas) {
    drawCoordinate(canvas);
    drawWrap(canvas);
  }

  public void showAnimator() {
    ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        mAnimProgress = (float) animation.getAnimatedValue();
        invalidate();
      }
    });
    animator.start();
  }

  /**
   * 绘制坐标系
   */
  private void drawCoordinate(Canvas canvas) {
    Point start = new Point();
    Point stop = new Point();
    // 1. 绘制横轴线和纵坐标单位
    int xLineCount = mCoordinateYValues.length;
    mGridHeight = (getHeight() - getPaddingTop() - getPaddingBottom() - mBottomTextHeight - mTopUnitHeight) / (xLineCount - 1);
    for (int i = 0; i < xLineCount; i++) {
      start.x = getPaddingLeft() + mLeftTextWidth;
      start.y = getHeight() - getPaddingBottom() - mBottomTextHeight - mGridHeight * i;
      stop.x = getRight() - getPaddingRight();
      stop.y = start.y;
      // 绘制横轴线
      canvas.drawLine(start.x, start.y, stop.x, stop.y, mCoordinatorPaint);
      // 绘制纵坐标单位
      if (i == 0) continue;
      String drawText = String.valueOf((int) mCoordinateYValues[i]);
      Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
      float offsetY = ((fontMetrics.bottom - fontMetrics.top) / 2 + fontMetrics.bottom) / 2;
      float baseLine = start.y + offsetY;
      float left = getPaddingLeft() + mLeftTextWidth / 2 - mTextPaint.measureText(drawText) / 2;
      canvas.drawText(drawText, left, baseLine, mTextPaint);
      // 绘制Y轴单位
      if (i == xLineCount - 1) {
        drawText = mYUnit;
        baseLine = getPaddingTop() + mTopUnitHeight / 2;
        canvas.drawText(drawText, left, baseLine, mTextPaint);
      }
    }
    // 2. 绘制纵轴线和横坐标单位
    int yLineCount = mCoordinateXValues.length;
    mGridWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - mLeftTextWidth) / (yLineCount - 1);
    for (int i = 0; i < yLineCount; i++) {
      start.x = getPaddingTop() + mLeftTextWidth + mGridWidth * i;
      start.y = getPaddingTop() + mTopUnitHeight;
      stop.x = start.x;
      stop.y = getHeight() - mBottomTextHeight - getPaddingBottom();
      // 绘制纵轴线
      canvas.drawLine(start.x, start.y, stop.x, stop.y, mCoordinatorPaint);
      // 绘制横坐标单位
      String drawText = String.valueOf((int) mCoordinateXValues[i]);
      Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
      float offsetY = ((fontMetrics.bottom - fontMetrics.top) / 2 + fontMetrics.bottom) / 2;
      float baseLine = getHeight() - getPaddingBottom() - mBottomTextHeight / 2 + offsetY;
      float left = start.x - mTextPaint.measureText(drawText) / 2;
      // 绘制X轴单位
      if (i == 0) {
        drawText = mXUnit;
        left = getPaddingLeft() + mLeftTextWidth / 2 - mTextPaint.measureText(drawText) / 2;
      }
      canvas.drawText(drawText, left, baseLine, mTextPaint);
    }
  }

  /**
   * 绘制曲线
   */
  private void drawWrap(Canvas canvas) {
    canvas.clipRect(new RectF(
        mLeftTextWidth,
        getPaddingTop() + mTopUnitHeight,
        (getRight() - getPaddingRight()) * mAnimProgress,
        getHeight() - getPaddingBottom() - mBottomTextHeight)
    );
    float yHeight = mGridHeight * (mCoordinateYCount - 1);
    for (WaveConfigData wave : mWaves) {
      Path path = new Path();
      path.moveTo(0, getHeight());
      float maxY = mCoordinateYValues[mCoordinateYCount - 1];// Y轴坐标的最大值
      for (int index = 1; index < wave.values.length; index++) {
        path.lineTo(
            mLeftTextWidth + mGridWidth * index,
            getHeight() - getPaddingBottom() - mBottomTextHeight
                - yHeight * (wave.values[index] / maxY)
        );
      }
      if (wave.isCoverRegion) {
        mWrapPaint.setStyle(Paint.Style.FILL);
        path.lineTo(getRight() - getPaddingRight(), getHeight());
        path.close();
      } else {
        mWrapPaint.setStyle(Paint.Style.STROKE);
        mWrapPaint.setStrokeWidth(10);
      }
      mWrapPaint.setColor(wave.color);
      canvas.drawPath(path, mWrapPaint);
    }
  }

  private int dp2px(float dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
        dp, getResources().getDisplayMetrics());
  }

  private int sp2px(float sp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
        sp, getResources().getDisplayMetrics());
  }

  public static class WaveConfigData {
    int color;
    boolean isCoverRegion;
    float values[];

    public WaveConfigData(int color, boolean isCoverRegion, float[] values) {
      this.color = color;
      this.isCoverRegion = isCoverRegion;
      this.values = values;
    }
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持呐喊教程。

声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:notice#nhooo.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。