Introduction
Welcome to the Enigma Puzzle App – a game as difficult as the Rubik's cube.
A few weeks ago, I posted an article about a game I've written with C#. This article here reports on the porting of this program to the Android platform. You can read the original CodeProject article about the game - EnigmaPuzzle. There you can find information about the game and how it is implemented with C#. I will not repeat that here.
Since this is my first Java program with more then 10 lines of code and the first Android App at all, I used a lot of time to Google for examples and tips on how things have to be done in Java and/or on the Android platform. A lot of things are very different from the way I am used to program and I had to find a way to implement it (maybe sometime it's not the right way). Some keywords may be: Activity, Layout, Preferences and SurfaceView. But quite a lot of things are also very similar to C# and .NET. For example, the base syntax of C# looks almost like Java. And the best thing is that the basics to draw graphics to the screen are very similar. There are Bitmaps, Paths and other graphic objects on both systems and they are used in similar ways.
The First Steps
I started my project by installing the Android SDK and Eclipse like it is described in Installing the SDK. After that, I went through the Hello World tutorial (Hello World tutorial) to get a feeling of how the Android framework looks like. I also checked out the Lunar Lander example which can be found at Lunar Lander.
Then I was ready to create my first Android project in Eclipse. Thanks to the ADT plugin for Eclipse, the work with the Android framework is very handy and very well supported. The functions to use Android are integrated in the context menus of Eclipse. So I was able to create the project structure in Eclipse by just using the menu: File-New-Project-Android Project. For the build target, I set Android 2.2, but I think even a lower target would have been possible. Next, I had to define a package name (ch.adiuvaris.enigma) which must be unique if the App will be offered via the Android market. I left the other settings as proposed by the system. After the click on the Finish button, I had a minimal Android project with all the necessary folders and files.
There are a lot of folders but I only worked in two of them. The Java source code files go to the folder src/ch.adiuvaris.enigma and the resource files (layouts and strings) are stored in the folder res and some sub-folders of it.
The Base Classes
Now I started to port some of my base classes from C# to Java and the Android framework. First of all, I had to find the right classes and methods in the framework to reflect some .NET classes and their methods.
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
|
As you can see, there are similar methods and classes. But I had to realize that they do not work in the very same way or that they have different parameters. There are, for example, the C# methods FillPath
to fill the area of a closed path with a certain color and DrawPath
which just draws the border of a path. The Android version of Path
knows only the method drawPath
and it depends on one of the parameters (Paint
) what will be drawn. The class Paint
contains a method (setStyle()
) which defines if the path will be filled (Style.FILL
) or if the border will be drawn (Style.STROKE
).
Sometimes the functionality of classes overlap. For example, the C# classes Brush
, Pen
and Graphics
and the Android classes Paint
and Canvas
offer the same functionality but the methods are spread over the classes. In C#, you have to draw into a Graphics
object whereas in Android, you have to use a Canvas
object. In C#, the settings for antialiasing of the drawing has to be set in the Graphics
object but in Android, it has to be set in the Paint
object.
Even things that seem to be the same may differ. Look at the C# class RectangleF
and the Android class RectF
and their constructors.
Java | C# |
RectangleF r = new RectangleF(6.60254F, 20, 160, 160);
|
RectF r = new RectF(6.60254F, -80, 166.60254F, 80);
|
They look the same but the C# version expects left, top, width and the height of the rectangle and the Android version expects left, top, right and bottom. So I had to do some calculations to get the correct rectangles in the Java code.
The Class Block
To start, I added some files to the folder src/ch.adiuvaris.enigma to reflect some of the classes I programmed for the C# version of the game. The filenames were Block.java, Figure.java and Board.java.
At the beginning, the porting of the class Block
was straight forward as you can see in the following code section:
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);
}
}
...
|
It was also no problem to port the static
members and arrays in the class Block
by using the Android class Paint
instead of the C# classes Brush
and Pen
. Because the Android class Paint
defines the color and if the region will be filled or just outlined.
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;
|
But then came the creation of the graphic parts int the Init()
method and the already mentioned problems of the different constructors for rectangles and there is also a different handling of adding lines and arcs to the Path
objects.
Java | C# |
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);
m_blocks[1].GP.addPath(m_blocks[0].GP);
m_blocks[1].GP.transform(mat120);
|
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));
m_blocks[1].GP.AddPath(m_blocks[0].GP, false);
m_blocks[1].GP.Transform(mat120);
|
In C# I added just some sort of lines together to get the path. Each part has a start point and an endpoint. Look, for example, at the C# methods AddLine(startpoint,endpoint)
or AddArc(rect,startangle,sweep)
. Android, on the other hand, expects that the parts of a path are line segments which starts at the last endpoint. It is therefore not possible to use addArc()
for all parts, but I had to use addArc()
for the first part of the path and then arcTo()
and lineTo()
for the next parts of the path. Fortunately, the parameters of AddArc
and arcTo
are the same (except for the rectangles of course).
The rotations and translations with the Matrix
objects work the same way on both systems. So no problems to port that. To create new Path
objects based on another Path
object with addPath()
was also easy to port, because it works the same way.
The Class Figure
This class was very easy to port, because it doesn't use any special things - except the C# class List
. I replaced that with Java class ArrayList
and had to change the syntax to loop through the array. Some members of the array classes are also different (size()
instead of Count
) and the access of an element at a given index is in Java not possible via the [idx]
syntax and I had to use the get()
method of the array as you can see in the getColorString()
method.
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();
}
}
|
The Class Board
Surprisingly, it was not too hard to port this class - after I removed some methods for the beginning. I added the removed functionality later or implemented it in the BoardView
class.
Methods like initBoard
, newGame
, getColorString
, onResize
and others could be ported straight forward. Even the port of the most complex methods in this class like rotateDisk
, rotate
and turnDisk
were quite easy.
Even in methods like paintDisk
or paintBackground
, I had just to replace a parameter (Canvas
instead of Graphics
) and made some syntax adjustments for 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);
}
}
}
|
Creating a Bitmap
I had to rewrite the methods which create the bitmaps for the board (e.g. createUpperDisk()
). They look really different.
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();
}
|
The member m_upperDisk
is a Bitmap
object. In both languages, the bitmap has to be created for the necessary size. Then both systems need an object to paint to. In C#, it's a Graphics
and in Android, a Canvas
object. The call of PaintDisk()
does the painting. You can find the details in onPaint()
of the class Block
.
In both systems, it is important to free the allocated memory for the bitmaps. In C#, that's done with a simple Dispose()
. In the Java code, I had to use recyle()
.
BoardView an the Main Activity
As soon as I was able to compile the base classes, I wanted to see my board on the Android emulator. And it was not too hard to achieve that after I learned that I need a special kind of view to paint onto the screen. The keyword is SurfaceView
. So I added a new class BoardView
to my project which extends the class 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);
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);
}
}
}
In the constructor, I create a Paint
object that will be used during the painting of the board. The Board
object itself is also created there. After the creation of the board, I can use the bitmaps of the board to paint them to the screen. The bitmap objects may be accessed via the getter methods getLowerDisk()
, getLowerDisk()
and getBackground()
. These methods are used in the onDraw()
method of the view.
The Android method onDraw()
in the code above reflects the C# method OnPaint()
from EnigmaPuzzleDlg.cs and they look very similar as you can see in the following C# code fragment.
protected override void OnPaint(PaintEventArgs e)
{
if (e.ClipRectangle.Width == 0 || m_b == null)
{
return;
}
base.OnPaint(e);
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);
}
}
There was only one single step to see the board on the screen. I had to set BoardView
as the main view for the main activity. Therefore I had to change the generated code in EnigmaPuzzle.java in the following way:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new BoardView(this));
}
The method setContentView
is used to define the view that will be shown if the program starts. After I started the project as an Android Application from the Eclipse context menu, I saw my board. It was small and sat on the top left corner.
Of course, it didn't work the first time I started the App, because of the already mentioned differences in the rectangle and path classes. But after I figured out how the parameter has to be set correctly and how to create the paths, I saw my board.
The Real BoardView Runs in a Thread
Because I had already ported all the rotation methods in the class Board
, it should have been possible to rotate the discs on the screen. But when I tried to turn a disc I only saw the resulting board, but no animation of the turning disc. The problem was that the onDraw()
method of my BoardView
was never called during the calculation of the rotation. A call to invalidate
did not help either.
The solution of the problem was a thread for the BoardView
. Only in a thread, it is possible to redraw the screen during a calculation. So I added another class GameThread
to my project which extends the class Thread
. I found the code for that class (and for the view) in different examples on the web and adapted it for my needs.
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;
}
Canvas c = null;
try {
c = m_view.getHolder().lockCanvas(null);
synchronized (m_view.getHolder()) {
m_view.onDraw(c);
}
} finally {
if (c != null) {
m_view.getHolder().unlockCanvasAndPost(c);
}
}
}
@Override
public void run() {
repaint();
while (m_run) {
try {
Thread.sleep(1000);
} catch (Exception ex) {
}
}
}
}
The main functionality lies in repaint()
. There I get thread safe access to a Canvas
to paint the view. After I created my own canvas with m_view.getHolder().lockCanvas(null)
and asked for a lock on the view with synchronized (m_view.getHolder())
, I could call the onDraw()
method of the view to paint the current bitmaps of the game board. Because no other code may call lockCanvas
it is very important to release that lock with m_view.getHolder().unlockCanvasAndPost(c)
in a finally block.
The class BoardView
has to be changed to use the thread and it must implement the SurfaceHolder.Callback
interface. The interface offers three methods which will be called when the surface has been created, when it has changed or when it will be destroyed. These methods may be used to start and stop the thread.
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;
m_thread.setRunning(false);
while (retry) {
try {
m_thread.join();
retry = false;
m_thread = null;
System.gc();
} catch (InterruptedException e) {
}
}
}
With these changes, the animation was visible and the main work of porting was done.
Using the full screen
Of course, the image of the board was still small and did not fill the whole screen. To correct that, I had to implement the method calcBoard()
in the class BoardView
and to call the method onResize()
of the class Board
. The following code shows the method calcBoard()
which calculates the width and height of the board, so that the board fills the screen and the circles still keep its aspect and also that it doesn't matter if the device is hold landscape or portrait.
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);
}
}
The following three lines of code can be called to create a board for a given game level an resize it so that if fills the screen. They are put together in the method createBoard()
of the class BoardView
.
m_board.initBoard(m_level);
m_board.onResize(BoardWidth, BoardHeight, getWidth());
repaint();
The following screenshot shows the App in the emulator when you start it the first time.
Extending the Main View
In C#, I used a timer to display and update the current standings of a running game. To display the current game standings was the next I wanted to do for the Android App. I started to use Toast
messages, but that was not good at all because they will be shown a certain time and are too slow.
What I needed was a part of the screen where I could write to at any time. And in Android, a part of the screen is always a view. For that, I had to change the main view. It was no longer just a SurfaceView
but a FrameLayout
that holds the SurfaceView
and an additional TextView
for the status text.
I also had to add some sort of timer. That could be done by using a Handler
that will be called after a defined time. The timer will be initialized by a call of the method postDelayed()
. The call needs two parameters, a runnable method refreshStatusText()
and a delay in milliseconds.
public void onCreate(Bundle savedInstanceState) {
...
FrameLayout f = new FrameLayout(this);
m_statusText = new TextView(this);
m_statusText.setPadding(3, 3, 3, 3);
m_view = new BoardView(this);
f.addView(m_view);
f.addView(m_statusText);
setContentView(f);
m_Handler = new Handler();
m_Handler.removeCallbacks(refreshStatusText);
m_Handler.postDelayed(refreshStatusText, 100);
}
The message handler shows the current standings in the TextView
and always calls the postDelayed()
method to initiate the next call to refresh the status text. The text that should be displayed is prepared by the method getStatusText()
in the class BoardView
.
private Runnable refreshStatusText = new Runnable() {
public void run() {
m_statusText.setText(m_view.getStatusText());
m_Handler.postDelayed(this, 1000);
}
};
Points of Interest
To work with the Android framework and Java was something new for me and I'm sure a lot of things in my program could be done more elegant, faster and easier. But I liked the work very much and it was very interesting to learn something new.
If you need more information about the game and how to play it, you find some more information in my article about the C# version of EnigmaPuzzle
which can be found here.
In the following sections, you will find some information on additions I made for the EnigmaPuzzle
for Android. For example, a menu or a preferences activity.
Options Menu
The options menu has to be defined in the main activity. But I implemented the handling of the menu in the BoardView
class.
There are three methods in the activity to create and handle the options menu. You can find the code for them in the following fragment from EnigmaPuzzle.java. The method onPrepareOptionsMenu
can be used to enable and disable menu items for the current state of the program.
@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);
}
You can find the implementation of the real work for the options menu in the class BoardView
. Some of the code will be shown in the following fragment:
public boolean onCreateOptionsMenu(Menu menu) {
MenuItem item;
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) {
if (m_gameState == eState.Active) {
menu.findItem(C_CmdStopGame).setEnabled(true);
menu.findItem(C_CmdNewGame).setEnabled(false);
...
}
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;
}
In the method onCreateOptionsMenu
, you can add menu items to the options menu. Therefore, you need a constant for the command and a string
for the text. As you can see, it is very easy to add an icon to the menu item. The next method is onPrepareOptionsMenu
where you can enable or disable menu items based on the state of the game. The last method is onMenuItemSelected
where you have to handle a selected menu item. In the example, you can see how another activity (preferences activity) may be started.
When you open the menu, it looks like the following screenshot.
Setting Preferences
I have added a second activity for the settings screen. I know (now) that there is a PreferenceActivity
for that. But it was a nice exercise on how to work with a layout and how to access the view-elements from an activity.
In the previous section about the options menu, you can see how the activity will be started when selected in the menu (case C_CmdSettings
).
The preferences screen looks like the following screenshot.
If you add a new activity to a project, you must not forget (as I did) to add this to the AndroidManifest.xml file in your project.
The Help Activity
The help text will be displayed in its own activity (the third one in my project). It is a very simple example of a view that may be scrolled. That is necessary because the text is larger than a screen (on small devices). You can see that in the following screenshot.
C# GraphicsPath.IsVisible()
The GraphicsObject
in .NET offers a method IsVisible()
. This method returns true
if a point lies within the path. I did not find such a method in Android and I therefore implemented it in the class Block
.
public boolean isPointInBlock(PointF p) {
RectF bounds = new RectF();
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;
}
The class Path
offers a method computeBounds()
which I used to check if a point lies in the region of a Path
object. The rest is a simple check for all directions if the point lies in the bounds of the path.
Multiple Languages
If you follow the recommendations and put all your string
s to the resources, then it is very easy to add another locale. The string
s for the default language are saved in the file strings.xml in the folder res/values. To add another language, you just have to add a folder with the short name of the locale in the name. For the string
values in German, I had to add the folder res/values-de. There I put a copy of the file strings.xml and translated the texts - e voila the messages on the screen are in German if the device uses a German locale.
Even the texts in layout files can be placeholders from the file strings.xml. So you don't need multiple layout files for the different languages. You have to use the following syntax for texts in a layout file @string/slTitle
where slTitle
is the name you used in the strings.xml file for the text that should be displayed. In the strings.xml file, there has to be an entry like the following example:
<string name="slTitle">"Enigma General Settings"</string>
iOS Version
Some time ago I've implemented a version of EnigmaPuzzle for Apple devices. The game can be found for free in the App Store (see link at the top of the article). I added the full source code of the iOS implementation to the article. It is an XCode project written in Objective-C using the GameKit from Apple.
The code of the iOS version can't be compared to the other versions ot the game because it uses a GameKit to handle the presentation of the screens and the handling of user input. On the other hand the drawing of the board parts is allmost the same, just the handling of coordinates and sizes is different.
History
- Version 1.1 - 16.11.2014 - added iOS version
- Version 1.0 - 16.12.2011