学堂 学堂 学堂公众号手机端

Android 项目(一):自定义View绘制“折线图”

lewis 1年前 (2024-04-27) 阅读数 13 #技术




  都说不要造轮子不要造轮子,可是我还是在不停的造轮子啊!!!闲话不多说了看看上面折线图的效果(Ps:当然了如果有现成的例子或者是开源的东西最好还是参考别人的或者是开源的,毕竟自己自定义view好看是好看但是太耗费时间很多东西需要自己去慢慢调试)。

  如果你有了一定的android基础,建议在绘制上面的折线图之前还是仔细观察一下,有了自己的思路再来看下面的博客。

  通过上面的效果图可以看出画布的底层是白色的背景,之后绘制了一个灰色的矩形区域+绘制网格背景(说白了就是画线),然后就是折线图的具体绘制了,关于折线图的具体绘制从效果上看是先绘制的线再绘制的两层的小圆点(一个底部蓝色的圆点+一个表层的白色圆点)然后就是折线所围区域的阴影部分;这里简单说一下折线就是通过画线绘制的,圆点画圆,阴影部分通过path路径绘制(给画笔透明度);绘制的时候为了确定折线的绘制路线可以先绘制圆点再绘制折线,最后绘制阴影

  好了,上面就是绘制折线图的大体思路了

一、创建自定义view

这里就是先创建一个class来继承View,然后再在canvas画布上进行绘制,先不要管怎么绘制,具体绘制下面会给出。
ps:
1、代码​​​private List<BrokenLineCusVisit> mdata;​​​中自己创建的一个类,为了添加数据方便,如果不是为了以后整个项目考虑,可以自己在该class中随意添加数据,之后在MainActivity中就不需要再添加数据了。
2、有使用到heigh的地方需要在onMeasure方法中使用,不然会是0;比如gridspace_heigh(每个网格的高度)

/**
* Created by Administrator on 2015/10/13.
*/
public class BrokenLineCusVisitView extends View
private int width;
private int heigh;

//网格的宽度与高度
private int gridspace_width;
private int gridspace_heigh;
//底部空白的高度
private int brokenline_bottom;


//灰色背景的画笔
private Paint mPaint_bg;
//灰色网格的画笔
private Paint mPaint_gridline;
//文本数据的画笔
private Paint mPaint_text;

//折线圆点的蓝色背景
private Paint mPaint_point_bg;
//折线圆点的白色表面
private Paint mPaint_point_sur;
//阴影路径的画笔
private Paint mPaint_path;
//折线的画笔
private Paint mPaint_brokenline;
//路径
private Path mpath=new Path();
//客户拜访的折线(BrokenLineCusVisit)数据
private List<BrokenLineCusVisit> mdata;

public BrokenLineCusVisitView(Context context) {
super(context);
}

public BrokenLineCusVisitView(Context context, AttributeSet attrs) {
super(context, attrs);
inite(context);
}

private void inite(Context context) {

mPaint_bg=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_bg.setColor(Color.argb(0xff,0xef,0xef,0xef));

mPaint_gridline=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_gridline.setColor(Color.argb(0xff,0xce,0xCB,0xce));

mPaint_brokenline=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_brokenline.setColor(Color.argb(0xff,0x91,0xC8,0xD6));
mPaint_brokenline.setTextSize(18);
mPaint_brokenline.setTextAlign(Paint.Align.CENTER);

mPaint_point_bg=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_point_bg.setColor(Color.argb(0xff, 0x91, 0xC8, 0xD6));
//注意path的画笔的透明度已经改变了
mPaint_path=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_path.setColor(Color.argb(0x33,0x91,0xC8,0xD6));


mPaint_point_sur=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_point_sur.setColor(Color.WHITE);


mPaint_text=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_text.setColor(Color.BLACK);
mPaint_text.setTextAlign(Paint.Align.CENTER);

invalidate();
}
//data的set/get方法,用于设置数据
public List<BrokenLineCusVisit> getMdata() {
return mdata;
}

public void setMdata(List<BrokenLineCusVisit> mdata) {
this.mdata = mdata;
requestLayout();
invalidate();
}

@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);
//绘制白色背景
canvas.drawColor(Color.WHITE);
//绘制灰色矩形区域
canvas.drawRect(10,0,width,heigh-brokenline_bottom,mPaint_bg);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
gridspace_width= 50;
if(mdata.size()==0){
width=getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
}
else{
//根据数据的条数设置宽度
width=gridspace_width*mdata.size()+10;
}

heigh=getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
brokenline_bottom=50;

Log.d("bottm",""+brokenline_bottom);
gridspace_heigh=(heigh-brokenline_bottom)/4;
setMeasuredDimension(width, heigh);
}
}
二、Dao模式创建数据类

  看上面的图可以发现它需要有数据,这里采用Dao模式创建客户拜访的折线类(折线BrokenLine),向其中添加数据。

Dao模式的折线类

创建类,给出属性,给出属性的set/get方法,给出构造器

package dao;

/**
* Created by Administrator on 2015/10/13.
*/
public class BrokenLineCusVisit
//拜访日期
private String date;
//拜访数量
private int data;

public BrokenLineCusVisit(String date, int data) {
this.date = date;
this.data = data;
}

public String getDate() {
return date;
}

public void setDate(String date) {
this.date = date;
}

public int getData() {
return data;
}

public void setData(int data) {
this.data = data;
}
}
Xml文件中引用自定义View(包名+类名)

将自定义的view放入HorzontalView中使自定义view能够水平滑动
ps:​​​android:scrollbars="none"​​设置水平滚动条的滚动条隐藏

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
<widget.chart.BrokenLineCusVisitView
android:id="@+id/brokenline"
android:layout_width="match_parent"
android:layout_height="match_parent"
</HorizontalScrollView>

</LinearLayout>
MainActivity中为自定义View添加数据

找到自定义view的id,通过自定义view的set/get方法设置它的data数据

public class MainActivity extends BaseActivity

private List<BrokenLineCusVisit> mdata=new ArrayList<>();
private BrokenLineCusVisitView brokenline;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

brokenline= (BrokenLineCusVisitView) findViewById(R.id.brokenline);
for (int i = 0; i <30 ; i++) {
BrokenLineCusVisit brokenline=new BrokenLineCusVisit(i+"",i+1);
mdata.add(brokenline);
}
brokenline.setMdata(mdata);
}
}
三、绘制折线图绘制背景(一、从简单开始)

白色背景+灰色矩形+网格+圆点+数据文本

@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);
//绘制折线图的白色背景
canvas.drawColor(Color.WHITE);
//绘制灰色矩形区域
canvas.drawRect(10,0,width,heigh-brokenline_bottom,mPaint_bg);

//绘制网格线,横向的;Y轴不变 X轴绘制直线
for (int j=0;j<4;j++){
canvas.drawLine(10,gridspace_heigh*(j+1),width,gridspace_heigh*(j+1),mPaint_gridline);
}


for (int i= 0; i < mdata.size();i++){

//绘制纵向网格线
canvas.drawLine(gridspace_width * i + 10, 0, gridspace_width * i + 10, heigh - brokenline_bottom, mPaint_gridline);


//绘制圆点,圆点位置根据网格线的位置确定
canvas.drawCircle(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, 10, mPaint_point_bg);
canvas.drawCircle(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, 5, mPaint_point_sur);

//绘制数据的数量
String data=mdata.get(i).getData()+"";
canvas.drawText(data,gridspace_width*i+10,heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData()/100-mPaint_brokenline.measureText(data),mPaint_brokenline);

//绘制底部空白处:数据的日期
String date=mdata.get(i).getDate();
canvas.drawText(date, gridspace_width * i + 10, heigh - brokenline_bottom / 2, mPaint_text);



}


}
绘制背景(二、循序渐进)

绘制折线+阴影
Ps:折线的绘制是根据圆点的位置来绘制的,阴影的绘制则是根据折线的路径进行绘制的,最后将路径闭合即可形成阴影了。

.onDraw(canvas);
//绘制折线图的白色背景
canvas.drawColor(Color.WHITE);
//绘制灰色矩形区域
canvas.drawRect(10,0,width,heigh-brokenline_bottom,mPaint_bg);

//绘制网格线,横向的;Y轴不变 X轴绘制直线
for (int j=0;j<4;j++){
canvas.drawLine(10,gridspace_heigh*(j+1),width,gridspace_heigh*(j+1),mPaint_gridline);
}


for (int i= 0; i < mdata.size();i++){
if (i==0){
//开始时需要将path移动到要开始绘制的位置,否则默认从(0,0)开始,绘制path路径
mpath.moveTo(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100);
}
//绘制纵向网格线
canvas.drawLine(gridspace_width * i + 10, 0, gridspace_width * i + 10, heigh - brokenline_bottom, mPaint_gridline);

if (i!=mdata.size()-1){
//根据圆点位置绘制折线
canvas.drawLine(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, gridspace_width * (i + 1) + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i + 1).getData() / 100, mPaint_brokenline);
//path的路径跟绘制的线的路径是相同的,因此path的起止点与线的起止点相同
mpath.quadTo(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, gridspace_width * (i + 1) + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i + 1).getData() / 100);
}
//绘制圆点,圆点位置根据网格线的位置确定
canvas.drawCircle(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, 10, mPaint_point_bg);
canvas.drawCircle(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, 5, mPaint_point_sur);

//绘制数据的数量
String data=mdata.get(i).getData()+"";
canvas.drawText(data,gridspace_width*i+10,heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData()/100-mPaint_brokenline.measureText(data),mPaint_brokenline);

//绘制底部空白处:数据的日期
String date=mdata.get(i).getDate();
canvas.drawText(date, gridspace_width * i + 10, heigh - brokenline_bottom / 2, mPaint_text);

if (i==mdata.size()-1){
//绘制完最后一个点,path需要沿着纵轴向下到达heigh - brokenline_bottom 的位置再水平到达(10,heigh - brokenline_bottom )的位置,最后闭合
mpath.quadTo(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100,gridspace_width * i + 10, heigh - brokenline_bottom );
mpath.quadTo(gridspace_width * i + 10, heigh - brokenline_bottom ,10, heigh - brokenline_bottom);
mpath.close();
}
}
//最终在画布上绘制path
canvas.drawPath(mpath,mPaint_path);
四、折线图完整代码
package widget.chart;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.example.danfeng.myhongquanyingxiao.Base.AppApplication;
import com.example.danfeng.myhongquanyingxiao.Base.SizeConvert;

import java.util.List;

import dao.BrokenLineCusVisit;

/**
* Created by Administrator on 2015/10/13.
*/
public class BrokenLineCusVisitView extends View
private int width;
private int heigh;

//网格的宽度与高度
private int gridspace_width;
private int gridspace_heigh;
//底部空白的高度
private int brokenline_bottom;


//灰色背景的画笔
private Paint mPaint_bg;
//灰色网格的画笔
private Paint mPaint_gridline;
//文本数据的画笔
private Paint mPaint_text;

//折线圆点的蓝色背景
private Paint mPaint_point_bg;
//折线圆点的白色表面
private Paint mPaint_point_sur;
//阴影路径的画笔
private Paint mPaint_path;
//折线的画笔
private Paint mPaint_brokenline;
//路径
private Path mpath=new Path();
//客户拜访的折线(BrokenLineCusVisit)数据
private List<BrokenLineCusVisit> mdata;

public BrokenLineCusVisitView(Context context) {
super(context);
}

public BrokenLineCusVisitView(Context context, AttributeSet attrs) {
super(context, attrs);
inite(context);
}

private void inite(Context context) {
mPaint_bg=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_bg.setColor(Color.argb(0xff,0xef,0xef,0xef));

mPaint_gridline=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_gridline.setColor(Color.argb(0xff,0xce,0xCB,0xce));

mPaint_brokenline=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_brokenline.setColor(Color.argb(0xff,0x91,0xC8,0xD6));
mPaint_brokenline.setTextSize(18);
mPaint_brokenline.setTextAlign(Paint.Align.CENTER);

mPaint_point_bg=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_point_bg.setColor(Color.argb(0xff, 0x91, 0xC8, 0xD6));

mPaint_path=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_path.setColor(Color.argb(0x33,0x91,0xC8,0xD6));


mPaint_point_sur=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_point_sur.setColor(Color.WHITE);


mPaint_text=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint_text.setColor(Color.BLACK);
mPaint_text.setTextAlign(Paint.Align.CENTER);

invalidate();
}

public List<BrokenLineCusVisit> getMdata() {
return mdata;
}

public void setMdata(List<BrokenLineCusVisit> mdata) {
this.mdata = mdata;
requestLayout();//相当于调用onMeasure方法
invalidate();//相当于调用onDraw方法
}

@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
canvas.drawRect(10,0,width,heigh-brokenline_bottom,mPaint_bg);

//Y轴不变 X轴绘制直线
for (int j=0;j<4;j++){
canvas.drawLine(10,gridspace_heigh*(j+1),width,gridspace_heigh*(j+1),mPaint_gridline);
}


for (int i= 0; i < mdata.size();i++){
if (i==0){
mpath.moveTo(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100);
}
canvas.drawLine(gridspace_width * i + 10, 0, gridspace_width * i + 10, heigh - brokenline_bottom, mPaint_gridline);

if (i!=mdata.size()-1){
canvas.drawLine(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, gridspace_width * (i + 1) + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i + 1).getData() / 100, mPaint_brokenline);
mpath.quadTo(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, gridspace_width * (i + 1) + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i + 1).getData() / 100);
}
canvas.drawCircle(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, 10, mPaint_point_bg);
canvas.drawCircle(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100, 5, mPaint_point_sur);
String data=mdata.get(i).getData()+"";
canvas.drawText(data,gridspace_width*i+10,heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData()/100-mPaint_brokenline.measureText(data),mPaint_brokenline);
String date=mdata.get(i).getDate();
canvas.drawText(date, gridspace_width * i + 10, heigh - brokenline_bottom / 2, mPaint_text);
if (i==mdata.size()-1){
mpath.quadTo(gridspace_width * i + 10, heigh - brokenline_bottom - (heigh - brokenline_bottom) * mdata.get(i).getData() / 100,gridspace_width * i + 10, heigh - brokenline_bottom );
mpath.quadTo(gridspace_width * i + 10, heigh - brokenline_bottom ,10, heigh - brokenline_bottom);
mpath.close();
}
}
canvas.drawPath(mpath,mPaint_path);

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
gridspace_width= 50;
if(mdata.size()==0){
//通过调用onMeasure源码中的方法进行获得宽度,
//宽度的获得有三种模式,具体介绍见博客底部连接
width=getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
}
else{
//根据数据条数设置宽度
width=gridspace_width*mdata.size()+10;
}

heigh=getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
//设置底部白色空白的宽度
brokenline_bottom=50;


gridspace_heigh=(heigh-brokenline_bottom)/4;
setMeasuredDimension(width, heigh);
}


}

​​onMeasure方法的具体介绍​​


版权声明

本文仅代表作者观点,不代表博信信息网立场。

热门