EnigmaPuzzle for Android






4.98/5 (55投票s)
Enigma Puzzle for Android – 一款与魔方一样困难的游戏
- 下载 Android 版本源代码 - 497.68 KB
- Google Play 商店中 EnigmaPuzzle for Android 设备链接
- 下载 iOS 版本源代码 - 1.4 MB
- App Store 中 EnigmaPuzzle for Apple 设备链接
引言
欢迎来到 Enigma Puzzle App – 一款与魔方一样难的游戏。
几周前,我发表了一篇关于我用 C# 编写的游戏的文章。本文将报道将该程序移植到 Android 平台的情况。您可以阅读原始的 CodeProject 文章,了解这款游戏 - EnigmaPuzzle。在那里您可以找到关于游戏及其 C# 实现的信息。我将不再在此重复。
由于这是我第一个代码行数超过 10 行的 Java 程序,也是我的第一个 Android App,我花费了大量时间谷歌搜索关于 Java 和/或 Android 平台上的实现方法和技巧。很多事情与我习惯的编程方式非常不同,我不得不找到一种实现它的方法(也许有时不是正确的方法)。一些关键词可能是:Activity、Layout、Preferences 和 SurfaceView。但很多事情也与 C# 和 .NET 非常相似。例如,C# 的基本语法看起来几乎和 Java 一样。而最棒的是,将图形绘制到屏幕上的基础知识非常相似。两个系统上都有Bitmap、Path 等图形对象,并且它们的使用方式也相似。
第一步
我通过安装 Android SDK 和 Eclipse 来开始我的项目,就像在 安装 SDK 中描述的那样。之后,我通过了“Hello World”教程(Hello World 教程)来感受 Android 框架的模样。我还查看了可以在 Lunar Lander 找到的 Lunar Lander 示例。
然后,我准备在 Eclipse 中创建我的第一个 Android 项目。得益于 Eclipse 的 ADT 插件,使用 Android 框架的工作非常方便且支持良好。使用 Android 的功能集成在 Eclipse 的上下文菜单中。因此,我只需使用菜单:File-New-Project-Android Project 即可在 Eclipse 中创建项目结构。对于构建目标,我设置了 Android 2.2,但我认为即使是较低的目标也可能实现。接下来,我必须定义一个包名(ch.adiuvaris.enigma),如果 App 将通过 Android 市场提供,该包名必须是唯一的。我保留了系统建议的其他设置。点击Finish 按钮后,我得到了一个最小化的 Android 项目,其中包含所有必需的文件夹和文件。
有很多文件夹,但我只在其中两个工作。Java 源代码文件放在src/ch.adiuvaris.enigma 文件夹中,资源文件(布局和字符串)存储在res 文件夹及其子文件夹中。
基本类
现在我开始将我的 C# 基本类移植到 Java 和 Android 框架。首先,我必须在框架中找到正确的类和方法来反映 .NET 类及其方法。
Java | C# |
---|---|
Path
drawPath()
addArc(), arcTo()
moveTo(), lineTo()
addPath()
transform()
RectF
Matrix
setRotate()
setTranslate()
Paint, Canvas
setAntiAlias()
Bitmap
|
GraphicsPath
FillPath(), DrawPath()
AddArc()
AddLine()
AddPath()
Transform()
RectangleF
Matrix
RotateAt()
Translate()
Brush, Pen, Graphics
SmoothingMode()
Bitmap
|
正如您所见,有相似的方法和类。但是,我不得不意识到它们的工作方式并不完全相同,或者它们的参数不同。例如,C# 方法 FillPath
用于用特定颜色填充闭合路径的区域,而 DrawPath
只绘制路径的边框。Android 版的 Path
只知道 drawPath
方法,具体绘制什么取决于其中一个参数(Paint)。Paint
类包含一个方法(setStyle()
),该方法定义路径是将被填充(Style.FILL
)还是只绘制边框(Style.STROKE
)。
有时类的功能会重叠。例如,C# 类 Brush
、Pen
和 Graphics
以及 Android 类 Paint
和 Canvas
提供相同的功能,但方法分布在不同的类中。在 C# 中,您必须绘制到 Graphics
对象,而在 Android 中,您必须使用 Canvas
对象。在 C# 中,绘图的抗锯齿设置必须在 Graphics
对象中设置,而在 Android 中,则必须在 Paint
对象中设置。
即使看起来相同的东西也可能不同。看看 C# 类 RectangleF
和 Android 类 RectF
及其构造函数。
Java | C# |
---|---|
RectangleF r = new RectangleF(6.60254F, 20, 160, 160);
|
RectF r = new RectF(6.60254F, -80, 166.60254F, 80);
|
它们看起来一样,但 C# 版本需要矩形的left、top、width 和height,而 Android 版本需要left、top、right 和bottom。因此,我不得不做一些计算才能在 Java 代码中获得正确的矩形。
Block 类
首先,我在src/ch.adiuvaris.enigma 文件夹中添加了一些文件来反映我为游戏的 C# 版本编写的一些类。文件名是Block.java、Figure.java 和Board.java。
起初,Block
类的移植很简单,正如您在以下代码段中所见
Java | C# |
---|---|
public class Block {
public Path GP;
public int Col;
public int Edge;
public Block() {
GP = new Path();
Col = -1;
Edge = -1;
}
public void onPaint(Canvas canvas) {
if (Col >= 0 &&
Col < m_colors.length) {
canvas.drawPath
(GP, m_colors[Col]);
}
if (Edge >= 0 &&
Edge < m_pens.length) {
canvas.drawPath
(GP, m_pens[Edge]);
}
}
...
|
public class Block
{
public GraphicsPath GP { get; set; }
public int Col { get; set; }
public int Edge { get; set; }
public Block()
{
GP = new GraphicsPath();
Col = -1;
Edge = -1;
}
public void Paint(Graphics g)
{
if (Col >= 0 && Col < m_colors.Count())
{
g.FillPath(m_colors[Col], GP);
}
if (Edge >= 0 && Edge < m_pens.Count())
{
g.DrawPath(m_pens[Edge], GP);
}
}
...
|
通过使用 Android 类 Paint
而不是 C# 类 Brush
和 Pen
,移植 Block
类中的 static
成员和数组也没有问题。因为 Android 类 Paint
定义了颜色以及区域是填充还是仅描边。
Java | C# |
---|---|
private static int[][] m_games = new int[][] {
...
};
private static Block[] m_blocks = null;
private static Paint[] m_colors = null;
private static Paint[] m_pens = null;
|
private static int[,] m_games = new int[11, 62] {
...
};
private static Block[] m_blocks = null;
private static Brush[] m_colors = null;
private static Pen[] m_pens = null;
|
但是,随后在 Init()
方法中创建图形部分时,遇到了前面提到的关于矩形不同构造函数的问题,并且向 Path
对象添加线条和弧形的处理方式也不同。
Java | C# |
---|---|
// Create the first sub-part of the first stone
m_blocks[0].GP.addArc(new RectF
(6.60254F, 20, 166.60254F, 180), 180, 21.31781F);
m_blocks[0].GP.arcTo(new RectF
(-80, 70, 80, 230), 278.68219F, 21.31781F);
m_blocks[0].GP.lineTo(28.86751F, 100);
m_blocks[0].GP.close();
Matrix mat120 = new Matrix();
mat120.setRotate(120.0F, 28.86751F, 100);
// The second sub-part of the first stone
// (rotate the first by 120 degrees)
m_blocks[1].GP.addPath(m_blocks[0].GP);
m_blocks[1].GP.transform(mat120);
|
// Create the first sub-part of the
// first stone
m_blocks[0].GP.AddArc(new RectangleF
(6.60254F, 20, 160, 160), 180, 21.31781F);
m_blocks[0].GP.AddArc(new RectangleF
(-80, 70, 160, 160), 278.68219F, 21.31781F);
m_blocks[0].GP.AddLine(new PointF(40.00000F, 80.71797F),
new PointF(28.86751F, 100));
m_blocks[0].GP.AddLine(new PointF(28.86751F, 100),
new PointF(6.60254F, 100F));
Matrix mat120 = new Matrix();
mat120.RotateAt(120.0F, new PointF(28.86751F, 100));
// The second sub-part of the
// first stone (rotate the first
// by 120 degrees)
m_blocks[1].GP.AddPath(m_blocks[0].GP, false);
m_blocks[1].GP.Transform(mat120);
|
在 C# 中,我只需将一些线条组合起来即可得到路径。每个部分都有一个起点和一个终点。例如,看看 C# 方法 AddLine(startpoint,endpoint)
或 AddArc(rect,startangle,sweep)
。另一方面,Android 需要路径的各部分是从上一个端点开始的线段。因此,不能对所有部分都使用 addArc()
,但我必须对路径的第一部分使用 addArc()
,然后对路径的下一部分使用 arcTo()
和 lineTo()
。幸运的是,AddArc
和 arcTo
的参数相同(当然,矩形除外)。
使用 Matrix
对象进行的旋转和变换在两个系统上工作方式相同。所以移植没有问题。通过 addPath()
基于另一个 Path
对象创建新的 Path
对象也很容易移植,因为它的工作方式相同。
Figure 类
这个类非常容易移植,因为它不使用任何特殊的东西——除了 C# 类 List
。我用 Java 类 ArrayList
替换了它,并且不得不改变遍历数组的语法。数组类的一些成员也不同(size() 而不是 Count),并且在 Java 中无法通过 [idx]
语法访问给定索引处的元素,我必须使用数组的 get()
方法,正如您在 getColorString()
方法中看到的。
Java | C# |
---|---|
public class Figure {
private ArrayList<Block> m_blocks;
private int m_orient;
public Figure() {
m_blocks = new ArrayList<Block>();
m_orient = 0;
}
public void incOrient() {
m_orient++;
m_orient %= 3;
}
public void decOrient() {
m_orient--;
if (m_orient < 0) {
m_orient += 3;
}
}
public void addBlock(int nr) {
m_blocks.add(Block.getBlocks()[nr]);
}
public void onPaint(Canvas canvas) {
for (Block block : m_blocks) {
block.onPaint(canvas);
}
}
public void transform(Matrix mat) {
for (Block block : m_blocks) {
block.GP.transform(mat);
}
}
public String getColorString() {
StringBuilder sb = new StringBuilder();
for (int i = 0;
i < m_blocks.size(); i++) {
int idx = (i + m_orient) % 3;
sb.append(Integer.toString
(m_blocks.get(idx).Col));
}
return sb.toString();
}
}
|
public class Figure
{
private List<block> m_blocks;
private int m_orient;
public Figure()
{
m_blocks = new List<block>();
m_orient = 0;
}
public void IncOrient()
{
m_orient++;
m_orient %= 3;
}
public void DecOrient()
{
m_orient--;
if (m_orient < 0)
{
m_orient += 3;
}
}
public void AddBlock(int nr)
{
m_blocks.Add(Block.Blocks[nr]);
}
public void Paint(Graphics g)
{
foreach (Block block in m_blocks)
{
block.Paint(g);
}
}
public void Transform(Matrix mat)
{
foreach (Block block in m_blocks)
{
block.GP.Transform(mat);
}
}
public string GetColorString()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < m_blocks.Count; i++)
{
int idx = (i + m_orient) % 3;
sb.Append(m_blocks[idx].Col.ToString());
}
return sb.ToString();
}
}
|
Board 类
令人惊讶的是,移植这个类并不太难——在我最初删除了一些方法之后。我后来添加了删除的功能,或者在 BoardView
类中实现了它。
像 initBoard
、newGame
、getColorString
、onResize
等方法都可以直接移植。即使是这个类中最复杂的方法,如 rotateDisk
、rotate
和 turnDisk
,也相当容易移植。
即使在 paintDisk
或 paintBackground
等方法中,我也只需替换一个参数(Canvas 而不是 Graphics)并对 Java 进行了一些语法调整。
Java | C# |
---|---|
private void paintDisk(Canvas canvas, eDisc disc) {
if (disc == eDisc.UpperDisc) {
for (int i = 0; i < 6; i++) {
m_bones[m_upperBones[i]].
onPaint(canvas);
m_stones[m_upperStones[i]].
onPaint(canvas);
}
} else {
for (int i = 0; i < 6; i++) {
m_bones[m_lowerBones[i]].
onPaint(canvas);
m_stones[m_lowerStones[i]].
onPaint(canvas);
}
}
}
|
private void PaintDisk(Graphics g, eDisc disc)
{
if (disc == eDisc.eUpperDisc)
{
for (int i = 0; i < 6; i++)
{
m_bones[m_upperBones[i]].Paint(g);
m_stones[m_upperStones[i]].Paint(g);
}
}
else
{
for (int i = 0; i < 6; i++)
{
m_bones[m_lowerBones[i]].Paint(g);
m_stones[m_lowerStones[i]].Paint(g);
}
}
}
|
创建 Bitmap
我不得不重写创建棋盘位图的方法(例如 createUpperDisk()
)。它们看起来确实很不一样。
Java | C# |
---|---|
private void createUpperDisk() {
if (m_upperDisk != null) {
m_upperDisk.recycle();
m_upperDisk = null;
}
m_upperDisk = Bitmap.createBitmap
(m_w, m_h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas();
canvas.setBitmap(m_upperDisk);
paintDisk(canvas, eDisc.UpperDisc);
canvas = null;
System.gc();
}
|
private void CreateUpperDisk()
{
if (m_upperDisk != null)
{
m_upperDisk.Dispose();
}
m_upperDisk = new Bitmap(m_w, m_h);
Graphics g = Graphics.FromImage(m_upperDisk);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
PaintDisk(g, eDisc.eUpperDisc);
g.Dispose();
}
|
成员 m_upperDisk
是一个 Bitmap
对象。在两种语言中,都需要为必要的尺寸创建位图。然后两个系统都需要一个用于绘制的对象。在 C# 中是 Graphics
,而在 Android 中是 Canvas
对象。调用 PaintDisk()
进行绘制。您可以在 Block
类的 onPaint()
中找到详细信息。
在两个系统中,释放位图的分配内存都很重要。在 C# 中,这通过一个简单的 Dispose()
来完成。在 Java 代码中,我不得不使用 recyle()
。
BoardView 和主 Activity
一旦我能够编译基本类,我就想在 Android 模拟器上看到我的棋盘。在了解到我需要一种特殊的视图来绘制到屏幕上后,这并不难实现。关键词是 SurfaceView
。因此,我在项目中添加了一个新类 BoardView
,它扩展了 SurfaceView
类。
public class BoardView extends SurfaceView {
private Context m_ctx = null;
private Paint m_paint = null;
public BoardView(Context context) {
super(context);
m_ctx = context;
m_paint = new Paint();
m_paint.setColor(Color.BLACK);
m_paint.setAntiAlias(true);
m_board = new Board();
m_board.initBoard(9);
}
@Override
public void onDraw(Canvas canvas) {
if (m_board == null || canvas == null) {
return;
}
super.onDraw(canvas);
// Draw the bitmaps in the right order
canvas.drawBitmap(m_board.getBackground(), 0, 0, m_paint);
if (m_board.getRotDisk() == Board.eDisc.UpperDisc) {
canvas.drawBitmap(m_board.getLowerDisk(), 0, 0, m_paint);
canvas.drawBitmap(m_board.getUpperDisk(), 0, 0, m_paint);
} else {
canvas.drawBitmap(m_board.getUpperDisk(), 0, 0, m_paint);
canvas.drawBitmap(m_board.getLowerDisk(), 0, 0, m_paint);
}
}
}
在构造函数中,我创建了一个将在绘制棋盘时使用的 Paint
对象。Board
对象本身也在那里创建。创建棋盘后,我就可以使用棋盘的位图将其绘制到屏幕上。可以通过 getter 方法 getLowerDisk()
、getLowerDisk()
和 getBackground()
访问位图对象。这些方法在视图的 onDraw()
方法中使用。
上面的代码中的 Android 方法 onDraw()
对应于 EnigmaPuzzleDlg.cs 中的 C# 方法 OnPaint()
,正如您在以下 C# 代码片段中看到的,它们看起来非常相似。
protected override void OnPaint(PaintEventArgs e)
{
if (e.ClipRectangle.Width == 0 || m_b == null)
{
return;
}
base.OnPaint(e);
// Optimize to repaint only the necessary disk
e.Graphics.DrawImageUnscaled(m_b.Background, 0, 0);
if (m_b.RotDisk == Board.eDisc.eUpperDisc)
{
e.Graphics.DrawImageUnscaled(m_b.LowerDisk, 0, 0);
e.Graphics.DrawImageUnscaled(m_b.UpperDisk, 0, 0);
}
else
{
e.Graphics.DrawImageUnscaled(m_b.UpperDisk, 0, 0);
e.Graphics.DrawImageUnscaled(m_b.LowerDisk, 0, 0);
}
}
只用了一个简单的步骤就可以在屏幕上看到棋盘。我必须将 BoardView
设置为主 Activity 的主视图。因此,我必须将 EnigmaPuzzle.java 中的生成代码更改为以下方式
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new BoardView(this));
}
setContentView
方法用于定义程序启动时显示的视图。从 Eclipse 上下文菜单中将项目作为 Android 应用程序启动后,我看到了我的棋盘。它很小,位于左上角。
当然,它第一次启动 App 时并没有工作,因为之前提到的矩形和路径类的差异。但在我弄清楚参数如何正确设置以及如何创建路径后,我就看到了我的棋盘。
真实的 BoardView 在一个线程中运行
因为我已经移植了 Board
类中的所有旋转方法,所以应该可以在屏幕上旋转圆盘。但是当我尝试旋转一个圆盘时,我只看到了结果棋盘,但没有旋转圆盘的动画。问题是,在计算旋转过程中,我的 BoardView
的 onDraw()
方法从未被调用。调用 invalidate
也没有帮助。
问题的解决方案是为 BoardView
创建一个线程。只有在线程中,才可以在计算过程中重绘屏幕。因此,我在项目中添加了另一个类 GameThread
,它扩展了 Thread
类。我从网络上的不同示例中找到了该类的代码(以及视图的代码),并根据我的需求进行了调整。
public class GameThread extends Thread {
private BoardView m_view;
private boolean m_run = false;
public GameThread(BoardView view) {
m_view = view;
}
public void setRunning(boolean run) {
m_run = run;
}
public void repaint() {
if (!m_run) {
return;
}
// Get the canvas and lock it
Canvas c = null;
try {
c = m_view.getHolder().lockCanvas(null);
synchronized (m_view.getHolder()) {
m_view.onDraw(c);
}
} finally {
// Make sure we don't leave the Surface in an inconsistent state
if (c != null) {
m_view.getHolder().unlockCanvasAndPost(c);
}
}
}
@Override
public void run() {
repaint();
while (m_run) {
try {
Thread.sleep(1000);
} catch (Exception ex) {
}
}
}
}
主要功能位于 repaint()
中。在那里,我可以线程安全地访问 Canvas
来绘制视图。在我使用 m_view.getHolder().lockCanvas(null)
创建了自己的 canvas 并使用 synchronized (m_view.getHolder())
请求视图的锁定后,我就可以调用视图的 onDraw()
方法来绘制当前游戏棋盘的位图。因为没有其他代码可以调用 lockCanvas
,所以在 finally 块中用 m_view.getHolder().unlockCanvasAndPost(c)
释放该锁非常重要。
BoardView
类必须进行更改才能使用该线程,并且必须实现 SurfaceHolder.Callback
接口。当 surface 被创建、更改或销毁时,将调用此接口的三个方法。这些方法可用于启动和停止线程。
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
public void surfaceCreated(SurfaceHolder holder) {
if (m_thread == null) {
m_thread = new GameThread(this);
}
m_thread.setRunning(true);
m_thread.start();
}
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
// Stop the thread
m_thread.setRunning(false);
while (retry) {
try {
m_thread.join();
retry = false;
m_thread = null;
System.gc();
} catch (InterruptedException e) {
// we will try it again and again...
}
}
}
通过这些更改,动画变得可见,并且移植的主要工作也完成了。
使用全屏
当然,棋盘图像仍然很小,没有填满整个屏幕。为了纠正这个问题,我不得不实现 BoardView
类中的 calcBoard()
方法,并调用 Board
类的 onResize()
方法。下面的代码显示了 calcBoard()
方法,该方法计算棋盘的宽度和高度,以便棋盘填满屏幕并且圆圈保持其纵横比,并且无论设备是横屏还是竖屏都可以显示。
private void calcBoard(int w, int h) {
if (h > w) {
BoardWidth = w;
BoardHeight = (int) (h * Board.C_BoardWidth / Board.C_BoardHeight);
} else {
BoardHeight = h;
BoardWidth = (int) (w * Board.C_BoardHeight / Board.C_BoardWidth);
}
}
以下三行代码可以用于创建给定游戏级别的棋盘并调整大小,使其填满屏幕。它们被组合在 BoardView
类的 createBoard()
方法中。
m_board.initBoard(m_level);
m_board.onResize(BoardWidth, BoardHeight, getWidth());
repaint();
下图显示了 App 在模拟器中首次启动时的样子。
扩展主视图
在 C# 中,我使用一个计时器来显示和更新正在运行的游戏的当前排名。显示当前游戏排名是我接下来想为 Android App 做的事情。我开始使用 Toast
消息,但这一点也不好,因为它们会显示一段时间并且太慢。
我需要屏幕的一部分,我可以随时向其中写入内容。而在 Android 中,屏幕的一部分始终是一个视图。为此,我不得不更改主视图。它不再只是一个 SurfaceView
,而是一个 FrameLayout
,它包含 SurfaceView
和一个用于状态文本的附加 TextView
。
我还必须添加一种计时器。这可以通过使用一个 Handler
来完成,该 Handler
将在定义的时间后被调用。计时器将通过调用 postDelayed()
方法来初始化。该调用需要两个参数:一个可运行的方法 refreshStatusText()
和一个延迟(以毫秒为单位)。
public void onCreate(Bundle savedInstanceState) {
...
// Create the main layout
FrameLayout f = new FrameLayout(this);
// Create the TextView for status text
m_statusText = new TextView(this);
m_statusText.setPadding(3, 3, 3, 3);
// Create the surface view for the board
m_view = new BoardView(this);
// Add the two elements to the layout
f.addView(m_view);
f.addView(m_statusText);
// Set the layout as view for the activity
setContentView(f);
// Add a handler for the refresh of the status text
m_Handler = new Handler();
m_Handler.removeCallbacks(refreshStatusText);
m_Handler.postDelayed(refreshStatusText, 100);
}
消息处理程序在 TextView
中显示当前排名,并始终调用 postDelayed()
方法来启动下一次调用以刷新状态文本。应该显示的文本由 BoardView
类中的 getStatusText()
方法准备。
private Runnable refreshStatusText = new Runnable() {
public void run() {
m_statusText.setText(m_view.getStatusText());
// Start the next refresh in a second
m_Handler.postDelayed(this, 1000);
}
};
关注点
使用 Android 框架和 Java 对我来说是全新的,我敢肯定我的程序中很多东西都可以做得更优雅、更快、更容易。但我非常喜欢这项工作,学习新东西非常有趣。
如果您需要更多关于游戏及其玩法的信息,您可以在我关于 C# 版 EnigmaPuzzle
的文章中找到更多信息,该文章可在 此处 找到。
在接下来的部分中,您将找到有关我为 Android 版 EnigmaPuzzle
所做的一些补充的信息。例如,菜单或首选项 Activity。
选项菜单
选项菜单必须在主 Activity 中定义。但我是在 BoardView
类中实现的菜单处理。
Activity 中有三个方法用于创建和处理选项菜单。您可以在 EnigmaPuzzle.java 的以下代码片段中找到它们的代码。onPrepareOptionsMenu
方法可用于启用和禁用当前程序状态的菜单项。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
return m_view.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
m_view.onPrepareOptionsMenu(menu);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (m_view.onMenuItemSelected(featureId, item)) {
return true;
}
return super.onMenuItemSelected(featureId, item);
}
您可以在以下代码片段中找到选项菜单的实际工作实现,位于 BoardView
类中
public boolean onCreateOptionsMenu(Menu menu) {
MenuItem item;
// Add the menu items
item = menu.add(0, C_CmdNewGame, 0, R.string.menuNewGame);
item.setIcon(R.drawable.newgame);
item = menu.add(0, C_CmdStopGame, 0, R.string.menuStopGame);
item.setIcon(R.drawable.resetgame);
...
return true;
}
public void onPrepareOptionsMenu(Menu menu) {
// Check which menu items have to be disabled
if (m_gameState == eState.Active) {
// The game is running so disable all but the stop item
menu.findItem(C_CmdStopGame).setEnabled(true);
menu.findItem(C_CmdNewGame).setEnabled(false);
...
}
/**
* Handles the selected menu item
*/
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch (item.getItemId()) {
case C_CmdNewGame:
newGame();
return true;
....
case C_CmdSettings: {
Intent intent = new Intent();
intent.setClass(m_ctx, GamePrefs.class);
m_ctx.startActivity(intent);
return true;
}
}
return false;
}
在 onCreateOptionsMenu
方法中,您可以向选项菜单添加菜单项。为此,您需要一个命令常量和一个用于文本的 string
。如您所见,为菜单项添加图标非常简单。下一个方法是 onPrepareOptionsMenu
,您可以在其中根据游戏状态启用或禁用菜单项。最后一个方法是 onMenuItemSelected
,您必须在那里处理选定的菜单项。在示例中,您可以看到如何启动另一个 Activity(首选项 Activity)。
当您打开菜单时,它看起来像下面的截图。
设置首选项
我为设置屏幕添加了第二个 Activity。我知道(现在)有一个 PreferenceActivity
可供使用。但这对于如何使用布局和如何从 Activity 访问视图元素来说是一次很好的练习。
在前面关于选项菜单的部分,您可以看到当在菜单中选择时(case C_CmdSettings
)如何启动 Activity。
首选项屏幕看起来像下面的截图。
如果您向项目添加新的 Activity,您(像我一样)不能忘记将其添加到项目中的AndroidManifest.xml 文件。
帮助 Activity
帮助文本将在其自己的 Activity 中显示(我项目中的第三个)。它是一个非常简单的可滚动视图示例。这是必需的,因为文本比屏幕(在小型设备上)大。您可以在下面的截图上看到这一点。
C# GraphicsPath.IsVisible()
.NET 中的 GraphicsObject
提供了一个 IsVisible()
方法。如果一个点位于路径内,此方法返回 true
。我在 Android 中没有找到这样的方法,因此我在 Block
类中实现了它。
public boolean isPointInBlock(PointF p) {
RectF bounds = new RectF();
// Get the bounds of the path and check the point against it
GP.computeBounds(bounds, true);
if (p.x >= bounds.left && p.x <= bounds.right &&
p.y >= bounds.top && p.y <= bounds.bottom) {
return true;
}
return false;
}
Path
类提供了一个 computeBounds()
方法,我用它来检查一个点是否位于 Path
对象的区域内。其余的是对所有方向的简单检查,看点是否位于路径的边界内。
多种语言
如果您遵循建议并将所有 string
放入资源中,那么添加另一种区域设置将非常容易。默认语言的 string
保存在res/values 文件夹中的strings.xml 文件中。要添加另一种语言,您只需添加一个文件夹,并在名称中包含区域设置的简短名称。对于德语中的 string
值,我必须添加res/values-de 文件夹。在那里我放了一个strings.xml 文件的副本并翻译了文本 - 瞧,如果设备使用德语区域设置,屏幕上的消息就是德语。
即使是布局文件中的文本也可以是strings.xml 文件中的占位符。因此,您不需要为不同的语言使用多个布局文件。您必须在布局文件中为文本使用以下语法 @string/slTitle
,其中 slTitle
是您在strings.xml 文件中为要显示的文本使用的名称。在strings.xml 文件中,必须有一个如下面的示例条目
<string name="slTitle">"Enigma General Settings"</string>
iOS 版本
很久以前,我为 Apple 设备实现了一个 EnigmaPuzzle 版本。您可以在 App Store 中免费找到这款游戏(请参阅文章顶部的链接)。我将 iOS 实现的完整源代码添加到了文章中。它是一个使用 Apple 的 GameKit 编写的 Objective-C XCode 项目。
iOS 版本的代码不能与其他版本的游戏进行比较,因为它使用 GameKit 来处理屏幕的呈现和用户输入的处理。另一方面,棋盘部分的绘制几乎相同,只是坐标和尺寸的处理不同。
历史
- 版本 1.1 - 2014 年 11 月 16 日 - 添加了 iOS 版本
- 版本 1.0 - 2011 年 12 月 16 日