Android实现自定义的折线图MyChartView

老规矩,先上实现的效果图


github地址

https://github.com/Alan222/MyChartView

这些基本的资源文件写了吧,以免最后忘了加

dimens文件

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="dp20">20dp</dimen>
    <dimen name="dp3">3dp</dimen>
    <dimen name="dp6">6dp</dimen>
    <dimen name="textSize">10dp</dimen>
</resources>
activity_main

<charview.com.mychartview.MyChartView
    android:id="@+id/chart"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    />

MyChartView 需求分析

需要画的东西

1.x,y轴坐标的线,箭头

2.x,y轴的标题,刻度下的字

3.折线和点,渐变背景色

分析完需求我们先不要急着去实现这些功能,先搭简单的模板;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    initData();         //画坐标轴之前我们需要先初始化一些数据    
    drawAxes(canvas);        //  画坐标轴    
    drawText(canvas);//  画文字    
    drawLine(canvas);//  画折线
}

private void initData() {

}

private void drawAxes(Canvas canvas) {

}

private void drawLine(Canvas canvas) {

}

private void drawText(Canvas canvas) {

}

好了,画完了




开个玩笑我们先来实现第一步,画线

分析一下画线需要的数据

   1.1.控件的宽高(用来确定x,y轴的宽高)

    1.2.画线必须要有画笔

    1.3.坐标起始点

    1.4.x轴两条线之间的间距

   1.5.y轴两条线之间的间距

分析完需求我们用代码去实现

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (changed) {
        //1.1.控件的宽高(用来确定x,y轴的宽高)
        mWidth = getWidth();
        mHeight = getHeight();
    }
}
private void initData() {
    //1.2.初始化画笔    
    mPaint = new Paint();
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setAntiAlias(true);//去锯齿    
    mPaint.setColor(mPaintColor);//颜色    
    mPaint.setTextSize(mTextSize);    
    //1.3.坐标起始点    
    xPoint = (int) getContext().getResources().getDimension(R.dimen.dp20);
    yTopPoint = (int) getContext().getResources().getDimension(R.dimen.dp20);
    yPoint = mHeight - yTopPoint;  
    
    //1.4.x轴两条线之间的间距    
    //1.5.y轴两条线之间的间距    
    dp3 = (int) getContext().getResources().getDimension(R.dimen.dp3);  //箭头的偏移量    
    dp6 = (int) getContext().getResources().getDimension(R.dimen.dp6);  //箭头的偏移量    
    yItemDistance = (yPoint - (yTopPoint + mTextSize)) / 5;//y轴字段坐标的间距 5代表除起始坐标外有5条线,暂时先死,稍后会根据y轴的数据来    
    xItemDistance = (mWidth - xPoint) / 7; //x轴字段坐标的间距 7代表除起始坐标外有7条线,暂时先死,稍后会根据y轴的数据来
}

有了这些数据我们就可以开始画线了

private void drawAxes(Canvas canvas) {
    //画起始坐标轴和箭头    
    canvas.drawLine(xPoint, yPoint, mWidth - xPoint, yPoint, mPaint);   //x轴起始坐标线    
    canvas.drawLine(mWidth - xPoint, yPoint, mWidth - xPoint - dp6, yPoint + dp3, mPaint);  //向右箭头    
    canvas.drawLine(mWidth - xPoint, yPoint, mWidth - xPoint - dp6, yPoint - dp3, mPaint);
    canvas.drawLine(xPoint, yPoint, xPoint, yTopPoint, mPaint);   //y起始轴线    
    canvas.drawLine(xPoint, yTopPoint, xPoint - dp3, yTopPoint + dp6, mPaint);  //向右箭头    
    canvas.drawLine(xPoint, yTopPoint, xPoint + dp3, yTopPoint + dp6, mPaint);    //画横着的线    
    int yTextPoint; //y轴字段的坐标    
    for (int i = 0; i < 5; i++) {
        yTextPoint = yTopPoint + mTextSize + yItemDistance * i;
        canvas.drawLine(xPoint, yTextPoint, mWidth - xPoint, yTextPoint, mPaint);   //x轴线    
    }

    //画竖着的线    
    int xTextPoint; //x轴字段的坐标    
    for (int i = 1; i < 7; i++) {
        xTextPoint = xPoint + xItemDistance * i;
        canvas.drawLine(xTextPoint, yPoint, xTextPoint, yTopPoint +
                mTextSize, mPaint);   //y轴线这里的线比起始Y轴短,留出位置画title    
    }
}

画完背景线,我们先测试一下

附上成员变量

PS:有值的成员变量暂时先写死,实现需求以后用attr实现

private int mTextSize = (int) getResources().getDimension(R.dimen.textSize);
private int mPaintColor = Color.RED;
private int mWidth;//控件的宽
private int mHeight;//控件的高
private Paint mPaint;
private int xPoint; //原点X轴坐标
private int yPoint; //原点Y轴坐标
private int yTopPoint;//y轴顶点坐标
private int dp3;
private int dp6;
private int yItemDistance;
private int xItemDistance;

ok,接下来开始画x,y轴的字段和标题

我们需要
2.1.x轴的数据
2.2.y轴的数据
2.3.x轴的title
2.4.y轴的title
2.5.数据的宽和高


分析完继续撸代码

private void initData() {
    //2.1.x轴的数据
    //2.2.y轴的数据    // FIXME: 2017/4/17  暂时先写死    
    xData = new ArrayList<>();
    xData.add("4-11");
    xData.add("4-12");
    xData.add("4-13");
    xData.add("4-14");
    xData.add("4-15");
    xData.add("4-16");
    xData.add("4-17");
    yData = new ArrayList<>();
    yData.add(0);
    yData.add(10);
    yData.add(20);
    yData.add(30);
    yData.add(40);
    yData.add(50);
    yData.add(60);

    mPaint = new Paint();
    ......
}

//2.3.x轴的title
public void setxTitle(String xTitle) {
    this.xTitle = xTitle;
}
//2.4.y轴的title
public void setyTitle(String yTitle) {
    this.yTitle = yTitle;
}

//2.5.测量文字的宽,文字的高为textsize
public int measureTextWidth(String text) {
    return (int) mPaint.measureText(text);
}

这里写个成员变量吧 不然代码要来亲戚了

private String xTitle = "近七日";
private String yTitle = "增长率/%";
private ArrayList<String> xData = new ArrayList<>();
private ArrayList<Integer> yData = new ArrayList<>();

做完准备工作可以开始画刻度了

private void drawText(Canvas canvas) {
    //x轴的title    
    int xTitleWidth = measureTextWidth(xTitle);
    canvas.drawText(xTitle, mWidth - xPoint - xTitleWidth / 2, yPoint + dp3 + mTextSize, mPaint);    //y轴的title    
    canvas.drawText(yTitle, xPoint + dp6, yTopPoint + mTextSize / 2, mPaint);    //x轴刻度    
    for (int i = 0; i < xData.size(); i++) {
        int dataWidth = measureTextWidth(xData.get(i));
        canvas.drawText(xData.get(i), xPoint + xItemDistance * i - dataWidth / 2, yPoint + dp3 + mTextSize, mPaint);
    }

    //y轴刻度    
    for (int i = 0; i < yData.size(); i++) {
        int dataWidth = measureTextWidth(yData.get(i) + "");
        canvas.drawText(yData.get(i) + "", xPoint - dataWidth - dp3, yPoint - yItemDistance * (i + 1), mPaint);
    }
}

这里有了xDatayData我们可以顺手把写死的线的条数

private void initData() {
    .......
    dp3 = (int) getContext().getResources().getDimension(R.dimen.dp3);  //箭头的偏移量    
    dp6 = (int) getContext().getResources().getDimension(R.dimen.dp6);  //箭头的偏移量    
    yItemDistance = (yPoint - (yTopPoint + mTextSize)) / yData.size();
    xItemDistance = (mWidth - xPoint - xPoint) / xData.size();
}
private void drawAxes(Canvas canvas) {
    ......
    //画横着的线    
    int yTextPoint; //y轴字段的坐标    
    for (int i = 0; i < yData.size(); i++) {
        yTextPoint = yTopPoint + mTextSize + yItemDistance * i;
        canvas.drawLine(xPoint, yTextPoint, mWidth - xPoint, yTextPoint, mPaint);   //x轴线    
    }    //画竖着的线    
    int xTextPoint; //x轴字段的坐标    
    for (int i = 1; i < xData.size(); i++) {
        xTextPoint = xPoint + xItemDistance * i;
        canvas.drawLine(xTextPoint, yPoint, xTextPoint, yTopPoint + mTextSize, mPaint);   //y轴线这里的线比起始Y轴短,留出位置画title    
    }
}

画完背景我们实现第三个需求

 

3.折线和点,渐变背景色

    分析折线和点

    3.1.画折线和折点需要的笔

    3.2.各个点的的数据

    3.3.连接数据的path

分析渐变背景色

    3.4.画背景的笔

    3.5.画背景的路线

private void initData() {
    ......
    //3.1.画折线和折点需要的笔   //折线画笔 
    mLinePaint = new Paint();
    mLinePaint.setStyle(Paint.Style.STROKE);
    mLinePaint.setAntiAlias(true);//去锯齿    mLinePaint.setColor(mLinePaintColor);//颜色  
    mLinePaint.setTextSize(mTextSize);    //画折点的笔  
    mCirclePaint = new Paint();
    mCirclePaint.setStyle(Paint.Style.FILL);
    mCirclePaint.setAntiAlias(true);//去锯齿  mCirclePaint.setColor(mLinePaintColor);//颜色    
    // 3.4.画背景的笔   
    mShawerPaint = new Paint();
    mShawerPaint.setAntiAlias(true);
    mShawerPaint.setStrokeWidth(2f);
    //3.3.连接数据的path    
    mPath = new Path();
    //3.5.连接数据的path
    mShowerPath = new Path();
    //背景的渐变色   
    shadeColors = new int[]{
            Color.argb(80, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
            Color.argb(20, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
            Color.argb(0, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor))};
}

private void initData() {
    //3.2.各个点的数据
    // FIXME: 2017/4/17    
    itemData.add(15);
    itemData.add(18);
    itemData.add(11);
    itemData.add(25);
    itemData.add(35);
    itemData.add(45);
    itemData.add(0);        
    // FIXME: 2017/4/17    
    xData.add("4-11");
    ......

}

准备完毕可以开始画线了

分析折点的坐标

x点的坐标就是x起点的坐标+x线之间的间距

x = xPoint + xItemDistance * i;

y轴的坐标则需要按比例计算

折点的值/y刻度的范围 = y顶点到y坐标的距离/y刻度件的长度

y坐标 = (yPoint - yItemDistance-((integer * 1.0-yMin) / itemLength) * yLength));

y刻度的范围我们需要yMax,yMin;

 

画线的成员变量也就基本确定了

private ArrayList<Integer> itemData = new ArrayList<>();
private Paint mShawerPaint;
private Paint mCirclePaint;
private Paint mLinePaint;
private Path mPath;
private int mLinePaintColor = Color.RED;
// FIXME: 2017/4/17
private Path mShowerPath;
private int[] shadeColors;
private int yMax;
private int yMin;

private void initData() {
    ......
    shadeColors = new int[]{
            Color.argb(80, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
            Color.argb(20, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
            Color.argb(0, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor))};
    if (yData.size() > 0) {
        yMin = yData.get(0);
        yMax = yData.get(0);
    }
    for (int i = 0; i < yData.size(); i++) {
        if (yData.get(i) > yMax) {
            yMax = yData.get(i);
        } else if (yData.get(i) < yMin) {
            yMin = yData.get(i);
        }
    }
}

做完准备工作就开画吧

private void drawLine(Canvas canvas) {
    for (int i = 0; i < itemData.size(); i++) {
        int integer = itemData.get(i);        //折点x坐标        
        int itemX = xPoint + xItemDistance * i;        //折点y坐标        
        int itemLength = yMax - yMin;
        int yLength = yPoint - (yTopPoint + mTextSize) - yItemDistance;
        int itemY = 0;
        if (itemLength != 0) {
            itemY = (int) (yPoint - yItemDistance - ((integer * 1.0 - yMin) / itemLength) * yLength);
        }

        //画折点        
        canvas.drawCircle((xPoint + xItemDistance * i), itemY, 3, mCirclePaint);//画小圆点        
        // 连接折线的路径,阴影的路径        
        if (i == 0) {
            mPath.moveTo(itemX, itemY);
            mShowerPath.moveTo(itemX, itemY);
        } else {
            mPath.lineTo(itemX, itemY);
            mShowerPath.lineTo(itemX, itemY);
            if (i == itemData.size() - 1) {
                mShowerPath.lineTo(itemX, yPoint - yItemDistance);
                mShowerPath.lineTo(xPoint, yPoint - yItemDistance);
                mShowerPath.close();
            }
        }
    }
}


写到这里我们基本的ui图已经都画出来了,但是我们前面的代码就标注了,还有些写死的数据需要改,而且我们的是动态刻度图,自然y轴的刻度也不会让亲们自己去写

我们这里先改简单的attr数据,这里自定义的attr数据有textSize,mPaintColor,mLinePaintColor,如果有其他需求,可以自行添加。


values下创建attrs文件夹,自定义名字和属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyChartView">
        <attr name="paintColor" format="color"/><!--坐标轴和刻度的颜色-->
        <attr name="textSize" format="dimension"/><!--字体的大小-->
        <attr name="chartLineColor" format="color"/><!--折线和阴影的颜色-->
    </declare-styleable>
</resources>

定义完后,我们在view中读取

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

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

public MyChartView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    float textSize = getContext().getResources().getDimension(R.dimen.textSize);
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyChartView);
    mPaintColor = typedArray.getColor(R.styleable.MyChartView_paintColor,Color.RED);
    mLineColor = typedArray.getColor(R.styleable.MyChartView_chartLineColor, Color.RED);
    mTextSize = (int) typedArray.getDimension(R.styleable.MyChartView_textSize, textSize);
    typedArray.recycle();
}

我们再来分析一下y轴的刻度;

y轴的刻度如果偏差加大,那刻度的差值也要随之改变,而且可能出现负数,显然y轴刻度不能写死,让用户自己设置也会让控件使用变得麻烦so我们这里在设置itemData的时候也动态修改yData的值。

通过分析这里我把最低刻度和最高刻度都设为10的整数倍,中间值则有折点值来决定

public void setItemData(@NonNull ArrayList<Integer> itemData) {
    this.itemData = itemData;
    if (itemData.size() == 0) {
        return;
    }

    itemMin = itemData.get(0);  //折点的最小值    
    itemMax = itemData.get(0);  //折点的最大值    
    for (int i = 0; i < itemData.size(); i++) {
        if (itemData.get(i) > itemMax) {
            itemMax = itemData.get(i);
        } else if (itemData.get(i) < itemMin) {
            itemMin = itemData.get(i);
        }
    }
    //设置y轴刻度的最小值为比itemMin小的 最大的10的整数倍    
    int yMin = itemMin / 10 * 10;
    if (yMin < 0) {
        yMin = yMin - 10;
    }
    //设置y轴刻度的最大值为比itemMax大的 最小的10的整数倍    
    int yMax = (itemMax / 10) * 10 + 10;
    if (itemMax % 10 == 0) {
        yMax = itemMax / 10 * 10;
    }
    yData.clear();        //最大值,最小值都是10的倍数,那么我们的中间刻度也好取了,    
    // 为了刻度美观,我们这里如果yMax-yMin≤50,刻度件的差值就≤10,如果>50,就取10的整数倍       
    for (int i = 0; i <= 5; i++) {
        if ((yMax - yMin) / 5 <= 10) {
            yData.add(yMin + (yMax - yMin) / 5 * i);
        } else {
            yData.add(yMin + ((yMax - yMin) / 50 + 1) * 10 * i);
        }
    }

    //动态设置x轴的data    
    Date date = new Date();
    long time = date.getTime();
    SimpleDateFormat dateFormat = new SimpleDateFormat("M-d");
    xData.clear();
    for (long i = itemData.size(); i > 0; i--) {
        long xDataTime = time - 24 * 60 * 60 * 1000 * i;
        xData.add(dateFormat.format(xDataTime));
    }
}

写到这里自定义的ChartView就实现的差不多了,我们可以删掉上面写死的数据,如果需要自己设置xData和yData添加如下代码

 

//注意在代码中调用这两个方法的话都要在调用setItemData()后面调用,否则无效。

public void setyData(ArrayList<String> xData) {
    this.yData = yData;
}

public void setxData(ArrayList<String> xData) {
    this.xData = xData;
    if (xData.size() != itemData.size()) {
        try {
            throw new Exception("XData Count Unmatched Exception");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


自定义的代码就写到这里了。

---------------------------------华丽的分割线-------------------------------------

这里说两句题外话

一开始拿到这种需求图我是懵逼的,对于阴影背景的效果实现,我原本想的画渐变的shape当背景色,然后想circleImageView那样擦掉折线上面的颜色使其透明

刚好写需求的当天看郭神的微信公众号刚好出了一篇自定义的View之颜色渐变折线图。



这简直是为我量身打造的有木有 ,老夫敲代码数十载,拿起键盘就是一顿ctrl+cctrl+v,瞬间阴影的效果就有了。

郭神千秋万代,一桶浆糊。

郭神链接

完事后看了关于shader的博客,感觉还是挺强大的,有兴趣的小伙伴可以去看看

Shader链接


  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值