The second tutorial will be about making a simple puzzle Android game. The idea is simple: you choose one image from a list, or you take a photo using the camera, which is then divided into several puzzle pieces, and you have to put it back. To check the app before you build it, you can download it from the Google Play Store: Puzzle – use your own images or choose from ours.

Here’s how the app will look in the end:

Projet Setup

Open Android Studio and create a new project. Name it AndroidPuzzleGame, enter your company or personal domain in reverse order ( com.dragosholban in my case) and choose the location for it to be saved to.

In the next dialog, check only the Phone and Tablet form factor and set the minimum SDK to API 16. Click Next, then select an Empty Activity. Click one more time on the Next button. You can keep the MainActivity as the name for the first activity and click Finish to have Android Studio generate our project files.

Let’s also restrict the MainActivity’s orientation to portrait before we go any further. Open the AndroidManifest.xml file and add this restriction like below:

<?xml version="1.0" encoding="utf-8"?> <manifest ... > <application ... > <activity android:name=".MainActivity" android:screenOrientation="portrait"> ... </activity> </application> </manifest>

Split the Image in Pieces

We will start by loading an image on the screen and split it into several rectangular pieces. Download an image from the internet, I recommend the Unsplash site, but you can choose whatever you want. I will start with this photo, by Macie Jones:

Resize it to something smaller, like 683 x 1024 px. Now copy and paste it into the res/drawable folder in your project. Also rename it to something simpler, like photo.jpg .

Now open the main_activity.xml layout file, from the res/layout folder, and, using the Design tab, select and delete the Hello World! TextView . Next, drag an ImageView into the center of the layout and, when prompted, select the photo we added earlier to be loaded into it. Set the ImageView constraints to 8dp to all the sides of the layout, the layout_weight and layout_height to match_constraint and the scaleType to centerCrop (this way the image will be cropped to fill all the available space). This is what you should see right now if you run the app:

Next is the XML code for the layout, in case something goes wrong. Please notice that I also added and ID for the ConstraintLayout , so we can access it from the code later:

<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="dragosholban.com.androidpuzzlegame.MainActivity" android:id="@+id/layout"> <ImageView android:id="@+id/imageView" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/photo" /> </android.support.constraint.ConstraintLayout>

Having the image loaded into the app, let’s write the code to split it into smaller pieces. Open the MainActivity.java file and add the following code:

public class MainActivity extends AppCompatActivity { ArrayList<Bitmap> pieces; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ConstraintLayout layout = findViewById(R.id.layout); ImageView imageView = findViewById(R.id.imageView); // run image related code after the view was laid out // to have all dimensions calculated imageView.post(new Runnable() { @Override public void run() { pieces = splitImage(); for(Bitmap piece : pieces) { ImageView iv = new ImageView(getApplicationContext()); iv.setImageBitmap(piece); layout.addView(iv); } } }); } private ArrayList<Bitmap> splitImage() { int piecesNumber = 12; int rows = 4; int cols = 3; ImageView imageView = findViewById(R.id.imageView); ArrayList<Bitmap> pieces = new ArrayList<>(piecesNumber); // Get the bitmap of the source image BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable(); Bitmap bitmap = drawable.getBitmap(); // Calculate the with and height of the pieces int pieceWidth = bitmap.getWidth()/cols; int pieceHeight = bitmap.getHeight()/rows; // Create each bitmap piece and add it to the resulting array int yCoord = 0; for (int row = 0; row < rows; row++) { int xCoord = 0; for (int col = 0; col < cols; col++) { pieces.add(Bitmap.createBitmap(bitmap, xCoord, yCoord, pieceWidth, pieceHeight)); xCoord += pieceWidth; } yCoord += pieceHeight; } return pieces; } }

If run the app now you will notice that the image is divided into 12 pieces (you only see one because al the other pieces are behind it). But the piece you see is way larger than it should. That’s because the code above does not take into account the scaled down size of the image you see on the screen, but the actual, much larger, size of the original image. We need to add more code to make the pieces smaller.

The following method will calculate the scaled image dimensions and its position into the ImageView :

private int[] getBitmapPositionInsideImageView(ImageView imageView) { int[] ret = new int[4]; if (imageView == null || imageView.getDrawable() == null) return ret; // Get image dimensions // Get image matrix values and place them in an array float[] f = new float[9]; imageView.getImageMatrix().getValues(f); // Extract the scale values using the constants (if aspect ratio maintained, scaleX == scaleY) final float scaleX = f[Matrix.MSCALE_X]; final float scaleY = f[Matrix.MSCALE_Y]; // Get the drawable (could also get the bitmap behind the drawable and getWidth/getHeight) final Drawable d = imageView.getDrawable(); final int origW = d.getIntrinsicWidth(); final int origH = d.getIntrinsicHeight(); // Calculate the actual dimensions final int actW = Math.round(origW * scaleX); final int actH = Math.round(origH * scaleY); ret[2] = actW; ret[3] = actH; // Get image position // We assume that the image is centered into ImageView int imgViewW = imageView.getWidth(); int imgViewH = imageView.getHeight(); int top = (int) (imgViewH - actH)/2; int left = (int) (imgViewW - actW)/2; ret[0] = left; ret[1] = top; return ret; }

Now change the splitImage code to use the method we just added, to create a scaled image, then split it in pieces:

private ArrayList<Bitmap> splitImage() { int pecesNumber = 12; int rows = 4; int cols = 3; ImageView imageView = findViewById(R.id.imageView); ArrayList<Bitmap> pieces = new ArrayList<>(piecesNumber); // Get the scaled bitmap of the source image BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable(); Bitmap bitmap = drawable.getBitmap(); int[] dimensions = getBitmapPositionInsideImageView(imageView); int scaledBitmapLeft = dimensions[0]; int scaledBitmapTop = dimensions[1]; int scaledBitmapWidth = dimensions[2]; int scaledBitmapHeight = dimensions[3]; int croppedImageWidth = scaledBitmapWidth - 2 * abs(scaledBitmapLeft); int croppedImageHeight = scaledBitmapHeight - 2 * abs(scaledBitmapTop); Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledBitmapWidth, scaledBitmapHeight, true); Bitmap croppedBitmap = Bitmap.createBitmap(scaledBitmap, abs(scaledBitmapLeft), abs(scaledBitmapTop), croppedImageWidth, croppedImageHeight); // Calculate the with and height of the pieces int pieceWidth = croppedImageWidth/cols; int pieceHeight = croppedImageHeight/rows; // Create each bitmap piece and add it to the resulting array int yCoord = 0; for (int row = 0; row < rows; x++) { int xCoord = 0; for (int col = 0; col < cols; y++) { pieces.add(Bitmap.createBitmap(croppedBitmap, xCoord, yCoord, pieceWidth, pieceHeight)); xCoord += pieceWidth; } yCoord += pieceHeight; } return pieces; }

Run the code again and see how the pieces are a lot smaller that before, matching the on screen image size:

Drag the Image Pieces Around

Now, that we have the pieces, let’s make them draggable around the screen with our finger. For this we will need to add a touch listener to all the image views that hold our pieces.

First, create a new TouchListener Java class to hold the touch listener:

public class TouchListener implements View.OnTouchListener { private float xDelta; private float yDelta; @Override public boolean onTouch(View view, MotionEvent motionEvent) { float x = motionEvent.getRawX(); float y = motionEvent.getRawY(); RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) view.getLayoutParams(); switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: xDelta = x - lParams.leftMargin; yDelta = y - lParams.topMargin; break; case MotionEvent.ACTION_MOVE: lParams.leftMargin = (int) (x - xDelta); lParams.topMargin = (int) (y - yDelta); view.setLayoutParams(lParams); break; } return true; } }

Next, add it to the image views we create in the main activity:

// ... public void run() { pieces = splitImage(); TouchListener touchListener = new TouchListener(); for(Bitmap piece : pieces) { ImageView iv = new ImageView(getApplicationContext()); iv.setImageBitmap(piece); iv.setOnTouchListener(touchListener); layout.addView(iv); } }

Finally, to be able to change the images position, we need to add a new relative layout into our main layout. Open the activity_main.xml layout and add it using the text editor:

<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout ...> <ImageView ... /> <RelativeLayout android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> </RelativeLayout> </android.support.constraint.ConstraintLayout>

Notice that we had to move the layout ID from the main container layout to the new relative one. We also need change the type of the layout to RelativeLayout , in the main activity:

final RelativeLayout layout = findViewById(R.id.layout);

That’s it. Run the app again and try to drag the pieces around. It should work without any issues.

Make the Pieces Snap into Place

Now that we can move the pieces around, let’s make them snap into place when they are close enough to their original position.

We’ll first create a new PuzzlePiece class, to hold our pieces and related information, like the original position and dimensions:

public class PuzzlePiece extends android.support.v7.widget.AppCompatImageView { public int xCoord; public int yCoord; public int pieceWidth; public int pieceHeight; public boolean canMove = true; public PuzzlePiece(Context context) { super(context); } }

Now, change the code in the MainActivity to use this new class and to set each piece object the original x and y coordinates and dimensions:

public class MainActivity extends AppCompatActivity { ArrayList<PuzzlePiece> pieces; @Override protected void onCreate(Bundle savedInstanceState) { // ... imageView.post(new Runnable() { @Override public void run() { pieces = splitImage(); TouchListener touchListener = new TouchListener(); for(PuzzlePiece piece : pieces) { piece.setOnTouchListener(touchListener); layout.addView(piece); } } }); } private ArrayList<PuzzlePiece> splitImage() { // ... ArrayList<PuzzlePiece> pieces = new ArrayList<>(piecesNumber); // ... // Create each bitmap piece and add it to the resulting array int yCoord = 0; for (int row = 0; row < rows; row++) { int xCoord = 0; for (int col = 0; col < cols; col++) { Bitmap pieceBitmap = Bitmap.createBitmap(croppedBitmap, xCoord, yCoord, pieceWidth, pieceHeight); PuzzlePiece piece = new PuzzlePiece(getApplicationContext()); piece.setImageBitmap(pieceBitmap); piece.xCoord = xCoord; piece.yCoord = yCoord; piece.pieceWidth = pieceWidth; piece.pieceHeight = pieceHeight; pieces.add(piece); xCoord += pieceWidth; } yCoord += pieceHeight; } return pieces; } // ... }

All we have to do now is to change the touch listener to snap the piece into place, if it is close enough, and to disable the drag by setting the canMove property to false .

public class TouchListener implements View.OnTouchListener { private float xDelta; private float yDelta; @Override public boolean onTouch(View view, MotionEvent motionEvent) { float x = motionEvent.getRawX(); float y = motionEvent.getRawY(); final double tolerance = sqrt(pow(view.getWidth(), 2) + pow(view.getHeight(), 2)) / 10; PuzzlePiece piece = (PuzzlePiece) view; if (!piece.canMove) { return true; } RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) view.getLayoutParams(); switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: xDelta = x - lParams.leftMargin; yDelta = y - lParams.topMargin; piece.bringToFront(); break; case MotionEvent.ACTION_MOVE: lParams.leftMargin = (int) (x - xDelta); lParams.topMargin = (int) (y - yDelta); view.setLayoutParams(lParams); break; case MotionEvent.ACTION_UP: int xDiff = abs(piece.xCoord - lParams.leftMargin); int yDiff = abs(piece.yCoord - lParams.topMargin); if (xDiff <= tolerance && yDiff <= tolerance) { lParams.leftMargin = piece.xCoord; lParams.topMargin = piece.yCoord; piece.setLayoutParams(lParams); piece.canMove = false; sendViewToBack(piece); } break; } return true; } public void sendViewToBack(final View child) { final ViewGroup parent = (ViewGroup)child.getParent(); if (null != parent) { parent.removeView(child); parent.addView(child, 0); } } }

Here we first calculated a tolerance of 10% of the diagonal size of the piece. If the piece is closer than this to the original position, it will snap into place and its canMove property will be set to false . This way, the next time the user tries to move it, it won’t work anymore.

We also made the piece came to the front of the others when touched, in case it is partially obscured by other pieces, and be sent to the back of the stack when it snaps into place, so it does not cover any other piece that can be moved.

Try to run the app and put the pieces in their original position. It will help if we make the original image a little faded: open the activity_main.xml layout and add this to the ImageView :

android:alpha="0.5"

You will notice something strange, the pieces will not snap in place in their original position, but a little off to the left and to the top:

That’s because when we check the original position of the piece, we check the x and y coordinates relatively to the layout the pieces are in, not to the original image from the ImageView . And because the image has some margins that difference is not taken into account. Let’s fix this by adding the ImageView’s left and right margin to the x and y coordinates for each piece when you define it, in the MainActivity.java file:

piece.xCoord = xCoord + imageView.getLeft(); piece.yCoord = yCoord + imageView.getTop();

Try the app again. Now everything should fit in nicely.

Cut the Puzzle Pieces

We almost have a puzzle game here, but our pieces do not look anything like a jigsaw puzzle piece. In the next part we will make them look like regular puzzle pieces that everybody loves.

First we will need to make our pieces bigger, because, if you take two puzzle pieces and imagine a rectangle around each them, you will see that the rectangles actually overlap. We will make this overlap to be one third of the piece width or height. This part will be cut off to give the piece the specific puzzle form. The pieces will have to grow by one third on the left and/or top side, except when they are on left/right outer side of the image.

// Create each bitmap piece and add it to the resulting array int yCoord = 0; for (int row = 0; row < rows; row++) { int xCoord = 0; for (int col = 0; col < cols; col++) { // calculate offset for each piece int offsetX = 0; int offsetY = 0; if (col > 0) { offsetX = pieceWidth / 3; } if (row > 0) { offsetY = pieceHeight / 3; } // apply the offset to each piece Bitmap pieceBitmap = Bitmap.createBitmap(croppedBitmap, xCoord - offsetX, yCoord - offsetY, pieceWidth + offsetX, pieceHeight + offsetY); PuzzlePiece piece = new PuzzlePiece(getApplicationContext()); piece.setImageBitmap(pieceBitmap); piece.xCoord = xCoord - offsetX + imageView.getLeft(); piece.yCoord = yCoord - offsetY + imageView.getTop(); piece.pieceWidth = pieceWidth + offsetX; piece.pieceHeight = pieceHeight + offsetY; pieces.add(piece); xCoord += pieceWidth; } yCoord += pieceHeight; }

Run the app again and notice how pieces overlap each other on the left and/or top inner sides.

Now it’s time to give them the puzzle-like form. We will need to draw a path using code then mask the piece to end up with this:

// ... piece.xCoord = xCoord - offsetX + imageView.getLeft(); piece.yCoord = yCoord - offsetY + imageView.getTop(); piece.pieceWidth = pieceWidth + offsetX; piece.pieceHeight = pieceHeight + offsetY; // this bitmap will hold our final puzzle piece image Bitmap puzzlePiece = Bitmap.createBitmap(pieceWidth + offsetX, pieceHeight + offsetY, Bitmap.Config.ARGB_8888); // draw path int bumpSize = pieceHeight / 4; Canvas canvas = new Canvas(puzzlePiece); Path path = new Path(); path.moveTo(offsetX, offsetY); if (row == 0) { // top side piece path.lineTo(pieceBitmap.getWidth(), offsetY); } else { // top bump path.lineTo(offsetX + (pieceBitmap.getWidth() - offsetX) / 3, offsetY); path.cubicTo(offsetX + (pieceBitmap.getWidth() - offsetX) / 6, offsetY - bumpSize, offsetX + (pieceBitmap.getWidth() - offsetX) / 6 * 5, offsetY - bumpSize, offsetX + (pieceBitmap.getWidth() - offsetX) / 3 * 2, offsetY); path.lineTo(pieceBitmap.getWidth(), offsetY); } if (col == cols - 1) { // right side piece path.lineTo(pieceBitmap.getWidth(), pieceBitmap.getHeight()); } else { // right bump path.lineTo(pieceBitmap.getWidth(), offsetY + (pieceBitmap.getHeight() - offsetY) / 3); path.cubicTo(pieceBitmap.getWidth() - bumpSize,offsetY + (pieceBitmap.getHeight() - offsetY) / 6, pieceBitmap.getWidth() - bumpSize, offsetY + (pieceBitmap.getHeight() - offsetY) / 6 * 5, pieceBitmap.getWidth(), offsetY + (pieceBitmap.getHeight() - offsetY) / 3 * 2); path.lineTo(pieceBitmap.getWidth(), pieceBitmap.getHeight()); } if (row == rows - 1) { // bottom side piece path.lineTo(offsetX, pieceBitmap.getHeight()); } else { // bottom bump path.lineTo(offsetX + (pieceBitmap.getWidth() - offsetX) / 3 * 2, pieceBitmap.getHeight()); path.cubicTo(offsetX + (pieceBitmap.getWidth() - offsetX) / 6 * 5,pieceBitmap.getHeight() - bumpSize, offsetX + (pieceBitmap.getWidth() - offsetX) / 6, pieceBitmap.getHeight() - bumpSize, offsetX + (pieceBitmap.getWidth() - offsetX) / 3, pieceBitmap.getHeight()); path.lineTo(offsetX, pieceBitmap.getHeight()); } if (col == 0) { // left side piece path.close(); } else { // left bump path.lineTo(offsetX, offsetY + (pieceBitmap.getHeight() - offsetY) / 3 * 2); path.cubicTo(offsetX - bumpSize, offsetY + (pieceBitmap.getHeight() - offsetY) / 6 * 5, offsetX - bumpSize, offsetY + (pieceBitmap.getHeight() - offsetY) / 6, offsetX, offsetY + (pieceBitmap.getHeight() - offsetY) / 3); path.close(); }

Now we mask the piece with the resulting path:

// mask the piece Paint paint = new Paint(); paint.setColor(0XFF000000); paint.setStyle(Paint.Style.FILL); canvas.drawPath(path, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(pieceBitmap, 0, 0, paint);

Finally, we draw some borders around the puzzle piece to make it look better and set the resulting bitmap to the piece object:

// draw a white border Paint border = new Paint(); border.setColor(0X80FFFFFF); border.setStyle(Paint.Style.STROKE); border.setStrokeWidth(8.0f); canvas.drawPath(path, border); // draw a black border border = new Paint(); border.setColor(0X80000000); border.setStyle(Paint.Style.STROKE); border.setStrokeWidth(3.0f); canvas.drawPath(path, border); // set the resulting bitmap to the piece piece.setImageBitmap(puzzlePiece); pieces.add(piece); xCoord += pieceWidth;

Run the app now and… enjoy your little puzzle game!

Adding More Images

We did it! We can now generate puzzle pieces from any image we want. Let’s now add more images to our app so the users have more options to play with this.

First, we have to move everything from the MainActivity into a new PuzzleActivity to make room for the list of images.

From the File menu, choose New -> Activity -> Empty Activity and give it the PuzzleActivity name. Then move all the code we added in the MainActivity to this one. The MainActivity should only have left the onCreate method:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }

Also cut and paste the ImageView and the RelativeLayout from the activity_main.xml layout file to the new activity_puzzle.xml one.

Now download more images from https://unsplash.com/ or any other source. When you’re done, back in Android Studio, select the File menu then New -> Folder -> Assets Folder. Leave the default options and click Finish. From the left panel, select the new Assets folder then right click on it and create a new Directory named img . Here copy and paste all your downloaded images. These files will be included in the app and we will use them in the code to allow the users choose them and make puzzle pieces.

Open the activity_main.xml and add a GridView to it:

<GridView android:id="@+id/grid" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:horizontalSpacing="10dp" android:numColumns="3" android:paddingHorizontal="10dp" android:paddingVertical="10dp" android:verticalSpacing="10dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />

This will list our images. In the MainActivity class, add the following code to make it work:

// ... protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AssetManager am = getAssets(); try { final String[] files = am.list("img"); GridView grid = findViewById(R.id.grid); grid.setAdapter(new ImageAdapter(this)); } catch (IOException e) { Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT); } }

The GridView we set up above uses an ImageAdapter we still have to define. Create a new java class named ImageAdapter and add the following:

public class ImageAdapter extends BaseAdapter { private Context mContext; private AssetManager am; private String[] files; public ImageAdapter(Context c) { mContext = c; am = mContext.getAssets(); try { files = am.list("img"); } catch (IOException e) { e.printStackTrace(); } } public int getCount() { return files.length; } public Object getItem(int position) { return null; } public long getItemId(int position) { return 0; } // create a new ImageView for each item referenced by the Adapter public View getView(final int position, View convertView, ViewGroup parent) { if (convertView == null) { final LayoutInflater layoutInflater = LayoutInflater.from(mContext); convertView = layoutInflater.inflate(R.layout.grid_element, null); } final ImageView imageView = convertView.findViewById(R.id.gridImageview); imageView.setImageBitmap(null); // run image related code after the view was laid out imageView.post(new Runnable() { @Override public void run() { new AsyncTask<Void, Void, Void>() { private Bitmap bitmap; @Override protected Void doInBackground(Void... voids) { bitmap = getPicFromAsset(imageView, files[position]); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); imageView.setImageBitmap(bitmap); } }.execute(); } }); return convertView; } private Bitmap getPicFromAsset(ImageView imageView, String assetName) { // Get the dimensions of the View int targetW = imageView.getWidth(); int targetH = imageView.getHeight(); if(targetW == 0 || targetH == 0) { // view has no dimensions set return null; } try { InputStream is = am.open("img/" + assetName); // Get the dimensions of the bitmap BitmapFactory.Options bmOptions = new BitmapFactory.Options(); bmOptions.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, new Rect(-1, -1, -1, -1), bmOptions); int photoW = bmOptions.outWidth; int photoH = bmOptions.outHeight; // Determine how much to scale down the image int scaleFactor = Math.min(photoW/targetW, photoH/targetH); is.reset(); // Decode the image file into a Bitmap sized to fill the View bmOptions.inJustDecodeBounds = false; bmOptions.inSampleSize = scaleFactor; bmOptions.inPurgeable = true; return BitmapFactory.decodeStream(is, new Rect(-1, -1, -1, -1), bmOptions); } catch (IOException e) { e.printStackTrace(); return null; } } }

This will load each image from the assets when needed and setup a grid_element layout to be used by the GridView for each cell.

We still have to create the new grid_element layout, so let’s get to it. Create a new layout resource file named grid_element.xml , with this content:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:padding="2dp" android:background="@color/colorPrimary" > <ImageView android:id="@+id/gridImageview" android:layout_width="0dp" android:layout_height="0dp" android:scaleType="centerCrop" app:layout_constraintDimensionRatio="H,4:5" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </android.support.constraint.ConstraintLayout>

This should be it. Run the app and check that the images you added are listed on the main screen:

Ok, now we have to open the PuzzleActivity with the new image, when the user clicks on one of the grid elements. To do this, first add a item click listener to the grid view after you load it in the onCreate method:

grid.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { Intent intent = new Intent(getApplicationContext(), PuzzleActivity.class); intent.putExtra("assetName", files[i % files.length]); startActivity(intent); } });

This will create an Intent to open the PuzzleActivity activity and send the asset’s name as an intent extra.

Next, open the PuzzleActivity and add the code to load the corresponding asset instead of the image we loaded in the first part of this tutorial. First, get the asset name from the intent, then, using a new setPicFromAsset method, load it in the ImageView and split it into pieces:

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_puzzle); final RelativeLayout layout = findViewById(R.id.layout); final ImageView imageView = findViewById(R.id.imageView); Intent intent = getIntent(); final String assetName = intent.getStringExtra("assetName"); // run image related code after the view was laid out // to have all dimensions calculated imageView.post(new Runnable() { @Override public void run() { if (assetName != null) { setPicFromAsset(assetName, imageView); } pieces = splitImage(); TouchListener touchListener = new TouchListener(); for(PuzzlePiece piece : pieces) { piece.setOnTouchListener(touchListener); layout.addView(piece); } } }); } private void setPicFromAsset(String assetName, ImageView imageView) { // Get the dimensions of the View int targetW = imageView.getWidth(); int targetH = imageView.getHeight(); AssetManager am = getAssets(); try { InputStream is = am.open("img/" + assetName); // Get the dimensions of the bitmap BitmapFactory.Options bmOptions = new BitmapFactory.Options(); bmOptions.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, new Rect(-1, -1, -1, -1), bmOptions); int photoW = bmOptions.outWidth; int photoH = bmOptions.outHeight; // Determine how much to scale down the image int scaleFactor = Math.min(photoW/targetW, photoH/targetH); is.reset(); // Decode the image file into a Bitmap sized to fill the View bmOptions.inJustDecodeBounds = false; bmOptions.inSampleSize = scaleFactor; bmOptions.inPurgeable = true; Bitmap bitmap = BitmapFactory.decodeStream(is, new Rect(-1, -1, -1, -1), bmOptions); imageView.setImageBitmap(bitmap); } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); } }

Run the app now, select any image and see how it is transformed into puzzle pieces for you to match. There is still work to do though.

Shuffle the Pieces and Check for End Game

Right now, all the puzzle pieces are stacked one over the other, in the right order for you to take them and put in the right place. Let’s make the game harder by shuffling their position and order:

public void run() { if (assetName != null) { setPicFromAsset(assetName, imageView); } pieces = splitImage(); TouchListener touchListener = new TouchListener(); // shuffle pieces order Collections.shuffle(pieces); for(PuzzlePiece piece : pieces) { piece.setOnTouchListener(touchListener); layout.addView(piece); // randomize position, on the bottom of the screen RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) piece.getLayoutParams(); lParams.leftMargin = new Random().nextInt(layout.getWidth() - piece.pieceWidth); lParams.topMargin = layout.getHeight() - piece.pieceHeight; piece.setLayoutParams(lParams); } }

If you complete the puzzle, nothing happens. You have to manually press the back button to return to the list and choose another image. It will be better to make the app detect that the puzzle is completed and return you to the main screen. Checking that the game is over is as simple as checking that each piece is not movable anymore:

public void checkGameOver() { if (isGameOver()) { finish(); } } private boolean isGameOver() { for (PuzzlePiece piece : pieces) { if (piece.canMove) { return false; } } return true; }

We will need to call the checkGameOver method from the touch listener, after the user makes a move. This means we need to have access to the activity, so we need, first, to add a constructor, then call the activity’s checkGameOver method:

public class TouchListener implements View.OnTouchListener { // ... private PuzzleActivity activity; public TouchListener(PuzzleActivity activity) { this.activity = activity; } @Override public boolean onTouch(View view, MotionEvent motionEvent) { // ... switch (motionEvent.getAction() & MotionEvent.ACTION_MASK) { // ... case MotionEvent.ACTION_UP: int xDiff = abs(piece.xCoord - lParams.leftMargin); int yDiff = abs(piece.yCoord - lParams.topMargin); if (xDiff <= tolerance && yDiff <= tolerance) { lParams.leftMargin = piece.xCoord; lParams.topMargin = piece.yCoord; piece.setLayoutParams(lParams); piece.canMove = false; sendViewToBack(piece); activity.checkGameOver(); } break; } return true; } // ... }

For this to work, you need to send the activity instance when defining the touchListener , in the PuzzleActivity class:

TouchListener touchListener = new TouchListener(PuzzleActivity.this);

Run the app now and check that everything works. When you finish a puzzle, you will be returned to the main screen to start a new one.

Getting Images from the Camera

Wouldn’t it be cool to be able to take photos with your camera and transform them into puzzles? Let’s do this next.

First we need our app to be able to write files on the user’s device. This means we need to edit the AndroidManifest.xml file to declare this:

<?xml version="1.0" encoding="utf-8"?> <manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application ...> <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data> </provider> </application> </manifest>

We also need to define a file provider so we can send files between activities. This provider needs a new resource xml file named file_paths.xml , where we will define the path to our files (create a new resource directory named xml for it):

<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_images" path="." /> </paths>

Now, open the activity_main.xml layout file and add a FloatingActionButton with a camera icon:

<android.support.design.widget.FloatingActionButton android:id="@+id/cameraButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:clickable="true" android:onClick="onImageFromCameraClick" android:src="@drawable/ic_photo_camera_black_24dp" android:tint="@android:color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" />

This button uses the ic_photo_camera_black_24dp drawable resource, for the camera icon, that we have to add to our project. From the File menu select New -> Vector Asset, then click on the “android” icon and search for the “photo camera”. Select the icon, then click on Next and then Finish. You should now have a new ic_photo_camera_black_24dp.xml file in the res/drawable folder.

In the main activity, define the method called by the camera button and the method to create the file to save the image to. Notice that we also need to check and request the WRITE_EXTERNAL_STORAGE permission, if not granted:

String mCurrentPhotoPath; private static final int REQUEST_PERMISSION_WRITE_EXTERNAL_STORAGE = 2; private static final int REQUEST_IMAGE_CAPTURE = 1; public void onImageFromCameraClick(View view) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) { File photoFile = null; try { photoFile = createImageFile(); } catch (IOException e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG); } if (photoFile != null) { Uri photoUri = FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".fileprovider", photoFile); intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); startActivityForResult(intent, REQUEST_IMAGE_CAPTURE); } } } private File createImageFile() throws IOException { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // permission not granted, initiate request ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION_WRITE_EXTERNAL_STORAGE); } else { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File image = File.createTempFile( imageFileName, /* prefix */ ".jpg", /* suffix */ storageDir /* directory */ ); mCurrentPhotoPath = image.getAbsolutePath(); // save this to use in the intent return image; } return null; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case REQUEST_PERMISSION_WRITE_EXTERNAL_STORAGE: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { onImageFromCameraClick(new View(this)); } return; } } }

Now, that we can capture the image from the camera, we need to receive it and send it to the PuzzleActivity using an intent extra:

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { Intent intent = new Intent(this, PuzzleActivity.class); intent.putExtra("mCurrentPhotoPath", mCurrentPhotoPath); startActivity(intent); } }

The last thing to to is to change the PuzzleActivity to check for and use the received image in the intent:

String mCurrentPhotoPath; @Override protected void onCreate(Bundle savedInstanceState) { // ... Intent intent = getIntent(); final String assetName = intent.getStringExtra("assetName"); mCurrentPhotoPath = intent.getStringExtra("mCurrentPhotoPath"); // run image related code after the view was laid out // to have all dimensions calculated imageView.post(new Runnable() { @Override public void run() { if (assetName != null) { setPicFromAsset(assetName, imageView); } else if (mCurrentPhotoPath != null) { setPicFromPath(mCurrentPhotoPath, imageView); } // ...

We now need to add the new setPicFromPath method:

private void setPicFromPath(String mCurrentPhotoPath, ImageView imageView) { // Get the dimensions of the View int targetW = imageView.getWidth(); int targetH = imageView.getHeight(); // Get the dimensions of the bitmap BitmapFactory.Options bmOptions = new BitmapFactory.Options(); bmOptions.inJustDecodeBounds = true; BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions); int photoW = bmOptions.outWidth; int photoH = bmOptions.outHeight; // Determine how much to scale down the image int scaleFactor = Math.min(photoW/targetW, photoH/targetH); // Decode the image file into a Bitmap sized to fill the View bmOptions.inJustDecodeBounds = false; bmOptions.inSampleSize = scaleFactor; bmOptions.inPurgeable = true; Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions); Bitmap rotatedBitmap = bitmap; // rotate bitmap if needed try { ExifInterface ei = new ExifInterface(mCurrentPhotoPath); int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: rotatedBitmap = rotateImage(bitmap, 90); break; case ExifInterface.ORIENTATION_ROTATE_180: rotatedBitmap = rotateImage(bitmap, 180); break; case ExifInterface.ORIENTATION_ROTATE_270: rotatedBitmap = rotateImage(bitmap, 270); break; } } catch (IOException e) { Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); } imageView.setImageBitmap(rotatedBitmap); } public static Bitmap rotateImage(Bitmap source, float angle) { Matrix matrix = new Matrix(); matrix.postRotate(angle); return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true); }

That’s it! Run the app now and you will be able to take pictures with the phone’s camera that will be transformed into puzzles.

If you get an error at this point, java.lang.ClassNotFoundException: Didn't find class "android.support.design.widget.FloatingActionButton" , you need to edit the app/build.gradle file and add:

dependencies { ... implementation 'com.android.support:design:26.1.0' }

Getting Images from the Gallery

The last “piece” of our puzzle game is to allow the user to choose images from the phone’s gallery. Let’s first add a new FloatingActionButton with a image icon:

<android.support.design.widget.FloatingActionButton android:id="@+id/galleryButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:clickable="true" android:onClick="onImageFromGalleryClick" android:src="@drawable/ic_image_black_24dp" android:tint="@android:color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/cameraButton" />

Add the ic_image_black_24dp drawable resource like we did before with the camera icon. This time search for the “image” icon.

Now, let’s add the onImageFromGalleryClick that is called when the button is clicked, in the MainActivity class:

static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 3; static final int REQUEST_IMAGE_GALLERY = 4; public void onImageFromGalleryClick(View view) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_READ_EXTERNAL_STORAGE); } else { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); startActivityForResult(intent, REQUEST_IMAGE_GALLERY); } }

Now we need to be ready to receive the image the user selected:

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // ... if (requestCode == REQUEST_IMAGE_GALLERY && resultCode == RESULT_OK) { Uri uri = data.getData(); Intent intent = new Intent(this, PuzzleActivity.class); intent.putExtra("mCurrentPhotoUri", uri.toString()); startActivity(intent); } }

Finally, in the PuzzleActivity class, we need to be able to load the image using the Uri send by the main activity:

String mCurrentPhotoUri; @Override protected void onCreate(Bundle savedInstanceState) { // ... mCurrentPhotoUri = intent.getStringExtra("mCurrentPhotoUri"); // run image related code after the view was laid out // to have all dimensions calculated imageView.post(new Runnable() { @Override public void run() { if (assetName != null) { setPicFromAsset(assetName, imageView); } else if (mCurrentPhotoPath != null) { setPicFromPath(mCurrentPhotoPath, imageView); } else if (mCurrentPhotoUri != null) { imageView.setImageURI(Uri.parse(mCurrentPhotoUri)); } pieces = splitImage(); // ...

That’s it! Run the app now and try selecting images from your phone’s gallery.

Final Touches

Our puzzle game is ready. You can choose one of the existing images or import new ones from the camera or from the device gallery. But we can still add some improvements.

In the PuzzleActivity , the image takes over all the available space. It will be nicer to leave some space at the bottom, where we put the puzzle pieces at the start of the game. Let’s add a aspect ratio constraint of 4:5 to the images to accomplish this:

<ImageView android:id="@+id/imageView" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:scaleType="centerCrop" app:layout_constraintDimensionRatio="H,4:5" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/photo" android:alpha="0.5" />

Notice that we had to remove the bottom margin and the bottom constraint that were conflicting with this new requirement.

Next, let’s add a nice background to our app. Download the background image from here and add it to a new res/drawable-xxhdpi directory. Now, in both activity_main.xml and activity_puzzle.xml files, set the background of the main ConstraintLayout to it:

android:background="@drawable/table_background"

The last this we will do is to add a frame to our images. Download the frame image from here, add it to same res/drawable-xxhdpi as before, then set it as a background for the ConstraintLayout from the grid_element.xml file:

android:background="@drawable/puzzle_frame"

For the PuzzleActivity , we will add another ImageView behind the existing one holding the semi-transparent image, with the frame. Also change the existing ImageView margins to make it fit inside the frame:

<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout ...> <ImageView android:layout_width="0dp" android:layout_height="0dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:alpha="1.0" app:layout_constraintDimensionRatio="H,4:5" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/puzzle_frame" /> <ImageView android:id="@+id/imageView" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="18dp" android:scaleType="centerCrop" app:layout_constraintDimensionRatio="H,4:5" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/photo" android:alpha="0.5" /> ... </android.support.constraint.ConstraintLayout>

We’re done. You should now have a nice Jigsaw Puzzle Android Application to play with. I hope you enjoyed building this and learned a lot along the way. Please let me know in the comments how it was for you, if you found any problems or if you have any improvements to suggest. Also, if this tutorial helped you build some other Android apps, please add some links in the comments and tell all of us about them.

You can get the final code from GitHub, in case something goes terribly wrong and you don’t manage to fix it. See you at the next tutorial!