Android 自定义坐标曲线图

简介: 自定义View,使用paint、point、path,画点、点与点连接成线、然后闭合起来就是一个多边形,画坐标,实现坐标曲线图

先看效果
image.png
项目开发中,被安排去调研实现 坐标曲线图,网上第三方的库很多,可以实现,但是有些样式无法做到符合自己要求,Android 与iOS效果上也存在差异,所以自己自定义了一个;

其实比较简单,就是画点,画线,画虚线,画曲线,添加点击事件即可;这里面需要涉及到的知识点主要是有:对自定义View有一点基础,比如onMeasure()、onLayout()、onDraw();至少得了解这三个方法;

另外就是需要会用画笔Paint、点Point、路径Path等,至少会使用这三个API,那基本就没有问题了,画点、点与点连接成线、然后闭合起来就是一个多边形、再给多边形填充颜色即可;

另外横坐标纵坐标,以及点的数据,都是外部传入,具体情况具体考虑;

先看使用方法:

在布局中添加

<com.test.jsontouijson.weight.LineGraphicView
        android:id="@+id/lineGraphicView"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

在activity或fragment中初始化,添加数据

lineGraphicView = findViewById(R.id.lineGraphicView); 
lineGraphicView.setData(pointData);
//添加点击事件
lineGraphicView.setListener((x, y) -> initPopupWindow(lineGraphicView, x, y));

点击后的弹框是外部实现的,这个自行使用popupWindow去实现,点击已经有返回点的坐标x、y了,弹框的显示位置可通过这个坐标点定位。

大概就是这些,下面是LineGraphicView具体代码,代码有比较详细的备注形式


import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LineGraphicView extends View {
    private static final int CIRCLE_SIZE = 40;

    private static enum LineStyle {LINE, CURVE}

    private static enum YLineStyle {DASHES_LINE, FULL_LINE}

    private static enum ShaderOrientationStyle {ORIENTATION_H, ORIENTATION_V}

    private Context mContext;
    private Resources res;
    private DisplayMetrics dm;
    private OnClickListener listener;
    private LineStyle mStyle = LineStyle.LINE;
    private YLineStyle mYLineStyle = YLineStyle.DASHES_LINE;
    private ShaderOrientationStyle mShaderOrientationStyle = ShaderOrientationStyle.ORIENTATION_V;
    private int canvasHeight;
    private int canvasWidth;
    private int bHeight = 0;
    private int bWidth = 0;
    private int marginLeft;
    private boolean isMeasure = true;
    private boolean isShowFirstXContent = false;
    private int xTextWidth = 0;//Y轴内容宽度
    private int spacingHeight;
    private double averageValue;
    private int marginTop = 0;
    private int marginBottom = 0;
    /**
     * data
     */
    private Point[] mPoints;//点
    private List<String> yRawDatas;//y轴数据
    private PointData pointData;//外部传入数据
    private List<String> xRawDatas;//x轴数据
    private List<Double> dataList = new ArrayList<>();//点的数据
    private List<Integer> xList = new ArrayList<>();// 记录每个x的值
    private Map<String, Integer> xMap = new HashMap<>();//用于保存 点-X坐标 对应起来
    /**
     * paint color
     */
    private int xTextPaintColor;
    private int yTextPaintColor;
    private int startShaderColor;
    private int endShaderColor;
    private int mCanvasColor;
    /**
     * paint size
     */
    private int xTextSize = 12;
    private int yTextSize = 12;
    private Point mSelPoint;


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

    public LineGraphicView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
        initView();
    }

    private void initView() {
        this.res = mContext.getResources();
        xTextPaintColor = res.getColor(R.color.black);
        yTextPaintColor = res.getColor(R.color.black);
        startShaderColor = res.getColor(R.color.colorYellow);
        endShaderColor = res.getColor(R.color.colorYellow1);
        mCanvasColor = res.getColor(R.color.colorGray1);
        dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);
    }

    public void setData(PointData pointData) {
        this.pointData = pointData;
        this.averageValue = pointData.getyAverageValue();
        this.xRawDatas = pointData.getxAxis();
        this.yRawDatas = pointData.getyAxis();
        for (int i = 0; i < pointData.getPointInfo().size(); i++) {
            dataList.add(pointData.getPointInfo().get(i).getPrice());
        }
        if (null != dataList) {
            this.mPoints = new Point[dataList.size()];
        }
        if (null != yRawDatas) {
            this.spacingHeight = yRawDatas.size();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if (isMeasure) {
            marginLeft = dip2px(30);
            marginTop = dip2px(30);
            marginBottom = dip2px(80);
            this.canvasHeight = getHeight();//获取画布高度
            this.canvasWidth = getWidth();//获取画布宽度
            if (bHeight == 0) {
                bHeight = getHeight() - marginBottom;//实际坐标图高度
            }
            if (bWidth == 0) {
                bWidth = canvasWidth - marginLeft;//实际坐标图宽度
            }
            isMeasure = false;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //draw X line
        drawAllXLine(canvas);
        if (YLineStyle.DASHES_LINE == mYLineStyle) {
            drawPathYDashesLine(canvas);//draw Y dashes line
        } else {
            drawAllYLine(canvas);// draw Y ine)
        }
        // point init
        mPoints = getPoints();
        //draw cure line
        drawCurve(canvas);
        //draw Polygon bg color
        drawPolygonBgColor(canvas);
        // is click point
        if (null == mSelPoint) {
            drawDot(canvas);// draw dot
        } else {
            clickUpdateDot(canvas);// update dot after click
        }
        canvas.drawColor(mCanvasColor);//canvas color
    }

    private void drawCurve(Canvas c) {
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(res.getColor(R.color.colorPrimaryDark));
        p.setStrokeWidth(dip2px(0.5f));
        p.setStyle(Paint.Style.STROKE);
        if (mStyle == LineStyle.CURVE) {
            drawScrollLine(c, p);
        } else {
            drawLine(c, p);
        }
    }

//画点
    private void drawDot(Canvas c) {
        if (null == mPoints || mPoints.length == 0) {
            return;
        }
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setStyle(Paint.Style.FILL);
        for (Point point : mPoints) {
            p.setColor(res.getColor(R.color.colorYellow2));
            c.drawCircle(point.x, point.y, CIRCLE_SIZE / 2, p);
            p.setColor(res.getColor(R.color.colorYellow3));
            c.drawCircle(point.x, point.y, CIRCLE_SIZE / 3, p);
        }
    }

//更新点
    private void clickUpdateDot(Canvas c) {
        if (null == mPoints || mPoints.length == 0) {
            return;
        }
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setStyle(Paint.Style.FILL);
        for (Point point : mPoints) {
            if (null != mSelPoint && mSelPoint.x == point.x && mSelPoint.y == point.y) {
                p.setColor(res.getColor(R.color.colorPrimary));
                c.drawCircle(point.x, point.y, CIRCLE_SIZE, p);
                p.setColor(res.getColor(R.color.colorYellow3));
                c.drawCircle(point.x, point.y, (float) (CIRCLE_SIZE / 1.5), p);
            } else {
                p.setColor(res.getColor(R.color.colorYellow2));
                c.drawCircle(point.x, point.y, CIRCLE_SIZE / 2, p);
                p.setColor(res.getColor(R.color.colorYellow3));
                c.drawCircle(point.x, point.y, CIRCLE_SIZE / 3, p);
            }
        }
    }

//填充曲线闭合起来的图
    private void drawPolygonBgColor(Canvas c) {
        if (null == mPoints || mPoints.length == 0) {
            return;
        }
        Path p = new Path();
        float startX = 0;
        float endX = 0;
        int endPoint = mPoints.length - 1;
        for (int i = 0; i < mPoints.length; i++) {
            if (i == 0) {
                startX = mPoints[i].x;
                p.moveTo(mPoints[i].x, 0);
                p.lineTo(mPoints[i].x, mPoints[i].y);
            } else {
                p.lineTo(mPoints[i].x, mPoints[i].y);
                if (i == endPoint) {
                    endX = mPoints[i].x;
                }
            }
        }
        p.lineTo(endX, bHeight + marginTop);
        p.lineTo(startX, bHeight + marginTop);
        p.close();
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        Shader shader = null;
        if (mShaderOrientationStyle == ShaderOrientationStyle.ORIENTATION_H) {
            shader = new LinearGradient(endX, bHeight + marginTop, startX, bHeight + marginTop,
                    startShaderColor, endShaderColor, Shader.TileMode.REPEAT);
        } else {
            Point point = getYBiggestPoint();
            if (null != point) {
                shader = new LinearGradient(point.x, point.y, endX, bHeight + marginTop,
                        startShaderColor, endShaderColor, Shader.TileMode.REPEAT);
            }
        }
        paint.setShader(shader);
        c.drawPath(p, paint);
    }

//获取Y坐标最高的点
    private Point getYBiggestPoint() {
        Point p = null;
        if (null != mPoints && mPoints.length > 0) {
            p = mPoints[0];
            for (int i = 0; i < mPoints.length - 1; i++) {
                if (p.y > mPoints[i + 1].y) {
                    p = mPoints[i + 1];
                }
            }
        }
        return p;
    }

//画Y方向虚线
    private void drawPathYDashesLine(Canvas canvas) {
        if (null == xRawDatas || xRawDatas.size() == 0) {
            return;
        }
        Path path = new Path();
        int dashLength = 16;
        int blankLength = 16;
        Paint p = new Paint();
        p.setStyle(Paint.Style.STROKE);
        p.setStrokeWidth(4);
        p.setColor(res.getColor(R.color.colorGray));
        p.setPathEffect(new DashPathEffect(new float[]{dashLength, blankLength}, 0));
        if (!isShowFirstXContent && null == mSelPoint) {
            xRawDatas.add(0, "");
        }
        for (int i = 0; i < xRawDatas.size(); i++) {
            drawTextY(xRawDatas.get(i), (getMarginWidth() + getBWidth() / xRawDatas.size() * i) - dip2px(8), bHeight + marginTop + dip2px(26),
                    canvas);
            if (null != xMap) {
                xMap.put(xRawDatas.get(i), getMarginWidth() + getBWidth() / xRawDatas.size() * i);
            }
            float startX = getMarginWidth() + getBWidth() / xRawDatas.size() * i;
            float startY = marginTop;
            float endY = bHeight + marginTop;
            path.moveTo(startX, startY);
            path.lineTo(startX, endY);
            canvas.drawPath(path, p);
        }
        getPointX();
    }

    /**
     * 画所有Y方向实线
     */
    private void drawAllYLine(Canvas canvas) {
        if (null == xRawDatas || xRawDatas.size() == 0) {
            return;
        }
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(res.getColor(R.color.colorBlack));
        for (int i = 0; i < xRawDatas.size(); i++) {
            canvas.drawLine(getMarginWidth() + getBWidth() / xRawDatas.size() * i, marginTop, getMarginWidth()
                    + getBWidth() / xRawDatas.size() * i, bHeight + marginTop, p);
            drawTextY(xRawDatas.get(i), getMarginWidth() + getBWidth() / xRawDatas.size() * i - dip2px(8), bHeight + marginTop + dip2px(26),
                    canvas);
            if (null != xMap) {
                xMap.put(xRawDatas.get(i), getMarginWidth() + getBWidth() / xRawDatas.size() * i);
            }
        }
        getPointX();
    }

//获取点的X坐标
    private void getPointX() {
        if (null == xMap || xMap.size() == 0) {
            return;
        }
        if (null != pointData && pointData.getPointInfo().size() > 0) {
            for (PointData.PointInfo info : pointData.getPointInfo()) {
                for (String key : xMap.keySet()) {
                    if (key.equals(info.getMouth())) {
                        xList.add(xMap.get(key));
                    }
                }
            }
        }
    }

    /**
     * 画所有X方向的线
     */
    private void drawAllXLine(Canvas canvas) {
        if (null == yRawDatas || yRawDatas.size() == 0) {
            return;
        }
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(res.getColor(R.color.colorBlack));
        p.setStyle(Paint.Style.FILL);
        for (int i = 0; i < yRawDatas.size(); i++) {
            drawTextX(yRawDatas.get(i), marginLeft / 2,
                    bHeight - (bHeight / spacingHeight) * i + marginTop + dip2px(2), canvas);
            canvas.drawLine(getMarginWidth(), bHeight - (bHeight / spacingHeight) * i + marginTop, canvasWidth,
                    bHeight - (bHeight / spacingHeight) * i + marginTop, p);// Y坐标
        }
    }

//画圆滑的曲线
    private void drawScrollLine(Canvas canvas, Paint paint) {
        if (null == mPoints || mPoints.length == 0) {
            return;
        }
        Point startP;
        Point endP;
        for (int i = 0; i < mPoints.length - 1; i++) {
            startP = mPoints[i];
            endP = mPoints[i + 1];
            int wt = (startP.x + endP.x) / 2;
            Point p3 = new Point();
            Point p4 = new Point();
            p3.y = startP.y;
            p3.x = wt;
            p4.y = endP.y;
            p4.x = wt;
            Path path = new Path();
            path.moveTo(startP.x, startP.y);
            path.cubicTo(p3.x, p3.y, p4.x, p4.y, endP.x, endP.y);
            canvas.drawPath(path, paint);
        }
    }

//画笔直的曲线
    private void drawLine(Canvas canvas, Paint paint) {
        if (null == mPoints || mPoints.length == 0) {
            return;
        }
        Point startP;
        Point endP;
        for (int i = 0; i < mPoints.length - 1; i++) {
            startP = mPoints[i];
            endP = mPoints[i + 1];
            canvas.drawLine(startP.x, startP.y, endP.x, endP.y, paint);
        }
    }

//添加X轴方向的文字
    private void drawTextY(String text, int x, int y, Canvas canvas) {
        if (null == yRawDatas || yRawDatas.size() == 0) {
            return;
        }
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setTextSize(dip2px(yTextSize));
        p.setColor(yTextPaintColor);
        p.setTextAlign(Paint.Align.LEFT);
        canvas.drawText(text, x, y, p);
    }

//添加Y轴方向的文字
    private void drawTextX(String text, int x, int y, Canvas canvas) {
        if (null == xRawDatas || xRawDatas.size() == 0) {
            return;
        }
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setTextSize(dip2px(xTextSize));
        p.setColor(xTextPaintColor);
        p.setTextAlign(Paint.Align.LEFT);
        xTextWidth = (int) p.measureText(text);
        canvas.drawText(text, x, y, p);
    }

//获取所有曲线点
    private Point[] getPoints() {
        Point[] points = new Point[dataList.size()];
        for (int i = 0; i < dataList.size(); i++) {
            int ph = bHeight - (int) (((dataList.get(i) - pointData.getyAxisSmallValue()) / averageValue) * (bHeight / spacingHeight));
            points[i] = new Point(xList.get(i), ph + marginTop);
        }
        return points;
    }

//获取实际的左边距
    private int getMarginWidth() {
        if (xTextWidth == 0) {
            return marginLeft;
        } else {
            return xTextWidth + marginLeft;
        }
    }

//获取实际的坐标图宽度
    private int getBWidth() {
        if (xTextWidth == 0) {
            return bWidth;
        } else {
            return bWidth - xTextWidth;
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                dealClick(x, y);
                break;
        }
        return true;
    }

//添加点击事件,并更新点
    private void dealClick(int x, int y) {
        if (null != mPoints && mPoints.length > 0) {
            for (int i = 0; i < mPoints.length; i++) {
                if ((mPoints[i].x - CIRCLE_SIZE) < x && x < (mPoints[i].x + CIRCLE_SIZE) &&
                        (mPoints[i].y - CIRCLE_SIZE) < y && y < (mPoints[i].y + CIRCLE_SIZE)) {
                    mSelPoint = mPoints[i];
                    invalidate();
                    if (null != listener) {
                        listener.onClick(mPoints[i].x, mPoints[i].y);
                    }
                    Toast.makeText(mContext, "点击了第" + i + "个", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }


    public void setAverageValue(int averageValue) {
        this.averageValue = averageValue;
    }

    public void setMarginTop(int marginTop) {
        this.marginTop = marginTop;
    }

    public void setMarginBottom(int marginBottom) {
        this.marginBottom = marginBottom;
    }

    public void setMStyle(LineStyle mStyle) {
        this.mStyle = mStyle;
    }

    public void setMYLineStyle(YLineStyle style) {
        this.mYLineStyle = style;
    }

    public void setShaderOrientationStyle(ShaderOrientationStyle shaderOrientationStyle) {
        this.mShaderOrientationStyle = shaderOrientationStyle;
    }

    public void setBHeight(int bHeight) {
        this.bHeight = bHeight;
    }

    public void setXTextPaintColor(int xTextPaintColor) {
        this.xTextPaintColor = xTextPaintColor;
    }


    public void setYTextPaintColor(int yTextPaintColor) {
        this.yTextPaintColor = yTextPaintColor;
    }


    public void setXTextSize(int xTextSize) {
        this.xTextSize = xTextSize;
    }

    public void setYTextSize(int yTextSize) {
        this.yTextSize = yTextSize;
    }

    public void setShaderColor(int startColor, int endColor) {
        this.startShaderColor = startColor;
        this.endShaderColor = endColor;
    }

    public void setIsShowFirstXContent(boolean isShowFirstXContent) {
        this.isShowFirstXContent = isShowFirstXContent;
    }

    /**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */
    private int dip2px(float dpValue) {
        return (int) (dpValue * dm.density + 0.5f);
    }

    public interface OnClickListener {
        void onClick(int x, int y);
    }

    public void setListener(OnClickListener listener) {
        this.listener = listener;
    }

}

that's all

相关文章
|
15天前
|
供应链 物联网 区块链
未来触手可及:探索新兴技术的趋势与应用安卓开发中的自定义视图:从基础到进阶
【8月更文挑战第30天】随着科技的飞速发展,新兴技术如区块链、物联网和虚拟现实正在重塑我们的世界。本文将深入探讨这些技术的发展趋势和应用场景,带你领略未来的可能性。
|
17天前
|
测试技术 Android开发 Python
探索软件测试的艺术:从基础到高级安卓应用开发中的自定义视图
【8月更文挑战第29天】在软件开发的世界中,测试是不可或缺的一环。它如同艺术一般,需要精细的技巧和深厚的知识。本文旨在通过浅显易懂的语言,引领读者从软件测试的基础出发,逐步深入到更复杂的测试策略和工具的使用,最终达到能够独立进行高效测试的水平。我们将一起探索如何通过不同的测试方法来确保软件的质量和性能,就像艺术家通过不同的色彩和笔触来完成一幅画作一样。
|
4天前
|
缓存 搜索推荐 Android开发
安卓应用开发中的自定义View组件实践
【9月更文挑战第10天】在安卓开发领域,自定义View是提升用户体验和实现界面个性化的重要手段。本文将通过一个实际案例,展示如何在安卓项目中创建和使用自定义View组件,包括设计思路、实现步骤以及可能遇到的问题和解决方案。文章不仅提供了代码示例,还深入探讨了自定义View的性能优化技巧,旨在帮助开发者更好地掌握这一技能。
|
15天前
|
XML 搜索推荐 Android开发
安卓开发中的自定义View组件实践
【8月更文挑战第30天】探索Android世界,自定义View是提升应用界面的关键。本文以简洁的语言带你了解如何创建自定义View,从基础到高级技巧,一步步打造个性化的UI组件。
|
7天前
|
前端开发 搜索推荐 Android开发
探索安卓开发中的自定义视图##
【9月更文挑战第6天】 在安卓应用开发的世界里,自定义视图如同绘画艺术中的色彩,它们为界面设计增添了无限可能。通过掌握自定义视图的绘制技巧,开发者能够创造出既符合品牌形象又提升用户体验的独特界面元素。本文将深入浅出地介绍如何从零开始构建一个自定义视图,包括基础框架搭建、关键绘图方法实现、事件处理机制以及性能优化策略。准备好让你的安卓应用与众不同了吗?让我们开始吧! ##
|
Android开发
Android Studio 自定义设置注释模板
Android Studio 自定义设置注释模板
448 0
Android Studio 自定义设置注释模板
|
4天前
|
IDE 开发工具 Android开发
安卓与iOS开发对比:平台选择对项目成功的影响
【9月更文挑战第10天】在移动应用开发的世界中,选择正确的平台是至关重要的。本文将深入探讨安卓和iOS这两大主要移动操作系统的开发环境,通过比较它们的市场份额、开发工具、编程语言和用户群体等方面,为开发者提供一个清晰的指南。我们将分析这两个平台的优势和劣势,并讨论如何根据项目需求和目标受众来做出最佳选择。无论你是初学者还是有经验的开发者,这篇文章都将帮助你更好地理解每个平台的特性,并指导你做出明智的决策。
|
2天前
|
API Android开发 iOS开发
安卓与iOS开发中的线程管理对比
【9月更文挑战第12天】在移动应用的世界中,安卓和iOS平台各自拥有庞大的用户群体。开发者们在这两个平台上构建应用时,线程管理是他们必须面对的关键挑战之一。本文将深入探讨两大平台在线程管理方面的异同,通过直观的代码示例,揭示它们各自的设计理念和实现方式,帮助读者更好地理解如何在安卓与iOS开发中高效地处理多线程任务。
|
4天前
|
开发框架 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的指南
在移动应用开发的广阔天地中,安卓与iOS两大平台各占半壁江山。本文将深入浅出地对比这两大操作系统的开发环境、工具和用户体验设计,揭示它们在编程语言、开发工具以及市场定位上的根本差异。我们将从开发者的视角出发,逐步剖析如何根据项目需求和目标受众选择适合的平台,同时探讨跨平台开发框架的利与弊,为那些立志于打造下一个热门应用的开发者提供一份实用的指南。
17 5
|
4天前
|
开发工具 Android开发 iOS开发
安卓与iOS开发:平台选择的艺术与科学
在移动应用开发的广阔天地中,安卓与iOS两大平台如同东西方哲学的碰撞,既有共通之处又各具特色。本文将深入探讨这两个平台的设计理念、开发工具和市场定位,旨在为开发者提供一份简明扼要的指南,帮助他们在这场技术与商业的博弈中找到自己的道路。通过比较分析,我们将揭示每个平台的优势与局限,以及它们如何影响应用的性能、用户体验和市场接受度。无论你是初涉江湖的新手,还是经验丰富的老手,这篇文章都将为你的选择提供新的视角和思考。
16 5