Java ME 入门: 一个简单的分形查看器






4.85/5 (8投票s)
在您的移动设备上渲染曼德尔布罗集,支持平移和缩放

引言
Sun Microsystems 发布的 Java ME SDK 3.0 版本使得为移动设备开发 Java 应用程序变得非常容易。该 SDK 配备了一个功能齐全(且令人惊讶地可用)的 IDE,以及一套模拟器和示例应用程序,所有这些都可以让您立即开始编写自己的应用程序。我一直想开发一些可以从手机上运行的小应用程序,所以我觉得现在是时候开始了。
我的第一个想法是开发一个非常基本的 曼德尔布罗集渲染器和缩放器。当然,它不是非常“有用”,但它绝对很漂亮,而且您将知道您将始终拥有现代数学的标志性符号之一触手可及。
当然,这也是在酒吧或舞厅泡妞的绝佳方式。我是说,拜托,拿出那个老掉牙的曼德尔布罗集,她们就会被你迷住,对吧?有人吗?
简单回顾一下,曼德尔布罗集是通过迭代复二次多项式 zn+1 = zn2 + c 来绘制的,其中 c 是我们想要绘制的复平面范围内的每个“像素”。如果对于给定的 c(在一定迭代次数内)该序列是有界的,则该点被认为是曼德尔布罗集的一部分,并且像素被着色为黑色。如果对于给定的 c 该序列是无界的,则像素的颜色取决于序列“发散”的速度。我们进行的计算迭代次数越多,“精度”就越高。
以下是应用程序的一些基本要求
- 使用预定义的调色板渲染曼德尔布罗集
- 允许用户通过按 L、R、U 和 D 键来“移动”绘图
- 允许用户通过按 1 和 3 键来“放大”和“缩小”集合
- 允许用户通过按 7 和 9 键分别减小/增加迭代次数
创建新项目
现在我们有了一些要求,让我们开始吧。我们将使用 Java ME SDK 创建一个全新的项目,并给它起一个原创的名字,比如 **Mandelbrot**

为了这个项目,使用所有默认设置就足够了。SDK 应该会生成一个针对 CLDC 1.1(Connected Limited Device Configuration)和 MIDP 2.0(Mobile Information Device Profile)的项目。它还将自动创建一个 MIDlet
类来表示我们的新应用程序。
自动生成的类继承自基类 MIDlet
,并实现了 CommandListener
接口,这使得我们的应用程序能够“监听”我们可以分配给手机按钮的命令。该类将被命名为 HelloMIDlet
之类的,但我们可以轻松地将其重命名为更相关的名称,如 MandelbrotApp
,并删除“hello world”代码,以便我们有一个完全干净的类。
理论上,除了继承 MIDlet
的类之外,我们还需要一个继承自基类 Canvas
的类(我们将称之为 MandelbrotCanvas
),以便我们可以执行任何我们需要的图形操作。这个类还将负责捕获手机键盘的按键(这与捕获 MIDlet
类所做的“命令”不同)。
代码
让我们首先看一下 MandelbrotApp
类。以下是该类的开头几行,后面附带一些解释
public class MandelbrotApp extends MIDlet implements CommandListener {
private Display myDisplay;
private MandelbrotCanvas myCanvas;
private Command exit = new Command ("Exit", Command.EXIT, 1);
private Command about = new Command ("About", Command.ITEM, 2);
private int[] colorPalette;
private int[] scanlineBuffer;
private int screenWidth = 0, screenHeight = 0;
public float rangex1 = -2F, rangex2 = 0.5F, rangey1 = -1.4F, rangey2 = 1.4F;
public int numIterations = 24;
public MandelbrotApp () {
super ();
myDisplay = Display.getDisplay (this);
myCanvas = new MandelbrotCanvas (this);
myCanvas.setCommandListener (this);
myCanvas.addCommand (exit);
myCanvas.addCommand(about);
scanlineBuffer = null;
colorPalette = new int[256];
for(int i=0; i<64; i++)
colorPalette[i] = (((i * 4) << 8) | ((63 - i) * 4));
for(int i=64; i<128; i++)
colorPalette[i] = ((((i - 64) * 4) << 16) | (((127 - i) * 4) << 8));
for(int i=128; i<192; i++)
colorPalette[i] = (((255) << 16) | ((i - 128) * 4));
for(int i=192; i<256; i++)
colorPalette[i] = ((((255 - i) * 4) << 16) | (255));
}
在上面的代码中,我们对稍后将要使用的一些东西进行了初始化。首先,我们创建了画布对象(MandelbrotCanvas
)的一个实例,并向画布分配了两个命令(“退出”和“关于”)。这意味着当画布可见时,这两个命令将可从您的手机键盘的两个顶部按钮中获得。此外,通过将画布的 CommandListener
设置为 this
,我们表明该类将处理画布显示时发出的命令。变量 rangex1
、rangex2
、rangey1
和 rangey2
代表我们曼德尔布罗集计算的初始边界。通过操作这些变量,我们可以平移和缩放图像。
我们还定义了一个将在渲染曼德尔布罗集时使用的调色板。调色板包含 256 个条目,并且只是从红色到绿色、再到蓝色、再回到红色的颜色渐变。此外,我们还定义了一个名为 scanlineBuffer
的变量,它将实际包含屏幕内容,然后再将其绘制到屏幕上。我们将其初始化为 null
,因为我们将在首次调用 paint
事件时动态分配此缓冲区。
接下来,我们将创建一个实际将曼德尔布罗集渲染到我们的屏幕缓冲区中的函数。此函数使用标准的曼德尔布罗集算法,没有任何花哨的优化尝试,因此对于某些手机来说可能比其他手机慢(在某些手机上会非常非常慢;抱歉!)。
public void RenderMandelbrot(){
if((myCanvas.getWidth() != screenWidth) ||
(myCanvas.getHeight() != screenHeight)){
screenWidth = myCanvas.getWidth();
screenHeight = myCanvas.getHeight();
scanlineBuffer = new int[screenWidth * screenHeight];
}
float bmpWidth = (float)screenWidth;
float bmpHeight = (float)screenHeight;
float x, y, xsquare, ysquare, dx, dy, bail = 4, j, p;
int i, mul, col;
int xpos, ypos;
float[] q = null;
if(screenWidth > screenHeight) q = new float[screenWidth + 1];
else q = new float[screenHeight + 1];
mul = 255 / numIterations;
dx = (rangex2 - rangex1) / bmpWidth;
dy = (rangey2 - rangey1) / bmpHeight;
q[0] = rangey2;
for(i=1; i < q.length; i++) q[i] = q[i - 1] - dy;
xpos = 0; ypos = 0;
for(p = rangex1; p <= rangex2; p += dx){
i = 0;
for(j = rangey1; j <= rangey2; j += dy){
x = 0; y = 0; xsquare = 0; ysquare = 0; col = 1;
while(true){
if(col > numIterations){
scanlineBuffer[ypos*screenWidth + xpos] = 0;
break;
}
if((xsquare + ysquare) > bail){
scanlineBuffer[ypos*screenWidth + xpos] =
colorPalette[(col*mul)%255];
break;
}
xsquare = x * x;
ysquare = y * y;
y *= x;
y += (y + q[i]);
x = xsquare - ysquare + p;
col++;
}
i++;
ypos++;
if(ypos >= screenHeight) break;
}
myCanvas.repaint();
myCanvas.serviceRepaints();
xpos++;
if(xpos >= screenWidth) break;
ypos = 0;
}
}
请注意,在上面的函数中,在外部循环内,我们强制重新绘制画布,以便用户能够实时感知图形的绘制。如果我们不这样做,应用程序将完全无响应,直到所有图像都渲染完毕。
当应用程序启动时,将调用以下函数,在该函数中,我们将画布设置为当前显示的对象的,并调用曼德尔布罗集渲染函数
public void startApp () throws MIDletStateChangeException {
myDisplay.setCurrent (myCanvas);
RenderMandelbrot();
}
此类的另一个有趣之处在于 paint 处理程序。此函数实际上是从 Canvas
类(见下文)调用的,但我为了方便将 paint 代码放在了这个类中。
public void paint (Graphics g) {
g.drawRGB(scanlineBuffer, 0, screenWidth, 0, 0, screenWidth, screenHeight, false);
g.setColor(0xFFFFFF);
int fontHeight = g.getFont().getHeight();
int strY = 4;
g.drawString("(C) Dmitry Brant", 4, strY, 0); strY += fontHeight;
g.drawString("Iterations: " + Integer.toString(numIterations),
4, strY, 0); strY += fontHeight;
}
在上面的函数中,我们所做的只是将屏幕缓冲区绘制到屏幕上,然后在图像上写一些文本,其中包括一条小版权信息和曼德尔布罗集计算中使用的当前迭代次数。请注意,我们动态获取手机字体的 (并相应地调整),因为不同手机型号的字体差异很大。
此类中另一个有趣的函数是命令处理程序。当我们的应用程序运行时,如果按下“退出”或“关于”命令,将调用此函数。如果按下“退出”,我们将销毁应用程序。如果按下“关于”,我们将显示一个简单的 Alert
消息
public void commandAction (Command cmd, Displayable disp) {
if (cmd == exit) {
destroyApp (true);
}
else if(cmd == about){
Alert alert = new Alert ("About...");
alert.setType (AlertType.INFO);
alert.setTimeout (Alert.FOREVER);
alert.setString ("Copyright 2009 Dmitry Brant.\nhttp://dmitrybrant.com");
myDisplay.setCurrent (alert);
}
}
最后,让我们快速看一下 MandelbrotCanvas
类
class MandelbrotCanvas extends Canvas {
MandelbrotApp myApp;
MandelbrotCanvas (MandelbrotApp mandelTestlet) {
myApp = mandelTestlet;
}
void init () {
}
void destroy () {
}
protected void paint (Graphics g) {
myApp.paint (g);
}
protected void keyPressed (int key) {
int action = getGameAction (key);
float xScale = (myApp.rangex2 - myApp.rangex1);
float yScale = (myApp.rangey2 - myApp.rangey1);
boolean gotAction = true, gotKey = true;
switch (action) {
case LEFT:
myApp.rangex1 += (xScale / 16.0F);
myApp.rangex2 += (xScale / 16.0F);
break;
case RIGHT:
myApp.rangex1 -= (xScale / 16.0F);
myApp.rangex2 -= (xScale / 16.0F);
break;
case UP:
myApp.rangey1 -= (yScale / 16.0F);
myApp.rangey2 -= (yScale / 16.0F);
break;
case DOWN:
myApp.rangey1 += (yScale / 16.0F);
myApp.rangey2 += (yScale / 16.0F);
break;
case FIRE:
default:
gotAction = false;
}
if(!gotAction){
switch (key){
case KEY_NUM1:
myApp.rangex1 -= (xScale / 4.0F);
myApp.rangex2 += (xScale / 4.0F);
myApp.rangey1 -= (yScale / 4.0F);
myApp.rangey2 += (yScale / 4.0F);
break;
case KEY_NUM3:
myApp.rangex1 += (xScale / 4.0F);
myApp.rangex2 -= (xScale / 4.0F);
myApp.rangey1 += (yScale / 4.0F);
myApp.rangey2 -= (yScale / 4.0F);
break;
case KEY_NUM7:
myApp.numIterations-=4;
if(myApp.numIterations < 2) myApp.numIterations = 2;
break;
case KEY_NUM9:
myApp.numIterations+=4;
break;
default:
gotKey = false;
}
}
if(gotAction || gotKey)
myApp.RenderMandelbrot();
}
以上类中唯一相关的函数是 paint
函数,该函数在每次需要重绘屏幕时自动调用,以及 keyPressed
函数,该函数在用户按下键盘上的任何键时被调用。
请注意,按下向上/向下/向左/向右按钮如何导致 x 和 y 范围被平移以模拟滚动效果,“1”和“3”键具有缩放效果。还对“7”和“9”键进行了编程,使其分别减小和增加迭代次数 4。在按下任何键后,通过再次调用 RenderMandelbrot()
来重绘图形。
测试应用
大概就是这样了。下一步是测试应用程序的运行方式。如果我们从 Java ME SDK 的 IDE 运行该应用程序,它会自动启动默认模拟器并加载该应用程序。

在模拟器上似乎运行正常!现在如何将其放到实际的手机上?构建应用程序会生成两个文件:一个 *.JAR 文件,即实际的应用程序,以及一个 *.JAD 文件,这是一个包含应用程序某些描述(例如作者、版权和 URL)的文本文件。但是我们用这些文件做什么呢?
将 Java 应用程序加载到手机上有几种不同的方法
- 从网上下载
- 通过蓝牙下载
- 使用 Java 应用程序加载器下载(仅限摩托罗拉手机)
- 从内存卡加载
- 直接破解手机
我很幸运拥有一款摩托罗拉 RAZR V3xx,它有一个 microSD 插槽。我惊讶于将它安装在这款手机上并运行是多么容易。以下是将 Java 应用程序安装到这款手机上(可能还有其他接受 microSD 卡的类似摩托罗拉型号)所需的所有步骤
- 将 microSD 卡插入手机后,用 USB 数据线将您的计算机连接到手机(它将被映射为磁盘驱动器)
- 将应用程序的 .JAR 和 .JAD 文件复制到 */mobile/kjava* 目录
- 断开手机的 USB 数据线
- 在手机上,转到菜单 -> 我的东西 -> 游戏和应用程序 -> [安装新应用程序],然后从列表中选择您的应用程序
- 就是这样!该应用程序现在将安装在手机上!
从网上安装
如果您没有允许您从 microSD 卡安装应用程序的手机,您可以通过手机从网上下载该应用程序。当然,前提是您的手机具有有效的帐户并支持网络访问。您可能还需要为数据传输支付几美分,具体取决于您从运营商购买的无线套餐。哦,当然,您的手机必须支持 CLDC 1.1(1.0 不支持浮点运算)和 MIDP 2.0,大多数现代手机都支持。
现在,如果您想通过网络将您编写的应用程序加载到手机上,您需要访问一个 Web 服务器,您可以在该服务器上放置您的应用程序以下载。以我的为例,我将使用我的 Web 服务器 dmitrybrant.com
,并将应用程序放在一个子目录中,如下所示
http://dmitrybrant.com/jme/Mandelbrot.jad
我将 *.JAR 和 *.JAD 文件都放在“jme”子目录中。要下载应用程序,您只需要链接到 *.JAD 文件。**但是等等!** 在应用程序可以从手机下载之前,还有一个关键步骤要做。我们需要编辑此目录中的 *.htaccess 文件,并为我们的文件添加正确的 MIME 类型
AddType text/vnd.sun.j2me.app-descriptor jad
AddType application/java-archive jar
完成此操作后,我们可以尝试将应用程序下载到我们的手机。输入 URL、同意安装未签名应用程序并等待安装完成大约一分钟后……瞧

记录在案,上面的屏幕在我 V3xx 上大约花了 20 秒才绘制完成。您的手机可能会明显慢(或快,但可能不会)。所以请准备好等待一会儿才能完成绘制。或者继续优化代码以使用纯整数数学!(如果您这样做了,请告诉我!)
历史
- 2009 年 7 月 14 日:初始修订