为 Android 创建位图 XY 图表/绘图
使用 Canvas/Bitmap 获取 XY 绘图
引言
绘制 XY 集是一个非常常见的任务,本文概述了在 Android 平台上实现此类模块的主要步骤。这与之前发布的类似的 BlackBerry 文章相符。
在 Android 平台上开发非常简单,其允许所有 GUI 在 XML 中定义(尽管它也可以通过代码创建)的模型非常好。大多数图形示例都处理 View,但在大多数情况下,图表应该作为定义为布局 XML 的屏幕的一部分,因此在这里我们展示了在 ImageView
布局对象上的实现。
背景
在 Android 环境中,有一整套图形例程,通常一个 Bitmap 包含像素,一个 Canvas 是我们可以应用绘制调用的地方(写入位图),有了这个,我们可以使用 paint 绘制基元(文本、线条等),它描述了颜色、样式等。由于 Android 开发平台都是 Java,因此它的结构非常好,并且可以使用 Java 的所有功能,如继承、子类化等。对于我们的示例,一切都保持简单。
GUI 被定义为 XML 布局文件,随附的示例代码显示了 XY 图表的基本实现,为了简单起见,X 点是均匀间隔的顺序数字。
1. 定义 GUI 的 XML 布局文件
具有 ID 为 testy_img
的 ImageView
对象是我们将用来绘制图表的那个
<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:background="#4B088A">
<TableRow android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="20px">
<TextView
android:id="@+id/some"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Some layout items here"
android:layout_marginLeft="10px"
android:layout_marginRight="10px"
android:textColor="#ff8000"
android:textStyle="bold"
/>
</TableRow>
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="#FFE6E6E6"
/>
<TableRow>
<ImageView
android:id="@+id/testy_img"
android:layout_marginLeft="20px"
android:padding="20px"
/>
</TableRow>
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="#FFE6E6E6" />
<TableRow android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="20px">
<TextView
android:id="@+id/more"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="More layout items here"
android:layout_marginLeft="10px"
android:layout_marginRight="10px"
android:textColor="#ff8000"
android:textStyle="bold"
/>
</TableRow>
</TableLayout>
2. 图表实现
要实现我们的图表,首先我们创建一个位图,我们将在其中写入,然后我们将其与布局 XML 对象关联。一旦我们有了位图,我们只需执行图表实现,在那里我们进行缩放、着色和数据处理。这个图表元素由一个网格组成,数据将在其中绘制,一个缩放/转置方法,以便可以将数据点连贯地映射到可用的屏幕空间,以及一个数据处理例程,用于循环遍历数据点。
2.1 定义要绘制的位图
如下所示,首先我们连接到 XML 布局中的对象,然后我们创建 Bitmap,我们将在其中进行所有绘制,这是由 quicky_XY
执行的,最后我们将此位图打印到图像上,从而打印到屏幕上。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.testy);
setTitle("Quick XY Plot");
ImageView image = (ImageView) findViewById(R.id.testy_img);
Bitmap emptyBmap = Bitmap.createBitmap(250,
200, Config.ARGB_8888);
int width = emptyBmap.getWidth();
int height = emptyBmap.getHeight();
Bitmap charty = Bitmap.createBitmap(width , height , Bitmap.Config.ARGB_8888);
charty = quicky_XY(emptyBmap);
image.setImageBitmap(charty);
}
2.2 绘制用于绘图的网格
一旦定义了基本的 Bitmap
,我们就需要相关的画布,这可以通过以下方式完成
Canvas canvas = new Canvas(bitmap)
从那里开始,所有绘图都可以在画布上完成。我们需要网格来定义将绘制数据点的地方,一个图表通常会有一个标签和一些数据点范围。在所示的代码中,Vector 参数包含 [Label
, units
, maxValue
, minValue
] 形式的对象,完成后,drawSizes
数组将具有绘图区域的位置和尺寸。
public static void draw_the_grid(Canvas this_g, Vector these_labels)
{
double rounded_max = 0.0;
double rounded_min = 0.0;
double rounded_max_temp;
Object curElt;
String[] cur_elt_array;
int left_margin_d, right_margin_d;
if( draw_only_this_idx == -1)
curElt = these_labels.elementAt(0); // default it to 1st one if non set
else
curElt = these_labels.elementAt(draw_only_this_idx); // now just the 1st elt
cur_elt_array = (String[])curElt;
rounded_max = get_ceiling_or_floor (Double.parseDouble(cur_elt_array[2]) , true);
rounded_min = get_ceiling_or_floor (Double.parseDouble(cur_elt_array[3]) , false);
// ok so now we have the max value of the set just get a cool ceiling and we go on
final Paint paint = new Paint();
paint.setTextSize(15);
left_margin_d = getCurTextLengthInPixels(paint, Double.toString(rounded_max));
//keep the position for later drawing -- leave space for the legend
int p_height = 170;
int p_width = 220;
int[] tmp_draw_sizes = {2 + left_margin_d, 25,p_width - 2 -
left_margin_d ,p_height - 25 -5};
drawSizes = tmp_draw_sizes; //keep it for later processing
//with the mzrgins worked out draw the plotting grid
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE );
// Android does by coords
this_g.drawRect(drawSizes[0], drawSizes[1],drawSizes[0]+
drawSizes[2], drawSizes[1]+ drawSizes[3] , paint);
paint.setColor(Color.GRAY );
// finally draw the grid
paint.setStyle(Paint.Style.STROKE);
this_g.drawRect(drawSizes[0], drawSizes[1],drawSizes[0]+
drawSizes[2], drawSizes[1]+ drawSizes[3] , paint);
for(int i=1; i < 5 ; i++)
{
this_g.drawLine(drawSizes[0], drawSizes[1] +
(i * drawSizes[3] / 5), drawSizes[0] + drawSizes[2],
drawSizes[1] + (i * drawSizes[3] / 5), paint);
this_g.drawLine(drawSizes[0]+ (i * drawSizes[2] / 5),
drawSizes[1], drawSizes[0] + (i * drawSizes[2] / 5),
drawSizes[1] + drawSizes[3], paint);
}
// good for one value
print_axis_values_4_grid(this_g, cur_elt_array[1] ,
Double.toString(rounded_max) , Double.toString(rounded_min),
cur_elt_array[0] , 2 ,0 );
} // --- end of draw_grid ---
2.3. 绘图和缩放
数据点需要从数据范围到屏幕坐标的正确映射,这通过下面所示的 scale 方法完成。循环遍历数据点并在两个连续点之间调用 drawLine
将结束我们的 Chart。数据点将作为包含数据的 Vector 传递,现在将调用 plot_array_list
。
private static Point scale_point(int this_x , double this_y , Point drawPoint ,
int scr_x , int scr_y , int scr_width , int src_height ,
double maxX , double minX , double maxY , double minY )
{
int temp_x, temp_y;
Point temp = new Point();
if (maxY == minY) //skip bad data
return null;
//don't touch it if is nothing
try
{
temp_x = scr_x + (int)( ((double)this_x - minX) *
((double)scr_width / (maxX - minX)) );
temp_y = scr_y + (int)( (maxY - this_y) *
((double)src_height / (maxY - minY)) );
temp.x = temp_x;
temp.y= temp_y;
drawPoint = temp;
}
catch (Exception e)
{
return (null);
}
return temp;
} // --- end of scale_point --
public static boolean plot_array_list(Canvas this_g, Vector this_array_list ,
Vector these_labels , String this_title , int only_this_idx )
{
int idx;
int lRow ;
int nParms;
int i, points_2_plot, shifted_idx ;
int prev_x, prev_y ;
int cur_x=0, cur_y=0 ;
//Dim ShowMarker As Object
Point cur_point = new Point();
cur_point.set(0,0);
double cur_maxX, cur_minX, cur_maxY=20, cur_minY=0, cur_rangeY;
int cur_start_x, cur_points_2_plot;
int POINTS_TO_CHANGE = 30;
double cur_OBD_val;
//Object curElt;
String curElt;
String[] cur_elt_array;
Object curElt2;
String[] cur_elt_array2;
final Paint paint = new Paint();
try // catch in this block for some thing
{
points_2_plot = this_array_list.size();
{
cur_start_x = 0;
cur_points_2_plot = points_2_plot;
cur_maxX = cur_points_2_plot;
cur_minX = 0;
}
//'Create the plot points for this series from the ChartPoints array:
curElt = (String)this_array_list.elementAt(0);
//the lines have to come out good
paint.setStyle(Paint.Style.STROKE);
//
//for( nParms = 0 ; nParms < cur_elt_array.length ; nParms++ )
nParms = only_this_idx;
{
//get cur item labels
curElt2 = these_labels.elementAt(nParms);
cur_elt_array2 = (String[]) curElt2;
cur_maxY = get_ceiling_or_floor
(Double.parseDouble(cur_elt_array2[2]) , true);
cur_minY = get_ceiling_or_floor
(Double.parseDouble(cur_elt_array2[3]) , false);
cur_points_2_plot = this_array_list.size();
cur_maxX = cur_points_2_plot;
curElt = (String)this_array_list.elementAt(0);
cur_OBD_val = Double.parseDouble( curElt);
cur_point = scale_point(0, cur_OBD_val, cur_point,
drawSizes[0], drawSizes[1], drawSizes[2], drawSizes[3],
cur_maxX, cur_minX, cur_maxY,
cur_minY); //'(CInt(curAxisValues.Mins(nParms - 2) / 5) + 1) * 5)
cur_x = cur_point.x;
cur_y = cur_point.y;
paint.setColor(Color.GREEN);
// the point is only cool when samples are low
if ( cur_points_2_plot < POINTS_TO_CHANGE)
this_g.drawRect(cur_x-2, cur_y-2, cur_x-2 + 4,
cur_y-2+ 4 , paint);
prev_x = cur_x;
prev_y = cur_y;
//'go and plot point for this parm -- pont after the 1st one
for (lRow = cur_start_x +1 ; lRow< cur_start_x +
cur_points_2_plot -1 ; lRow++)
{
curElt = (String)this_array_list.elementAt(lRow);
cur_OBD_val = Double.parseDouble( curElt);
//'work out an approx if cur Y values not avail(e.g. nothing)
// if (! (cur_elt_array[nParms ] == null ) ) //skip bad one
if( cur_OBD_val == Double.NaN) continue; //skip bad one
{
cur_point=scale_point(lRow, cur_OBD_val, cur_point,
drawSizes[0], drawSizes[1],
drawSizes[2], drawSizes[3],
cur_maxX, cur_minX, cur_maxY, cur_minY);
cur_x = cur_point.x;
cur_y = cur_point.y;
if ( cur_points_2_plot < POINTS_TO_CHANGE)
this_g.drawRect(cur_x-2, cur_y-2, cur_x-2 +4,
cur_y-2 + 4, paint );
this_g.drawLine( prev_x, prev_y, cur_x, cur_y, paint);
prev_x = cur_x;
prev_y = cur_y;
} // ' if end of this_array(lRow, nParms - 1)<> nothing
} // end of for lrow
} // end of for nParmns
//this_g.invalidate();
return( true);
}
catch (Exception e)
{
return( false);
}
} // --- end of plot_array_list --
这样,绘图就完成了,输出现在看起来像下图

最终想法
所示的方法运行良好,当我们确实需要进行实时绘图时,使用了例程的轻微修改,其中使用 Queue 数据结构代替了 Vector,因为我们需要能够绘制数据,因为它也具有滚动功能,但基本组件是相同的。可以根据要求应用更多修改。其中一种修改可以使用 drawLines
调用,但在这里我们需要设置 lines 数组参数,这可能与我们的 plot_array_list
方法没有太大区别。
历史
- 2010 年 7 月 27 日:最初发布