图表绘制
在项目中,常常会用到一些图表,我们可以使用第三方开源控件进行绘制,但是如果有一些特殊的定制化的需求,
我们可能需要自己进行一些修改。所以,最好对这种需求有所准备。
简易示波器
图中为显示一组正弦数据,及其FFT后的数据。接着开始贴代码吧
示波器控件类
类中包含了控件的绘制方法以及相关数据结构。
基本思路:
1.测量控件,获取表格中的缩放比例等信息
2.获取数据,获取原始输入数据,并根据需求转换为控件的显示坐标(控件的左上角为0,0点)
3.绘制线图
package com.example.myxytable.view;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
/**
* XY控件
* @author vonchenchen
*
*/
public class XYWaveTable extends View{
private static float mScale;
private Paint paint;
/** 控件宽度 单位为像素点 */
private int mWidthSize;
/** 控件高度 单位为像素点 */
private int mHheightSize;
/** 基准点 */
private Point basePoint = new Point();
/** 横坐标点数 */
private int mHorizantalPointsNumber;
/** 纵坐标点数 */
private int mVerticalPointsNumber;
/** 横坐标间隔 */
private int mHorizantalPointsGap;
/** 纵坐标间隔 */
private int mVerticalPointsGap;
private ArrayList<Line> mLines = new ArrayList<Line>();
public XYWaveTable(Context context) {
this(context, null);
}
public XYWaveTable(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XYWaveTable(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mScale = context.getResources().getDisplayMetrics().density;
// 初始化一个抗锯齿的画笔
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
}
/**
* 设置横坐标 纵坐标点数
* @param width
* @param height
*/
public void setPointsNumber(int width, int height){
this.mHorizantalPointsNumber = width;
this.mVerticalPointsNumber = height;
}
public static int dip2px(Context context, float dpValue) {
return (int) (dpValue * mScale);
}
/** 增加一条曲线 */
public void addLine(Line line){
mLines.add(line);
invalidate();
}
/** 画坐标线 */
private void drawFrame(Canvas canvas){
paint.setColor(Color.WHITE);
canvas.drawLine(0, mHheightSize/2, mWidthSize, mHheightSize/2, paint);
}
/**
* 测量控件,获取需要的各项长度参数信息,此处我们为了获取个点间 水平与垂直的单位距离
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//int widthMode = MeasureSpec.getMode(widthMeasureSpec);// 获取到当前FlowLayout的宽的模式
mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
//int heightMode = MeasureSpec.getMode(heightMeasureSpec);// 获取到当前FlowLayout的高的模式
mHheightSize = MeasureSpec.getSize(heightMeasureSpec);
basePoint.x = 0;
basePoint.y = mHheightSize/2;
//计算点与点间的间隔
mHorizantalPointsGap = (int)(mWidthSize / mHorizantalPointsNumber);
mVerticalPointsGap = (int)(mHheightSize / mVerticalPointsNumber);
Log.i("XYWaveTable", "mWidthSize="+mWidthSize+" mHheightSize="+mHheightSize);
}
/**
* 坐标转换 将绝对坐标转换为显示坐标
*
* 绝对坐标是实际数据的坐标
* 显示坐标是我们在当前图像上绘制点时的坐标
* */
private Point changeXY(Point originalPoint){
originalPoint.x = originalPoint.x + basePoint.x;
originalPoint.y = mHheightSize - (originalPoint.y*mVerticalPointsGap + basePoint.y);
return originalPoint;
}
/**
* 绘制坐标连线
* @param canvas
* @param line
*/
private void drawLine(Canvas canvas, Line line){
paint.setColor(line.color);
ArrayList<Point> points = line.points;
Point pointStart; //起始点
Point pointStop; //结束点
pointStart = changeXY(points.get(0)); //获取起始点的显示坐标
int horizantalLenthStart = pointStart.x;
int horizantalLenthStop = horizantalLenthStart + mHorizantalPointsGap;
for(int i=1; i<line.points.size(); i++){
pointStop = changeXY(points.get(i)); //获取结束点的显示坐标
pointStop.y += mVerticalPointsGap;
//画线
canvas.drawLine(horizantalLenthStart, pointStart.y, horizantalLenthStop, pointStop.y, paint);
//更新起始点与结束点
pointStart = pointStop;
horizantalLenthStart = horizantalLenthStop;
horizantalLenthStop = horizantalLenthStart + mHorizantalPointsGap;
}
}
@Override
protected void onDraw(Canvas canvas) {
drawFrame(canvas);
//画出所有的曲线
for(int i=0; i<mLines.size(); i++){
drawLine(canvas, mLines.get(i));
}
super.onDraw(canvas);
}
public static class Point{
public int x;
public int y;
}
public static class Line{
public ArrayList<Point> points;
public int color;
}
}
控件的引用
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<com.example.myxytable.view.XYWaveTable
android:id="@+id/xy_table"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="#000000"
/>
</RelativeLayout>
控件的调用
调用分为四步,详见代码注释
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFFt = new FFT();
//1.获取控件实例
mXYTable = (XYWaveTable) findViewById(R.id.xy_table);
//2.设置 横坐标 纵坐标 点数
mXYTable.setPointsNumber(128,128);
//3.按照Line的结构创建序列
Line line1 = createDemoLine();
Line line2 = createFFtLine(line1);
//4.使用 addLine方法在控件上绘制一条曲线
mXYTable.addLine(line1);
mXYTable.addLine(line2);
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFFt = new FFT();
//1.获取控件实例
mXYTable = (XYWaveTable) findViewById(R.id.xy_table);
//2.设置 横坐标 纵坐标 点数
mXYTable.setPointsNumber(128,128);
//3.按照Line的结构创建序列
Line line1 = createDemoLine();
Line line2 = createFFtLine(line1);
//4.使用 addLine方法在控件上绘制一条曲线
mXYTable.addLine(line1);
mXYTable.addLine(line2);
}
创建曲线
/**
* 构造一条正弦曲线并绘制
* @return
*/
private Line createDemoLine(){
ArrayList<Point> list = new ArrayList<Point>();
for(int i=0; i<128; i++){
Point point = new Point();
point.x = i;
point.y = (int) (Math.sin((double)i)*32);
list.add(point);
}
Line line = new Line();
line.points = list;
line.color = Color.RED;
return line;
}
附录 java FFT代码
把原先嵌入式系统里的FFT C函数包装了一下,本文用到的FFT类,放在这里,方便日后粘贴
package com.example.myxytable.fft;
public class FFT {
private static double PI = 3.141592653;
private static int FFT_N = 128;
public FFT(){
create_sin_tab(SIN_TAB);
}
public static class Compx {
public float real;
public float imag;
};
//private Compx[] S = new Compx[FFT_N];
private float SIN_TAB[] = new float[FFT_N/2];
/*******************************************************************
函数原型:struct compx EE(struct compx b1,struct compx b2)
函数功能:对两个复数进行乘法运算
输入参数:两个以联合体定义的复数a,b
输出参数:a和b的乘积,以联合体的形式输出
*******************************************************************/
private Compx EE(Compx a,Compx b)
{
Compx c = new Compx();
c.real=a.real*b.real-a.imag*b.imag;
c.imag=a.real*b.imag+a.imag*b.real;
return(c);
}
/******************************************************************
函数原型:void create_sin_tab(float *sin_t)
函数功能:创建一个正弦采样表,采样点数与福利叶变换点数相同
输入参数:*sin_t存放正弦表的数组指针
输出参数:无
******************************************************************/
private void create_sin_tab(float[] sin_t)
{
int i;
for(i=0;i<FFT_N/2;i++)
sin_t[i]=(float) Math.sin(2*PI*i/FFT_N);
}
/******************************************************************
函数原型:void sin_tab(float pi)
函数功能:采用查表的方法计算一个数的正弦值
输入参数:pi 所要计算正弦值弧度值,范围0--2*PI,不满足时需要转换
输出参数:输入值pi的正弦值
******************************************************************/
private float sin_tab(float pi)
{
int n;
float a = 0;
n=(int)(pi*FFT_N/2/PI);
if(n>=0&&n<FFT_N/2)
a=SIN_TAB[n];
else if(n>=FFT_N/2&&n<FFT_N)
a=-SIN_TAB[n-FFT_N/2];
return a;
}
/******************************************************************
函数原型:void cos_tab(float pi)
函数功能:采用查表的方法计算一个数的余弦值
输入参数:pi 所要计算余弦值弧度值,范围0--2*PI,不满足时需要转换
输出参数:输入值pi的余弦值
******************************************************************/
private float cos_tab(float d)
{
float a,pi2;
pi2=(float) (d+PI/2);
if(pi2>2*PI)
pi2-=2*PI;
a=sin_tab(pi2);
return a;
}
/*****************************************************************
函数原型:void FFT(struct compx *xin,int N)
函数功能:对输入的复数组进行快速傅里叶变换(FFT)
输入参数:*xin复数结构体组的首地址指针,struct型
输出参数:无
*****************************************************************/
public void FFT(Compx[] xin)
{
int f,m,nv2,nm1,i,k,l,j=0;
Compx u,w,t;
nv2=FFT_N/2; //变址运算,即把自然顺序变成倒位序,采用雷德算法
nm1=FFT_N-1;
for(i=0;i<nm1;i++)
{
if(i<j) //如果i<j,即进行变址
{
t=xin[j];
xin[j]=xin[i];
xin[i]=t;
}
k=nv2; //求j的下一个倒位序
while(k<=j) //如果k<=j,表示j的最高位为1
{
j=j-k; //把最高位变成0
k=k/2; //k/2,比较次高位,依次类推,逐个比较,直到某个位为0
}
j=j+k; //把0改为1
}
{
int le,lei,ip; //FFT运算核,使用蝶形运算完成FFT运算
f=FFT_N;
for(l=1;(f=f/2)!=1;l++) //计算l的值,即计算蝶形级数
;
for(m=1;m<=l;m++) // 控制蝶形结级数
{ //m表示第m级蝶形,l为蝶形级总数l=log(2)N
le=2<<(m-1); //le蝶形结距离,即第m级蝶形的蝶形结相距le点
lei=le/2; //同一蝶形结中参加运算的两点的距离
u = new Compx();
u.real=(float) 1; //u为蝶形结运算系数,初始值为1
u.imag=(float) 0;
//w.real=cos(PI/lei); //不适用查表法计算sin值和cos值
// w.imag=-sin(PI/lei);
w = new Compx();
w.real=cos_tab((float) (PI/lei)); //w为系数商,即当前系数与前一个系数的商
w.imag=-sin_tab((float) (PI/lei));
for(j=0;j<=lei-1;j++) //控制计算不同种蝶形结,即计算系数不同的蝶形结
{
for(i=j;i<=FFT_N-1;i=i+le) //控制同一蝶形结运算,即计算系数相同蝶形结
{
ip=i+lei; //i,ip分别表示参加蝶形运算的两个节点
t=EE(xin[ip],u); //蝶形运算,详见公式
xin[ip].real=xin[i].real-t.real;
xin[ip].imag=xin[i].imag-t.imag;
xin[i].real=xin[i].real+t.real;
xin[i].imag=xin[i].imag+t.imag;
}
u=EE(u,w); //改变系数,进行下一个蝶形运算
}
}
}
}
}