Classic 3 × 3 Puzzle

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 TileViews 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.

Source code in Puzzle.zip

  1. MainActivity.java.
  2. 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.
  3. 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.
  4. activity_main.xml: unused and ignored.
  5. strings.xml.
  6. .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.
    1. i00.png
    2. i01.png
    3. i02.png
    4. i10.png
    5. i11.png
    6. i12.png
    7. i20.png (lower left tile is unused)
    8. i21.png
    9. i22.png
  7. AndroidManifest.xml.
  8. build.gradle (Module: app).

Create the project

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.

Things to try

  1. Change the nested 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);
                    }
                }
            }
    

  2. Instead of hardwiring the numbers 300 and 394 into the 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();
    

  3. The screenshots in Text showed that the Samsung Galaxy S5 and the Nexus 5 have the same number of pixels per inch vertically and horizomtally (480 and 480). But the screenshot of my Azpen A727 showed that it has more pixels per inch vertically than horizontally. This explained why Obama’s face looked slightly fat. How could you compensate for this disparity?

  4. When we change the orientation of the device, the 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 ints. Do not add the above attribute to the <activity> element in the AndroidManifest.xml file.
    1. Add the following method to class 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);
          }
      
    2. Change the constructor of class 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;
                      }
                  }
              }
          }
      
    3. The 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);
          }
      
    4. Add the following method to class MainActivity. It will be called automatically (its name begins with on) before the MainActivity is destroyed.
    5.     @Override
          protected void onSaveInstanceState(Bundle outState) {
              super.onSaveInstanceState(outState);
              puzzleView.saveInstanceState(outState);
          }