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 日


