相信折线图大家都不陌生,有些数据统计,基金股票都是通过通过折线图的方式去展示数据,接下来就介绍一种方法,去通过代码编写一个类似的功能。
首先看一下效果图
编写之前要了解一下怎么样去入手,首先从界面出发,看似复杂的一张折线图,如果放大就会发现其实是有一条条长度、大小不一的直线拼凑而成
在代码中直线的绘制比较简单了,一句代码而已,参数分别为 起始 x、y坐标和结束 x、y 坐标
通过一组组不同的数据不就能拼凑成一张看似复杂的折线了吗,艾玛,讲完了。
其实这里面还是有许多需要注意的地方的,不妨接着看下去。
上面讲完了折线,再讲一下折线下的阴影部分,其实这也没有太复杂,只是不同的绘画,同折线频率一样,一次次拼凑而成。实际中间空隙是不存在的,当然也要看代码中怎么写了,这里添加空隙是为了更直观些。
阴影部分的代码部分也不是很复杂。。和直线差不多,就是需要四条线去画出范围,然后通过 .close() 方法将范围内进行填充
以上就是对界面的解析,接下来是对逻辑的解析。
通过上面的解析图可以看出,每次画线和画背景的时候,下一次的开始坐标都是上一次的结束坐标,这也说明每次数据更新时只需要一组 x、y 数据即可,所以在计算时只需拿到这组数据就行,首先要建一个实体类了 entity,来定义一下 x、y 坐标,这里只写了需要用到的数据。
因为数据展示一般都是即时展示,比如股票、基金,目前股票是三秒进行一次数据刷新,每次刷新都进行一次 onDraw() 进行绘画,就形成了我们看到的分时图实时刷新的效果,这里通过线程每一秒进行一次数据刷新达到类似效果。
因为数据是上下波动的,这里通过随机数的形式进行数据伪造。
要明白一点,折线图是一直向前展示的,所以 X 轴是一直比上一次的数据要大的(如果是反向折线图,那相反),数据也要尽可能与我们的界面相配合,不能说我们的界面高度为 100,你给我的数据是 200,那界面不就混乱了吗,需要对返回的数据进行适配,比如说股票是有涨停与跌停的,当达到某极限值时设置Y轴为当前界面最大值即可。或者规定给我的数据在我界面的承受范围内,在此基础上进行改造。
数据有了,接下来就要进行绘制了,将折线数据给折线View之后,拿到数据之后进行界面重新绘制,这里注意一下哦,不是讲过下一直线的起始坐标为上个直线的结束坐标么,第一条直线的坐标呢,他前面似乎没有直线,不要把第一条线的起始坐标设置为 ( 0,0 )哦,否则你会发现一开始会有一个飞流直下的线,那咋办,还没开始结束了。
你可以把第一条直线想想成一个点,因为最开始是从没有数据到有了第一条数据,那这个第一条数据是开始也是结束。
具体表现在代码中如下:
直线完成了,背景部分也差不多,这里我将背景单独拿出来,根据需求进行判断是否绘制,和其他参数的修改,具体表现在:
这里不用担心,单独设置之后会不会有效果,因为在赋值之后有一个刷新操作,会重新拿起设置的这些值。
为了更直观些,将折线下背景逻辑画出来,加深一下印象,从第一个坐标开始,到第二个,第三个,第四个,最后别忘了回到第一个,然后将该范围内进行颜色填充,就形成了印象部分
以上就是走势图的大体逻辑,接下来将源码贴上,供需要的同学进行参考
目录结构
MainActivity
/**
*注释:
*分时图绘画
*/
public class MainActivity extends AppCompatActivity {
//存放数据的容器
private ArrayList<TimeViewEntity> mTimeList = new ArrayList<>();
private TimeView tv_view;
int startX = 0;
boolean isSetData = false;//是否添加数据开关
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取控件
tv_view = findViewById(R.id.tv_view);
tv_view.setViewColor(Color.YELLOW,Color.GRAY,R.color.purple_200);
tv_view.setShowBgBottom(false);
tv_view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
isSetData = !isSetData;
}
});
//开启线程,每一秒都去设置分时图数据
thread.start();
}
@RequiresApi(api = Build.VERSION_CODES.N)
private void initData(){
//模拟数据
TimeViewEntity t = new TimeViewEntity();
//设置X轴数据的终止位置,无需设置起始位置,因为每条线的起始位置就是上一条线的终点位置
int stopX = new SplittableRandom().nextInt(startX, startX+10);
startX = stopX;
t.setStopX(startX);
//设置Y轴数据的终止位置,无需设置起始位置,因为每条线的起始位置就是上一条线的终点位置
Random ra = new Random();
int stopY = ra.nextInt(tv_view.getMeasuredHeight()/2);
t.setStopY(stopY);
//给分时图设置数据
mTimeList.add(t);
tv_view.setViewData(mTimeList);
}
/**
* 通过线程来动态设置View达到动画效果
* */
private final Thread thread = new Thread(){
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void run() {
while(true){
try {
//通过此开关进行伪停止添加数据
if (isSetData){
initData();
}
//如果数据终止X的值大于界面宽度停止线程或者其他操作
if (mTimeList.size() > 0 && mTimeList.get(mTimeList.size()-1).getStopX() > tv_view.getMeasuredWidth()){
//清除容器数据,重新添加
startX = 0;
mTimeList.clear();
//thread.stop(); //终止线程
}
/**休息时间*/
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
TimeView
/**
* 作者:zch
* 时间:2022/3/18 9:37
* 描述:自定义分时图View
*/
public class TimeView extends View {
private Paint paint;
private int mCanvasHeight;
private int mCanvasWidth;
private int mStartY;
private int paintColor = 0;
private int canvasColor = 0;
private int bgColor = 0;
private boolean isShowBg = false;
private ArrayList<TimeViewEntity> mTimeList = new ArrayList<>();
public TimeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
//开始绘画逻辑
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//首先定义画笔
paint = new Paint();
//防锯齿
paint.setAntiAlias(true);
//获取画板高度
mCanvasHeight = getMeasuredHeight();
//获取画板宽度
mCanvasWidth = getMeasuredWidth();
//设置画笔颜色
paint.setColor(paintColor == 0 ? Color.YELLOW : paintColor);
//设置画板背景
canvas.drawColor(canvasColor == 0 ? Color.GRAY : canvasColor);
//设置画笔粗细
paint.setStrokeWidth((float)3);
//Y轴的起止都在折线图的四分之一处开始,因为数据的范围是折线图的二分之一
//这样数据波动位置就处于折线图居中位置
mStartY = mCanvasHeight / 4;
//画线
drawView(canvas);
}
/**
*注释:
*绘画逻辑
*/
private void drawView(Canvas canvas){
//如果有数据
if (mTimeList.size() > 0){
int startX = 0,startY = 0;
int stopX,stopY;
for (int i = 0; i < mTimeList.size(); i++){
TimeViewEntity t = mTimeList.get(i);
stopY = t.getStopY() + mStartY;
stopX = t.getStopX();
//首次的时候起始位置就等于终止位置
if (i == 0){
startX = stopX;
startY = stopY;
}
//画线
canvas.drawLine(startX,startY,stopX,stopY,paint);
//是否画背景
if (isShowBg){
setDrawBg(startX,startY,stopX,stopY,canvas);
}
//下次画线的时候的起始位置等于上次画线位置的终止位置
startX = stopX;
startY = stopY;
}
}
}
/**
*注释:
* 画折线下背景
*/
private void setDrawBg(int tx,int ty,int px,int py,Canvas canvas){
//设置画笔
Paint paint = new Paint();
//防锯齿
paint.setAntiAlias(true);
//设置颜色
paint.setColor(bgColor == 0 ? Color.WHITE : bgColor);
//画阴影部分
Path bg = new Path();
bg.moveTo(tx, ty);
bg.lineTo(tx, mCanvasHeight);
bg.lineTo(px, mCanvasHeight);
bg.lineTo(px, py);
bg.lineTo(tx, ty);
bg.close();
//添加到画板上
canvas.drawPath(bg, paint);
}
/**
*注释:
* 设置分数图数据,由外部传输
*/
public void setViewData(ArrayList<TimeViewEntity> m) {
this.mTimeList = m;
//刷新界面 - 无需在UI线程,在工作线程即可被调用,invalidate()必须在UI线程
postInvalidate();
}
/**
*注释:
*设置分时图背景和画笔颜色
* p - 画笔颜色
* c - 背景颜色
* b - 折线下背景颜色
*/
public void setViewColor(int p,int c,int b){
this.paintColor = p;
this.canvasColor = c;
this.bgColor = b;
}
/**
*注释:
* 是否显示折线下的背景
*/
public void setShowBgBottom(boolean b){
this.isShowBg = b;
}
}
TimeViewEntity
/**
* 作者:zch
* 时间:2022/3/18 10:04
* 描述:分时图数据类
*/
public class TimeViewEntity {
private int stopX;
private int stopY;
public int getStopX() {
return stopX;
}
public void setStopX(int stopX) {
this.stopX = stopX;
}
public int getStopY() {
return stopY;
}
public void setStopY(int stopY) {
this.stopY = stopY;
}
}
activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.example.fsview.TimeView
android:id="@+id/tv_view"
android:layout_width="match_parent"
android:layout_height="200dp"/>
</LinearLayout>
以上就是走势图基本用法,千篇一律,下面是在以上基础上做了一些改动,看一下效果图:
这里就不贴代码了,代码写法与上面的基本相似,全部代码上传到 资源了,免C币下载,需要的去下载吧。。 传送门