Tap each tile to move it.
A tile that is landlocked will not move.
The tiles are separated by a
one-pixel white hairline;
see the
margin
field of class
PuzzleView
.
The intelligence of the game is in the big white
PuzzleView
object.
Each
TileView
object does little more than hold a pair of numbers,
its current row and column numbers.
The
PuzzleView
is a
ViewGroup
(i.e., class
PuzzleView
is a subclass of class
ViewGroup
),
and the
TileView
s
are its children (subviews).
See
getChildCount
and
getChildAt
.
Note that class
PuzzleView
has no
onDraw
method,
even though it is a subclass of class
View
.
That’s because all it has to do is display a
white
background
and display its own subviews.
Class
TileView
has no
onDraw
either.
MainActivity.java
.
PuzzleView.java
.
Class
PuzzleView
is a subclass of class
RelativeLayout
.
It had to be a subclass of some class of
ViewGroup
because I inserted subviews into it with the
addView
method of class
ViewGroup
.
The constructor has a two-dimensional array of
int
.
onLayout
is called automatically as soon as it is safe to call
getWidth
and
getHeight
.
TileView.java
.
Class
TileView
is a subclass of class
ImageView
.
It holds a pair of numbers (row and col),
and,
when touched,
passes itself to the
wasTouched
method of the
PuzzleView
.
activity_main.xml
:
unused and ignored.
strings.xml
.
.png
files in the
app/res/drawable
directory.
The first digit is the row number (top to bottom),
the second digit is the column number (left to right).
Each image is 300 × 394 pixels.
AndroidManifest.xml
.
build.gradle
(Module: app).
I got the original image from the Obama
Wikipedia
article.
I split it into nine smaller images with
ImageSplitter
and stored them on my Desktop.
I wanted to name them
00.png
,
01.png
,
etc.,
but I couldn’t do that because
R.drawable.00
is not a legal variable name in Java.
Copy them one by one into the folder
app/res/drawable
the way we copied the image file in
America.
Create the files
PuzzleView.java
and
TileView.java
the way we created the file
JapanView.java
in
Japan.
for
loops in the
PuzzleView
constructor to the following.
Then remove the two-dimensional array.
Resources resources = getResources(); String packageName = getContext().getPackageName(); for (int row = 0; row < n; ++row) { for (int col = 0; col < n; ++col) { //Lower left tile is missing. if (row != emptyRow || col != emptyCol) { int resId = resources.getIdentifier("i" + row + col, "drawable", packageName); TileView tileView = new TileView(context, row, col, resId); addView(tileView); } } }
onLayout
method of class
PuzzleView
,
get them like this:
Resources resources = getResources(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inTargetDensity = DisplayMetrics.DENSITY_DEFAULT; Bitmap bitMap = BitmapFactory.decodeResource(resources, R.drawable.i00, options); int width = bitMap.getWidth(); int height = bitMap.getHeight();
Activity
object is destroyed and we forget the locations of the tiles.
We could keep the
Activity
object alive by adding the attribute
android:configChanges="orientation|screenSize"to the
<activity>
element in the
AndroidManifest.xml
file.
See
Handling
the Configuration Change Yourself.
But let’s allow the old
Activity
to die,
and transmit a
Bundle
of information from the
old
Activity
to the new one.
The
Bundle
will contain
a
short
integer named
emptyRow
,
a
short
integer named
emptyCol
,
and an
ArrayList<Integer>
named
arrayList
.
I used
short
because class
Bundle
has no
putInt
method.
An
ArrayList<Integer>
is like an expandable array of
int
s.
Do not add the above attribute to the
<activity>
element in the
AndroidManifest.xml
file.
PuzzleView
.
//Store three pieces of information into the Bundle object //that some unseen benefactor has given us. public void saveInstanceState(Bundle bundle) { bundle.putShort("emptyRow", (short)emptyRow); bundle.putShort("emptyCol", (short)emptyCol); ArrayList<Integer> arrayList = new ArrayList<Integer>(2 * getChildCount()); for (int i = 0; i < getChildCount(); ++i) { TileView tileView = (TileView)getChildAt(i); arrayList.add(tileView.getRow()); arrayList.add(tileView.getCol()); } bundle.putIntegerArrayList("arrayList", arrayList); }
PuzzleView
to the following.
public PuzzleView(Context context, Bundle bundle) { super(context); setBackgroundColor(Color.WHITE); int[][] a = new int[][] { {R.drawable.i00, R.drawable.i01, R.drawable.i02}, //top row {R.drawable.i10, R.drawable.i11, R.drawable.i12}, //middle row {R.drawable.i20, R.drawable.i21, R.drawable.i22} //bottom row }; //If this is the first incarnation of the MainActivity, put the empty //location in the lower left corner of the puzzle. Otherwise, put it //where it was in the previous incarnation. ArrayList<Integer> arrayList; if (bundle == null) { arrayList = null; emptyRow = n - 1; emptyCol = 0; } else { arrayList = bundle.getIntegerArrayList("arrayList"); emptyRow = bundle.getShort("emptyRow"); emptyCol = bundle.getShort("emptyCol"); } int child = 0; for (int row = 0; row < a.length; ++row) { for (int col = 0; col < a[row].length; ++col) { //Lower left tile is missing. if (row != emptyRow || col != emptyCol) { TileView tileView; if (arrayList == null) { tileView = new TileView(context, row, col, a[row][col]); } else { tileView = new TileView(context, arrayList.get(2 * child), arrayList..get(2 * child + 1), a[row][col]); } addView(tileView); ++child; } } } }
PuzzleView
object will have to be mentioned by more than one method of class
MainActivity
,
so let it be a field of the class.
public class MainActivity extends AppCompatActivity { PuzzleView puzzleView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); puzzleView = new PuzzleView(this, savedInstanceState); setContentView(puzzleView); }
MainActivity
.
It will be called automatically (its name begins with
on
)
before the
MainActivity
is destroyed.
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); puzzleView.saveInstanceState(outState); }