Android 曲线图的绘制

作者: SharryChoo | 来源:发表于2018-02-23 13:53 被阅读838次

    效果展示

    效果展示.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;
            }
        }
    }
    
    

    相关文章

      网友评论

        本文标题:Android 曲线图的绘制

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