65.9K
CodeProject 正在变化。 阅读更多。
Home

EnigmaPuzzle for Android

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (55投票s)

2011年12月22日

CPOL

16分钟阅读

viewsIcon

100588

downloadIcon

12716

Enigma Puzzle for Android – 一款与魔方一样困难的游戏

引言

欢迎来到 Enigma Puzzle App – 一款与魔方一样难的游戏。

几周前,我发表了一篇关于我用 C# 编写的游戏的文章。本文将报道将该程序移植到 Android 平台的情况。您可以阅读原始的 CodeProject 文章,了解这款游戏 - EnigmaPuzzle。在那里您可以找到关于游戏及其 C# 实现的信息。我将不再在此重复。

由于这是我第一个代码行数超过 10 行的 Java 程序,也是我的第一个 Android App,我花费了大量时间谷歌搜索关于 Java 和/或 Android 平台上的实现方法和技巧。很多事情与我习惯的编程方式非常不同,我不得不找到一种实现它的方法(也许有时不是正确的方法)。一些关键词可能是:ActivityLayoutPreferencesSurfaceView。但很多事情也与 C# 和 .NET 非常相似。例如,C# 的基本语法看起来几乎和 Java 一样。而最棒的是,将图形绘制到屏幕上的基础知识非常相似。两个系统上都有BitmapPath 等图形对象,并且它们的使用方式也相似。

第一步

我通过安装 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# 类 BrushPenGraphics 以及 Android 类 PaintCanvas 提供相同的功能,但方法分布在不同的类中。在 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# 版本需要矩形的lefttopwidthheight,而 Android 版本需要lefttoprightbottom。因此,我不得不做一些计算才能在 Java 代码中获得正确的矩形。

Block 类

首先,我在src/ch.adiuvaris.enigma 文件夹中添加了一些文件来反映我为游戏的 C# 版本编写的一些类。文件名是Block.javaFigure.javaBoard.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# 类 BrushPen,移植 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()。幸运的是,AddArcarcTo 的参数相同(当然,矩形除外)。

使用 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 类中实现了它。

initBoardnewGamegetColorStringonResize 等方法都可以直接移植。即使是这个类中最复杂的方法,如 rotateDiskrotateturnDisk,也相当容易移植。

即使在 paintDiskpaintBackground 等方法中,我也只需替换一个参数(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 类中的所有旋转方法,所以应该可以在屏幕上旋转圆盘。但是当我尝试旋转一个圆盘时,我只看到了结果棋盘,但没有旋转圆盘的动画。问题是,在计算旋转过程中,我的 BoardViewonDraw() 方法从未被调用。调用 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 日
© . All rights reserved.