Android 动态折线图表实现

原创
04/24 19:11
阅读数 72

一、动态折线图效果

(为了便于观察,初始等待了5秒)

二、代码实现

package com.appwidget;


public class LineChartView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private float OUT_LINE_RADIUS = 0f;
    private final String TAG = "LineChartView";
    private final String TAG_ERROR = "ChartViewError";
    private Thread mRenderThread = null;
    private TextPaint mTextPaint;
    private Paint mPathPaint;
    private DisplayMetrics mDisplayMetrics;
    private volatile boolean mIsDrawing = false;
    private int contentHeight = 0;
    private int contentWidth = 0;
    private float PATH_EFFECT_CONER = 0l;
    private float OUT_LINE_WIDTH = 0;
    private String lineChartName = "峰值";

    private int maxYAxis = 2;
    private final List<Point> mPathPoints = new ArrayList<>();
    private final int MAX_VIEW_POINT_SIZE = 15;
    private final int MAX_CACHE_POINT_SIZE = 5;

    private final Pair<Integer, Integer> GRID_LINE_COLOR = new Pair<Integer, Integer>(0xeeC9C9C9, 0xddd4d4d4);
    private final ColorPair[] COLOR_PAIRS = new ColorPair[]{
            ColorPair.create(ColorPair.HIGH, 0xffFE5561, 0xaaFFBBC0),
            ColorPair.create(ColorPair.MIDDLE, 0xffFEA315, 0xaaFFDAA1),
            ColorPair.create(ColorPair.NORMAL, 0xff25C384, 0xaaA8E7CE),
    };
    private int mXAxis = 10;
    private final int maxXAxis = 10;
    private float mXOffsetLeft = 0f;
    private final LinkedList<Integer> mPercentList = new LinkedList<>();
    private boolean useTransitionPoint = true;
    private volatile boolean isFirstDraw = true;
    private int colorLevel = ColorPair.HIGH;
    private SurfaceHolder mHolder = null;
    private boolean showYAxisScale = false;

    public void setShowYAxisScale(boolean showYAxisScale) {
        this.showYAxisScale = showYAxisScale;
    }

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

    public LineChartView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LineChartView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initRenderPaint();
        // setSurfaceTextureListener(this);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public LineChartView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initRenderPaint();
        // setSurfaceTextureListener(this);

    }

    public void setMaxYAxis(int maxYAxis) {
        if (maxYAxis <= 0) {
            throw new IllegalArgumentException(" maxYAxis must be postive num");
        }
        this.maxYAxis = maxYAxis;
    }

    public void setUseTransationPoint(boolean useTransationPoint) {
        this.useTransitionPoint = useTransationPoint;
    }

    private void initRenderPaint() {
        mDisplayMetrics = getResources().getDisplayMetrics();

        OUT_LINE_WIDTH = dp2px(1);
        PATH_EFFECT_CONER = dp2px(3);
        OUT_LINE_RADIUS = dp2px(2);
        mXOffsetLeft = OUT_LINE_WIDTH / 2;
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(sp2px(15));
        mTextPaint.setColor(0x99666666);

        mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPathPaint.setAntiAlias(true);
        mHolder = this.getHolder();
        mHolder.addCallback(this);


    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        synchronized (this) {
            mIsDrawing = true;
        }
        startChartViewThread();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        contentWidth = width;
        contentHeight = height;

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d(TAG, "surfaceDestroyed");
        synchronized (this) {
            mIsDrawing = false;
        }
    }

    @Override
    public void run() {

        do {
            synchronized (this) {
                if (!mIsDrawing) {
                    return;
                }
                drawSurface();
            }
            sleepWaitTimeout(100);
        } while (true);
    }

    private void sleepWaitTimeout(long time) {
        try {
            TimeUnit.MILLISECONDS.sleep(time);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void drawSurface() {
        Canvas canvas = null;
        try {
            if (!isAvailable()) {
                return;
            }
            //锁定画布并返回画布对象
            canvas = mHolder.lockCanvas();
            if (canvas == null) {
                return;
            }
            onDrawSurface(canvas);
        } catch (Error e) {
            e.printStackTrace();
            Log.e(TAG_ERROR, e.getLocalizedMessage() + "", e);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG_ERROR, e.getLocalizedMessage() + "", e);
        } finally {
            //当画布内容不为空时,才post,避免出现黑屏的情况。
            if (canvas != null) {
                mHolder.unlockCanvasAndPost(canvas);
            }
        }

    }

    private void onDrawSurface(Canvas canvas) {
        if (contentHeight * contentWidth == 0) {
            return;
        }

        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        canvas.drawColor(Color.WHITE);
        //需要清理画布,否则可能出现Path close煽动效果
        int saveCount = canvas.save();
        canvas.translate(0, contentHeight);
        mPathPaint.setStrokeWidth(OUT_LINE_WIDTH);
        mPathPaint.setColor(GRID_LINE_COLOR.first);
        mPathPaint.setStyle(Paint.Style.STROKE);
        float leftWidth = 0f;
        int Ny = getYScale();
        CharSequence[] texts = new CharSequence[Ny + 1];
        float zoneHeight = (contentHeight - OUT_LINE_WIDTH) / Ny;

        if (showYAxisScale) {
            float perScale = maxYAxis * 1f / (Ny + 1);
            float maxTextWidth = 0f;
            for (int i = 0; i < texts.length; i++) {
                float v = perScale * (i + 1);
                texts[i] = String.format(Locale.CHINA, maxYAxis >= 5 ? "%.0fGB" : "%.1fGB", v);
                float w = mTextPaint.measureText(texts[i].toString());
                maxTextWidth = w > maxTextWidth ? w : maxTextWidth;
            }
            leftWidth = maxTextWidth + dp2px(5);
        }

        RectF chartRect = new RectF();
        chartRect.left = OUT_LINE_WIDTH / 2 + leftWidth;
        chartRect.right = contentWidth - OUT_LINE_WIDTH / 2;
        chartRect.top = -contentHeight + OUT_LINE_WIDTH / 2;
        chartRect.bottom = -OUT_LINE_WIDTH / 2;

        drawGridRect(canvas, chartRect);
        clipRoundRect(canvas, chartRect);
        drawPathPoint(canvas, chartRect);

        canvas.restoreToCount(saveCount);

        saveCount = canvas.save();
        canvas.translate(0, contentHeight);

        if (showYAxisScale) {
            float baseline = getTextPaintBaseline(mTextPaint);
            for (int i = 0; i < texts.length; i++) {
                CharSequence cs = texts[i];
                if (cs == null) continue;
                float y = chartRect.bottom + (-i * zoneHeight) + baseline;
                if (i == 0) {
                    y = y - baseline;
                } else if (i == (texts.length - 1)) {
                    y = y + baseline;
                }
                canvas.drawText(cs.toString(), 0, y, mTextPaint);
            }
        }
        canvas.restoreToCount(saveCount);
        addNextPoint(chartRect);
        movePoints(chartRect, mPathPoints);

        drawLineChartNameText(canvas, chartRect);

    }

    private void drawLineChartNameText(Canvas canvas, RectF chartRect) {
        if (TextUtils.isEmpty(lineChartName)) {
            return;
        }
        float nameTextHeight = getTextHeight(mTextPaint);
        RectF rectBox = new RectF();
        rectBox.left = chartRect.left + dp2px(5);
        rectBox.right = rectBox.left + nameTextHeight;

        rectBox.top = rectBox.top + dp2px(5);
        rectBox.bottom = rectBox.top + nameTextHeight;

        mTextPaint.setColor(0xaaFFBBC0); //0xaaFFBBC0
        canvas.drawRect(rectBox, mTextPaint);
        mTextPaint.setColor(0xffFFBBC0); //0xaaFFBBC0
        canvas.drawText(lineChartName, rectBox.right + dp2px(5), rectBox.centerY() + getTextPaintBaseline(mTextPaint), mTextPaint);
    }


    public static float getTextPaintBaseline(Paint p) {
        Paint.FontMetrics fontMetrics = p.getFontMetrics();
        return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
    }

    //真实宽度 + 笔画上下两侧间隙(符合文本绘制基线)
    private static int getTextHeight(Paint paint) {
        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
        int textHeight = ~fm.top - (~fm.top - ~fm.ascent) - (fm.bottom - fm.descent);
        return textHeight;
    }


    private void drawPathPoint(Canvas canvas, RectF chartRect) {
        ColorPair pair = getPathColorPair(colorLevel);


        float startY = chartRect.height() / 2f;

        addFirstPoint(chartRect.left, -startY);

        Path linePath = new Path();
        List<Point> markPoints = new ArrayList<>(mPathPoints);
        int markSize = markPoints.size();

        for (int i = 0; i < markSize; i++) {
            Point point = markPoints.get(i);
            if (i == 0) {
                linePath.moveTo(point.x, point.y);
            } else {
                linePath.lineTo(point.x, point.y);
            }
        }
        linePath.lineTo(chartRect.right, markPoints.get(markSize - 1).y);


        Path path = new Path(linePath);
        path.lineTo(chartRect.right, chartRect.bottom);
        path.lineTo(chartRect.left + OUT_LINE_RADIUS * 2, chartRect.bottom);
        RectF arcRect = new RectF();
        arcRect.left = chartRect.left;
        arcRect.top = chartRect.bottom - OUT_LINE_RADIUS * 2;
        arcRect.right = chartRect.left + OUT_LINE_RADIUS * 2;
        arcRect.bottom = chartRect.bottom;
        path.arcTo(arcRect, 90, 90, false);
        path.close();

        mPathPaint.setColor(pair.fillColor);
        mPathPaint.setStyle(Paint.Style.FILL);
        mPathPaint.setPathEffect(null);
        canvas.drawPath(path, mPathPaint);


        mPathPaint.setStrokeWidth(OUT_LINE_WIDTH * 3 / 2);
        mPathPaint.setStyle(Paint.Style.STROKE);
        mPathPaint.setColor(pair.lineColor);
        mPathPaint.setPathEffect(new CornerPathEffect(PATH_EFFECT_CONER));
        canvas.drawPath(linePath, mPathPaint);

        path.reset();
        linePath.reset();
        mPathPaint.setStrokeWidth(OUT_LINE_WIDTH);
    }

    private void movePoints(RectF chartRect, List<Point> markPoints) {

        List<Point> drawPoints = new ArrayList<>();
        if (markPoints != null) {
            drawPoints.addAll(markPoints);
        }
        int pointSize = drawPoints.size();
        if (pointSize < 2) {
            return;
        }

        float width = chartRect.width();
        float zoneWidth = width / (MAX_VIEW_POINT_SIZE);
        int sum = 0;


        for (int i = 0; i < pointSize; i++) {
            Point point = drawPoints.get(i);
            if (point.x < chartRect.right) {
                sum += 1;
            }
        }

        boolean displayGather = sum < (MAX_VIEW_POINT_SIZE - 1);

        for (int j = 0; j < pointSize; j++) {
            Point point = drawPoints.get(j);
            if (j == 0) {
                if (!displayGather) {
                    point.x = point.x - zoneWidth / 2;
                }
                continue;
            }

            Point prePoint = drawPoints.get(j - 1);
            float x1 = prePoint.x;
            float x2 = point.x;
            if (x2 - x1 > zoneWidth) {
                float offsetWidth = (x2 - x1) / 2;
                point.x = x2 - offsetWidth;
                x2 = point.x;
            }
            if (x2 - x1 <= zoneWidth) {
                point.x = prePoint.x + zoneWidth;
            }
        }
        trimSize();
    }


    private void trimSize() {
        int size = mPathPoints.size();
        while (size > (MAX_VIEW_POINT_SIZE)) {
            Point top = mPathPoints.get(1);
            if (top.x >= mXOffsetLeft) {
                //防止线条跳线
                break;
            }
            mPathPoints.remove(0);
            size = mPathPoints.size();
        }
        if (size <= 2) {
            return;
        }
        //清理无效数据,防止锁屏等没有新数据等情况下无效数据堆积问题
        Point point = mPathPoints.get(size - 1);
        if (point.x < mXOffsetLeft) {
            mPathPoints.clear();
        }
    }


    private void addNextPoint(RectF chartRect) {
        Integer percent = null;

        synchronized (mPercentList) {
            percent = mPercentList.poll();
            while ((percent == null || percent < 0) && !mPercentList.isEmpty()) {
                percent = mPercentList.poll();
            }
        }
        if (percent == null || percent.intValue() < 0) {
            return;
        }
        float ratio = (float) percent / 100f;
        int size = mPathPoints.size();
        float y = ratio * chartRect.height();
        float x = chartRect.right + chartRect.width() / MAX_VIEW_POINT_SIZE;

        if (size > 2 && useTransitionPoint) {
            //过度点
            float fixY = (mPathPoints.get(size - 1).y + (-y)) / 2;
            addPoint(chartRect.right, fixY, true);
        }
        addPoint(x, -y, false);
    }

    public void addPercent(int percent) {
        synchronized (mPercentList) {
            while (mPercentList.size() > MAX_CACHE_POINT_SIZE) {
                mPercentList.removeFirst();
            }
            mPercentList.add(new Integer(percent));
        }

    }

    private void startChartViewThread() {
        if (!isAttachedToWindow()) {
            return;
        }
        if (!isAvailable()) {
            return;
        }
        mRenderThread = new Thread(this);
        mRenderThread.start();
    }

    public boolean isAvailable() {
        return mHolder != null && mHolder.getSurface() != null;
    }

    private void clipRoundRect(Canvas canvas, RectF chartRect) {
        Path clipRectPath = new Path();
        clipRectPath.addRoundRect(chartRect, OUT_LINE_RADIUS, OUT_LINE_RADIUS, Path.Direction.CCW);
        canvas.clipPath(clipRectPath);
    }

    private void drawGridRect(Canvas canvas, RectF chartRect) {

        canvas.drawRoundRect(chartRect, OUT_LINE_RADIUS, OUT_LINE_RADIUS, mPathPaint);

        float zoneHeight = 0f;
        float zoneWidth = chartRect.width() / maxXAxis;

        int Ny = getYScale();

        zoneHeight = chartRect.height() / Ny;
        mPathPaint.setColor(GRID_LINE_COLOR.second);
        mPathPaint.setStrokeWidth(OUT_LINE_WIDTH / 2);
        for (int i = 1; i < Ny; i++) {
            float y = chartRect.bottom + (-i * zoneHeight) - OUT_LINE_WIDTH / 2;
            canvas.drawLine(chartRect.left, -i * zoneHeight, chartRect.right, y, mPathPaint);
        }
        for (int j = 1; j < mXAxis; j++) {
            float x = chartRect.left + j * zoneWidth + OUT_LINE_WIDTH / 2;
            canvas.drawLine(x, chartRect.bottom, x, chartRect.top, mPathPaint);
        }
    }

    private int getYScale() {
        int N = maxYAxis > 5 ? maxYAxis % 5 : 0;
        return 5 + N - 1;
    }


    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDisplayMetrics);
    }

    public ColorPair getPathColorPair(int type) {
        for (int i = 0; i < COLOR_PAIRS.length; i++) {
            if (COLOR_PAIRS[i].type == type) {
                return COLOR_PAIRS[i];
            }
        }
        return COLOR_PAIRS[0];
    }

    public void setColorLevel(int colorLevel) {
        this.colorLevel = colorLevel;
    }


    public static class ColorPair {

        public static final int HIGH = 1;
        public static final int MIDDLE = 2;
        public static final int NORMAL = 3;
        private int fillColor;
        private int lineColor;
        private int type;

        public static ColorPair create(int type, int lineColor, int fillColor) {
            ColorPair colorPair = new ColorPair();
            colorPair.type = type;
            colorPair.lineColor = lineColor;
            colorPair.fillColor = fillColor;
            return colorPair;
        }
    }

    private boolean addPoint(float x, float y, boolean isTransition) {
        if (mPathPoints.size() > MAX_VIEW_POINT_SIZE) {
            return false;
        }
        Point p = new Point(x, y);
        p.isTransition = isTransition;
        mPathPoints.add(p);
        return true;
    }

    private void addFirstPoint(float x, float y) {
        if (!mPathPoints.isEmpty()) return;
        addPoint(x, y, true);
    }

    public static class Point {
        float x;
        float y;
        boolean isTransition;

        public Point(float x, float y) {
            this.x = x;
            this.y = y;
        }
    }

}

三、使用方式

 public void startChartView(final LineChartView chartView){
        executors.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                int p = (int) (20 + 70*Math.random());
                chartView.addPercent(p);
            }
        },1000,100, TimeUnit.MILLISECONDS);
    }

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部