This tutorial explains how to build your own image gallery browser in your android applications with swipe and pinch zooming functionality. This article is combination of three separate concepts such as Grid View, Swiping and Pinch Gesture in one project.

I already covered Grid View previously. Refer Android GridView Layout Tutorial incase if you are new to Grid View.

android full screen image slider with swipe gesture

This project is divided in to three tasks. First one is building Grid View display of all the images. Second is showing selected grid image in full screen slider. And finally adding pinch zooming functionality to fullscreen image.

Let’s start with creating a new project

Creating new project

1. Create new project in Eclipse IDE from File ⇒ New ⇒ Android Application Project and fill all the required details. I kept my package name as info.androidhive.imageslider and main activity name as GridViewActivity.java

App Constant Class

I am creating a app constant class file to store static variables which will be used across application. In this way you don’t have modify much code if you want to change app configuration.

2. In order to maintaining good project structure I am creating a separate package for storing this kind of helper classes. Right click on src ⇒ New ⇒ Package and name it as your_package_name.helper.
(So my helper package becomes info.androidhive.imageslider.helper)

3. Under helper package create a new class named AppConstant.java and paste the following code. In the following I declared required constant variables

NUM_OF_COLUMNS – Number of columns to be displayed in Grid view
PHOTO_ALBUM – Name of the photo album directory in the sd card. Make sure that you have some images inside this directory before you start the project.
FILE_EXTN – Image file extensions to be supported

package info.androidhive.imageslider.helper;

import java.util.Arrays;
import java.util.List;

public class AppConstant {

	// Number of columns of Grid View
	public static final int NUM_OF_COLUMNS = 3;

	// Gridview image padding
	public static final int GRID_PADDING = 8; // in dp

	// SD card image directory
	public static final String PHOTO_ALBUM = "androidhive";

	// supported file formats
	public static final List<String> FILE_EXTN = Arrays.asList("jpg", "jpeg",
			"png");
}

Helper Utils Class

4. I am creating another class to define reusable functions across applications. Create another class under helper package named Utils.java

Following are major functions defined in Utils class

getFilePaths() – This function will return all image paths of a directory
IsSupportedFile() – This will check for supported file extensions

package info.androidhive.imageslider.helper;

import java.io.File;
import java.util.ArrayList;
import java.util.Locale;

import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;

public class Utils {

	private Context _context;

	// constructor
	public Utils(Context context) {
		this._context = context;
	}

	// Reading file paths from SDCard
	public ArrayList<String> getFilePaths() {
		ArrayList<String> filePaths = new ArrayList<String>();

		File directory = new File(
				android.os.Environment.getExternalStorageDirectory()
						+ File.separator + AppConstant.PHOTO_ALBUM);

		// check for directory
		if (directory.isDirectory()) {
			// getting list of file paths
			File[] listFiles = directory.listFiles();

			// Check for count
			if (listFiles.length > 0) {

				// loop through all files
				for (int i = 0; i < listFiles.length; i++) {

					// get file path
					String filePath = listFiles[i].getAbsolutePath();

					// check for supported file extension
					if (IsSupportedFile(filePath)) {
						// Add image path to array list
						filePaths.add(filePath);
					}
				}
			} else {
				// image directory is empty
				Toast.makeText(
						_context,
						AppConstant.PHOTO_ALBUM
								+ " is empty. Please load some images in it !",
						Toast.LENGTH_LONG).show();
			}

		} else {
			AlertDialog.Builder alert = new AlertDialog.Builder(_context);
			alert.setTitle("Error!");
			alert.setMessage(AppConstant.PHOTO_ALBUM
					+ " directory path is not valid! Please set the image directory name AppConstant.java class");
			alert.setPositiveButton("OK", null);
			alert.show();
		}

		return filePaths;
	}

	// Check supported file extensions
	private boolean IsSupportedFile(String filePath) {
		String ext = filePath.substring((filePath.lastIndexOf(".") + 1),
				filePath.length());

		if (AppConstant.FILE_EXTN
				.contains(ext.toLowerCase(Locale.getDefault())))
			return true;
		else
			return false;

	}

	/*
	 * getting screen width
	 */
	public int getScreenWidth() {
		int columnWidth;
		WindowManager wm = (WindowManager) _context
				.getSystemService(Context.WINDOW_SERVICE);
		Display display = wm.getDefaultDisplay();

		final Point point = new Point();
		try {
			display.getSize(point);
		} catch (java.lang.NoSuchMethodError ignore) { // Older device
			point.x = display.getWidth();
			point.y = display.getHeight();
		}
		columnWidth = point.x;
		return columnWidth;
	}
}

Displaying thumbnail images in Grid View

Until now we are done with some utility functions which required further. So let’s start first view of the application which is displaying images in grid view.

5. Create a new layout file under res ⇒ layout folder named activity_grid_view.xml and paste the following code

<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/grid_view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:numColumns="auto_fit"
    android:gravity="center"
    android:stretchMode="columnWidth"
    android:background="#000000"> 
</GridView>

GridView Adapter

I am creating a custom grid adapter class for the grid view. In this way you can create a reusable view for the grid view blocks. As this class is not an activity class I prefer to keep it in another package.

6. Create another package named adapter under src folder. I created another package named info.androidhive.imageslider.adapter

7. Under adapter package create a class file named GridViewImageAdapter.java and extend it from BaseAdapter This adapter class simply returns image view to gridview.

package info.androidhive.imageslider.adapter;

import info.androidhive.imageslider.FullScreenViewActivity;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

public class GridViewImageAdapter extends BaseAdapter {

	private Activity _activity;
	private ArrayList<String> _filePaths = new ArrayList<String>();
	private int imageWidth;

	public GridViewImageAdapter(Activity activity, ArrayList<String> filePaths,
			int imageWidth) {
		this._activity = activity;
		this._filePaths = filePaths;
		this.imageWidth = imageWidth;
	}

	@Override
	public int getCount() {
		return this._filePaths.size();
	}

	@Override
	public Object getItem(int position) {
		return this._filePaths.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ImageView imageView;
		if (convertView == null) {
			imageView = new ImageView(_activity);
		} else {
			imageView = (ImageView) convertView;
		}

		// get screen dimensions
		Bitmap image = decodeFile(_filePaths.get(position), imageWidth,
				imageWidth);

		imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
		imageView.setLayoutParams(new GridView.LayoutParams(imageWidth,
				imageWidth));
		imageView.setImageBitmap(image);

		// image view click listener
		imageView.setOnClickListener(new OnImageClickListener(position));

		return imageView;
	}

	class OnImageClickListener implements OnClickListener {

		int _postion;

		// constructor
		public OnImageClickListener(int position) {
			this._postion = position;
		}

		@Override
		public void onClick(View v) {
			// on selecting grid view image
			// launch full screen activity
			Intent i = new Intent(_activity, FullScreenViewActivity.class);
			i.putExtra("position", _postion);
			_activity.startActivity(i);
		}

	}

	/*
	 * Resizing image size
	 */
	public static Bitmap decodeFile(String filePath, int WIDTH, int HIGHT) {
		try {

			File f = new File(filePath);

			BitmapFactory.Options o = new BitmapFactory.Options();
			o.inJustDecodeBounds = true;
			BitmapFactory.decodeStream(new FileInputStream(f), null, o);

			final int REQUIRED_WIDTH = WIDTH;
			final int REQUIRED_HIGHT = HIGHT;
			int scale = 1;
			while (o.outWidth / scale / 2 >= REQUIRED_WIDTH
					&& o.outHeight / scale / 2 >= REQUIRED_HIGHT)
				scale *= 2;

			BitmapFactory.Options o2 = new BitmapFactory.Options();
			o2.inSampleSize = scale;
			return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}

}

8. Finally open the activity class for grid view. In my case my grid view activity name is GridViewActivity.java.

InitilizeGridLayout() – This will initialize the grid view layout by setting necessary attributes like padding, number of columns, spacing between grid images etc.,

package info.androidhive.imageslider;

import info.androidhive.imageslider.adapter.GridViewImageAdapter;
import info.androidhive.imageslider.helper.AppConstant;
import info.androidhive.imageslider.helper.Utils;

import java.util.ArrayList;

import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.TypedValue;
import android.widget.GridView;

public class GridViewActivity extends Activity {

	private Utils utils;
	private ArrayList<String> imagePaths = new ArrayList<String>();
	private GridViewImageAdapter adapter;
	private GridView gridView;
	private int columnWidth;

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

		gridView = (GridView) findViewById(R.id.grid_view);

		utils = new Utils(this);

		// Initilizing Grid View
		InitilizeGridLayout();

		// loading all image paths from SD card
		imagePaths = utils.getFilePaths();

		// Gridview adapter
		adapter = new GridViewImageAdapter(GridViewActivity.this, imagePaths,
				columnWidth);

		// setting grid view adapter
		gridView.setAdapter(adapter);
	}

	private void InitilizeGridLayout() {
		Resources r = getResources();
		float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
				AppConstant.GRID_PADDING, r.getDisplayMetrics());

		columnWidth = (int) ((utils.getScreenWidth() - ((AppConstant.NUM_OF_COLUMNS + 1) * padding)) / AppConstant.NUM_OF_COLUMNS);

		gridView.setNumColumns(AppConstant.NUM_OF_COLUMNS);
		gridView.setColumnWidth(columnWidth);
		gridView.setStretchMode(GridView.NO_STRETCH);
		gridView.setPadding((int) padding, (int) padding, (int) padding,
				(int) padding);
		gridView.setHorizontalSpacing((int) padding);
		gridView.setVerticalSpacing((int) padding);
	}

}

Now run your project and test it once. You should see a grid view displaying the images from the sd card. Following is the screenshot of my grid view

android grid view and fullscreen image slider

Creating Fullscreen Image Slider

Second task in this tutorial is to build a fullscreen image slider for the images displayed in the grid view. This also involves swiping gesture to navigate through the album.

To create swiping gesture functionality I am using PagerAdapter class provided by android.

9. Create an xml layout file for full screen activity. I created a file called activity_fullscreen_view.xml under res ⇒ layout folder. Add a android.support.v4.view.ViewPager element inside it.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>

10. In fullscreen view instead of showing only fullview image you might want to show some other UI elments like text, buttons along with the image. So I created a separate layout for fullscreen view, so that you can customize the fullscreen view in this layout file.

As of now I am just adding a close button over the image. Create an xml layout file under res ⇒ layout folder named layout_fullscreen_image.xml and add the following code

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:id="@+id/imgDisplay"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="fitCenter" />

    <Button
        android:id="@+id/btnClose"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginRight="15dp"
        android:layout_marginTop="15dp"
        android:paddingTop="2dp"
        android:paddingBottom="2dp"
        android:background="@drawable/button_background"
        android:textColor="#ffffff"
        android:text="Close" />

</RelativeLayout>

Also create a file called button_background.xml under drawable folder. This is just for styling the button

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <corners android:radius="3dp" />

    <solid android:color="#000000" />

    <stroke
        android:width="2px"
        android:color="#ffffff" />

</shape>

Fullscreen Image Viewer Adapter

Just like grid adapter, we are going to create another adapter for the full screen activity too. This is the data provider for the fullscreen image viewer.

11. Create another class under adapter package named FullScreenImageAdapter.java and extend it from PagerAdapter.

package info.androidhive.imageslider.adapter;

import info.androidhive.imageslider.R;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;

public class FullScreenImageAdapter extends PagerAdapter {

	private Activity _activity;
	private ArrayList<String> _imagePaths;
	private LayoutInflater inflater;

	// constructor
	public FullScreenImageAdapter(Activity activity,
			ArrayList<String> imagePaths) {
		this._activity = activity;
		this._imagePaths = imagePaths;
	}

	@Override
	public int getCount() {
		return this._imagePaths.size();
	}

	@Override
    public boolean isViewFromObject(View view, Object object) {
        return view == ((RelativeLayout) object);
    }
	
	@Override
    public Object instantiateItem(ViewGroup container, int position) {
        ImageView imgDisplay;
        Button btnClose;
 
        inflater = (LayoutInflater) _activity
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View viewLayout = inflater.inflate(R.layout.layout_fullscreen_image, container,
                false);
 
        imgDisplay = (ImageView) viewLayout.findViewById(R.id.imgDisplay);
        btnClose = (Button) viewLayout.findViewById(R.id.btnClose);
        
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        Bitmap bitmap = BitmapFactory.decodeFile(_imagePaths.get(position), options);
        imgDisplay.setImageBitmap(bitmap);
        
        // close button click event
        btnClose.setOnClickListener(new View.OnClickListener() {			
			@Override
			public void onClick(View v) {
				_activity.finish();
			}
		});
 
        ((ViewPager) container).addView(viewLayout);
 
        return viewLayout;
	}
	
	@Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        ((ViewPager) container).removeView((RelativeLayout) object);
 
    }
}

Also make sure that you have added FullScreenViewActivity to AndroidManifest.xml file before testing your project.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.imageslider"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="info.androidhive.imageslider.GridViewActivity"
            android:theme="@android:style/Theme.Holo.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity
            android:name="info.androidhive.imageslider.FullScreenViewActivity"
            android:theme="@android:style/Theme.Holo.NoActionBar">
        </activity>
    </application>

</manifest>

Run your project once again and click on grid view image. You should see fullscreen view of selected grid view image. Also swipe left or right to cycle through album.

android full screen image slider with swipe

Adding Pinch Zooming Functionality

For pinching functionality instead of writing my own class from scratch, I just borrowed the code from TouchImageView. Thanks to Michael Ortiz for writing such a beautiful code 🙂

Adding this class in your project needs very few modifications.

12. Create class in helper package named TouchImageView.java and paste the following code.

package info.androidhive.imageslider.helper;

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;

public class TouchImageView extends ImageView {

	Matrix matrix;

	// We can be in one of these 3 states
	static final int NONE = 0;
	static final int DRAG = 1;
	static final int ZOOM = 2;
	int mode = NONE;

	// Remember some things for zooming
	PointF last = new PointF();
	PointF start = new PointF();
	float minScale = 1f;
	float maxScale = 3f;
	float[] m;

	int viewWidth, viewHeight;
	static final int CLICK = 3;
	float saveScale = 1f;
	protected float origWidth, origHeight;
	int oldMeasuredWidth, oldMeasuredHeight;

	ScaleGestureDetector mScaleDetector;

	Context context;

	public TouchImageView(Context context) {
		super(context);
		sharedConstructing(context);
	}

	public TouchImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		sharedConstructing(context);
	}

	private void sharedConstructing(Context context) {
		super.setClickable(true);
		this.context = context;
		mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
		matrix = new Matrix();
		m = new float[9];
		setImageMatrix(matrix);
		setScaleType(ScaleType.MATRIX);

		setOnTouchListener(new OnTouchListener() {

			@Override
			public boolean onTouch(View v, MotionEvent event) {
				mScaleDetector.onTouchEvent(event);
				PointF curr = new PointF(event.getX(), event.getY());

				switch (event.getAction()) {
				case MotionEvent.ACTION_DOWN:
					last.set(curr);
					start.set(last);
					mode = DRAG;
					break;

				case MotionEvent.ACTION_MOVE:
					if (mode == DRAG) {
						float deltaX = curr.x - last.x;
						float deltaY = curr.y - last.y;
						float fixTransX = getFixDragTrans(deltaX, viewWidth,
								origWidth * saveScale);
						float fixTransY = getFixDragTrans(deltaY, viewHeight,
								origHeight * saveScale);
						matrix.postTranslate(fixTransX, fixTransY);
						fixTrans();
						last.set(curr.x, curr.y);
					}
					break;

				case MotionEvent.ACTION_UP:
					mode = NONE;
					int xDiff = (int) Math.abs(curr.x - start.x);
					int yDiff = (int) Math.abs(curr.y - start.y);
					if (xDiff < CLICK && yDiff < CLICK)
						performClick();
					break;

				case MotionEvent.ACTION_POINTER_UP:
					mode = NONE;
					break;
				}

				setImageMatrix(matrix);
				invalidate();
				return true; // indicate event was handled
			}

		});
	}

	public void setMaxZoom(float x) {
		maxScale = x;
	}

	private class ScaleListener extends
			ScaleGestureDetector.SimpleOnScaleGestureListener {
		@Override
		public boolean onScaleBegin(ScaleGestureDetector detector) {
			mode = ZOOM;
			return true;
		}

		@Override
		public boolean onScale(ScaleGestureDetector detector) {
			float mScaleFactor = detector.getScaleFactor();
			float origScale = saveScale;
			saveScale *= mScaleFactor;
			if (saveScale > maxScale) {
				saveScale = maxScale;
				mScaleFactor = maxScale / origScale;
			} else if (saveScale < minScale) {
				saveScale = minScale;
				mScaleFactor = minScale / origScale;
			}

			if (origWidth * saveScale <= viewWidth
					|| origHeight * saveScale <= viewHeight)
				matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2,
						viewHeight / 2);
			else
				matrix.postScale(mScaleFactor, mScaleFactor,
						detector.getFocusX(), detector.getFocusY());

			fixTrans();
			return true;
		}
	}

	void fixTrans() {
		matrix.getValues(m);
		float transX = m[Matrix.MTRANS_X];
		float transY = m[Matrix.MTRANS_Y];

		float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
		float fixTransY = getFixTrans(transY, viewHeight, origHeight
				* saveScale);

		if (fixTransX != 0 || fixTransY != 0)
			matrix.postTranslate(fixTransX, fixTransY);
	}

	float getFixTrans(float trans, float viewSize, float contentSize) {
		float minTrans, maxTrans;

		if (contentSize <= viewSize) {
			minTrans = 0;
			maxTrans = viewSize - contentSize;
		} else {
			minTrans = viewSize - contentSize;
			maxTrans = 0;
		}

		if (trans < minTrans)
			return -trans + minTrans;
		if (trans > maxTrans)
			return -trans + maxTrans;
		return 0;
	}

	float getFixDragTrans(float delta, float viewSize, float contentSize) {
		if (contentSize <= viewSize) {
			return 0;
		}
		return delta;
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		viewWidth = MeasureSpec.getSize(widthMeasureSpec);
		viewHeight = MeasureSpec.getSize(heightMeasureSpec);

		//
		// Rescales image on rotation
		//
		if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight
				|| viewWidth == 0 || viewHeight == 0)
			return;
		oldMeasuredHeight = viewHeight;
		oldMeasuredWidth = viewWidth;

		if (saveScale == 1) {
			// Fit to screen.
			float scale;

			Drawable drawable = getDrawable();
			if (drawable == null || drawable.getIntrinsicWidth() == 0
					|| drawable.getIntrinsicHeight() == 0)
				return;
			int bmWidth = drawable.getIntrinsicWidth();
			int bmHeight = drawable.getIntrinsicHeight();

			Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);

			float scaleX = (float) viewWidth / (float) bmWidth;
			float scaleY = (float) viewHeight / (float) bmHeight;
			scale = Math.min(scaleX, scaleY);
			matrix.setScale(scale, scale);

			// Center the image
			float redundantYSpace = (float) viewHeight
					- (scale * (float) bmHeight);
			float redundantXSpace = (float) viewWidth
					- (scale * (float) bmWidth);
			redundantYSpace /= (float) 2;
			redundantXSpace /= (float) 2;

			matrix.postTranslate(redundantXSpace, redundantYSpace);

			origWidth = viewWidth - 2 * redundantXSpace;
			origHeight = viewHeight - 2 * redundantYSpace;
			setImageMatrix(matrix);
		}
		fixTrans();
	}
}

13. After adding the class open your layout_fullscreen_image.xml file which used to display fullscreen image and modify ImageView to info.androidhive.imageslider.helper.TouchImageView element.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <info.androidhive.imageslider.helper.TouchImageView
        android:id="@+id/imgDisplay"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="fitCenter" />

    <Button
        android:id="@+id/btnClose"
        android:layout_width="wrap_content"
        android:layout_height="30dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginRight="15dp"
        android:layout_marginTop="15dp"
        android:paddingTop="2dp"
        android:paddingBottom="2dp"
        android:background="@drawable/button_background"
        android:textColor="#ffffff"
        android:text="Close" />

</RelativeLayout>

14. In FullScreenImageAdapter.java class also we used ImageView. Just replace this one with TouchImageView

public class FullScreenImageAdapter extends PagerAdapter {
	.
	.
	.	
	@Override
    public Object instantiateItem(ViewGroup container, int position) {
        TouchImageView imgDisplay; // Replace here with TouchImageView
	.
	// this one too
        imgDisplay = (TouchImageView) viewLayout.findViewById(R.id.imgDisplay); // this one too
	.
	.
	.
 
        return viewLayout;
	}
}

Run your project now and test the pinch zooming functionality.

Testing Pinch Zooming in Emulator

As of now Emulator is not supporting multi touch gesture. So you should use real device to test the pinch zooming functionality

I know this project has some performance issues like grid scrolling is little bit slow. Search and try to find a solution to make it better.

Happy Coding …. 🙂

Subscribe
Notify of
guest
416 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
samir
samir
7 years ago

thanks for uploading………..

nirmal
nirmal
7 years ago

very nice

iMaulik
iMaulik
7 years ago

great! & thanks

Costis
Costis
7 years ago

It is not running.
There is a message:
“NAT directory path is not valid
Please set the image directory
name AppConstant.java class”

How can I set the directory in the emulator???????
thanks

Ravi Tamada
7 years ago
Reply to  Costis

You should keep a image album name in AppConstant.java class. Load some images in sdcard and give the folder name in AppConstatn.java class.

In Eclipse use DDMS tool to create folder and push some images into it.

Costis
Costis
7 years ago
Reply to  Ravi Tamada

Thanks!!
..and congratulations for Androidhive
the best android site ever!!

partha
partha
7 years ago
Reply to  Ravi Tamada

can you tell me how can i do that

ravikant
ravikant
7 years ago
Reply to  Ravi Tamada

hii plz tell me how creat foldr in DDMS tool and then how set the path of PHOTO_ALBUM=””

Piter
Piter
7 years ago

“NAT directory path is not valid
Please set the image directory
name AppConstant.java class” 🙁

Ravi Tamada
7 years ago
Reply to  Piter

You should keep a image album name in AppConstant.java class. Load some images in sdcard and give the folder name in AppConstatn.java class.

Siva
Siva
7 years ago
Reply to  Ravi Tamada

Hi i have the viewpager to show the image and how can i implement the TouchImageView class into viewpager in my code

vineet
vineet
7 years ago
Reply to  Ravi Tamada

hi ravi,this app is running bt when images are load and show in gridview after that when i touch the image than nothing was happen..

gunz
gunz
7 years ago

How can i aoutomatically create folder “NAT” and add image to that folder.?

Ravi Tamada
7 years ago
Reply to  gunz

What do you mean by automatically ?

gunz
gunz
7 years ago
Reply to  Ravi Tamada

I mean this app will automatically create a folder called NAT, so we just insert images into the folder

Ravi Tamada
7 years ago
Reply to  gunz

In Eclipse using DDMS tools you can create a folder in Emulator memory.

If you want to create through code follow this
http://stackoverflow.com/questions/2130932/how-to-create-directory-automatically-on-sd-card

gunz
gunz
7 years ago
Reply to  Ravi Tamada

Thanks,…

Nijo George P
Nijo George P
7 years ago

I have developed a file manager application. Is it possible to integrate this image slider app with my file manager?

Ravi Tamada
7 years ago
Reply to  Nijo George P

Yes it is.

Nirmal Revar
Nirmal Revar
7 years ago

public static final String PHOTO_ALBUM = “Images”;

just Simply Write Your Album folder name,,and make sure that album has to be in Phone Memory,if that album is in SD card then it will not work…..

Ravi Tamada
7 years ago
Reply to  Nirmal Revar

Thanks Nirmal for explaining this. I already did mentioned this but not many people are noticing 🙂

Nirmal Revar
Nirmal Revar
7 years ago
Reply to  Ravi Tamada

Ya ,u r right ..

Costis
Costis
7 years ago
Reply to  Ravi Tamada

What about when I want to show in the application fotos from res -> drawable folder???

Aisha
7 years ago
Reply to  Costis

Yeah. I also want to load from res folder? How can I do that? Anyone?

Kutalp*DH
Kutalp*DH
7 years ago
Reply to  Aisha

Hey ravi i need that too how can us do that?

baha_odeh
baha_odeh
7 years ago

Nice tutorial bro , but why you didn’t use PhotoView to support zoom ?
try it
https://github.com/chrisbanes/PhotoView
its very nice and clean

Antony Kor
Antony Kor
7 years ago

Excellent tutorial Ravi, thanks once again 🙂

Ravi Tamada
7 years ago
Reply to  Antony Kor

You are welcome 🙂

JMarv
JMarv
7 years ago
Reply to  Ravi Tamada

Hello.. I’m newbie in android programming.. My concern is what’s the use of FullScreenViewActivity because I confuse on it. In your given code I don’t get the function of this class FullScreenViewActivity.. I test this code but got me error in displaying the full-screen image.. Thanks in advance.. ^_^

Christopher J
Christopher J
7 years ago

Thanks a lot. this is exactly what I was looking for.

partha
partha
7 years ago

my images are in DCIM/Camera folder , what code can i write , so that i got the images from the specified folder

Ravi Tamada
7 years ago
Reply to  partha

Then use this in AppConstant.java

public static final String PHOTO_ALBUM = “DCIM/Camera”; and try

IDS07
7 years ago

Thanks!

I launched it and the everything loads great, however its lags on scrolling up and down on grid view and the images dont auto-rotate to their taken layout (portrait/landscape) Is this the intended result?

Edit: I’m also getting crashes on the device (below) and Nexus 4 (Emulator) I checked the log OutOfMemory

On a Galaxy S 4 Google Edition GT-i9505G (4.3)

billy
billy
7 years ago

Hi, may I know if it is possible to load the image from the Asset folder instead of load the image from SD card?
Thanks in advance.

Lugati
7 years ago

Very nice Tutorial… How to load images from url links?

Karan
Karan
7 years ago

Hi.. I loved this Application. But it gives Error on Galaxy Tab2. After swiping the image 2-3 times, it Force Closes. It works fine on Xperia U.
Please have a look.

JMarv
JMarv
7 years ago

Hello.. I’m newbie in android programming.. My concern is what’s the use of FullScreenViewActivity because I confuse on it. In your given code I don’t get the function of this class FullScreenViewActivity.. I test this code but got me error in displaying the full-screen image.. Thanks in advance.. ^_^ God bless…

Omi
Omi
7 years ago

Thank You Ravi for such a great app.
I have to swipe between different layouts? how to do it can you please explain

ravikant
ravikant
7 years ago

plzz tell me yaarr I took all effort bt the problm is nt solved…. i m using android emulator in eclipse and how and where to set path for memory…. 🙁

Wayan
Wayan
7 years ago

hey Ravi, where is FullScreenViewActivity.java ? can you tell me?

Aisha
7 years ago
Reply to  Wayan

src > info.androidhive.imageslider > FullScreenViewActivity.java

Tulai Paul
Tulai Paul
7 years ago

Extensive article with full of fertility. The codes will surely help the mobile apps and games developers. We hope that availability of such free codes will not provoke the companies like admob or revmob or adcolony or appnext to reduce their ecpm.

Guest
Guest
7 years ago

whr can i download source code zip ravi bro??

Guest
Guest
7 years ago

i downloaded the source code as zip , and installed the source code from here. and when i run this app, there is pop up window appears and shows”NAT directory path is not valid! Please set the image directory name AppConstant.java class”

wat can i do for this ravi bro??

Aisha
7 years ago
Reply to  Guest

In AppConstant.java class:

Change “NAT” to the preferred location (like for example the bluetooth folder):
public static final String PHOTO_ALBUM = “bluetooth”;

vineet
vineet
7 years ago
Reply to  Aisha

hey i’m using DDMS than where i can stored the images …??

Aisha
7 years ago
Reply to  vineet

Sorry. I’m using the real device. No clue on the emulator.

vineet
vineet
7 years ago
Reply to  Aisha

its ok aisha but can u plz tell me thz …,this app is running bt when images are load and show in gridview
after that when i touch the image than nothing was happen..

Shashiraj
Shashiraj
7 years ago
Reply to  vineet

Same error here when i run this app on emulator. What may be the solution? Pls suggest…:)

Diviya
Diviya
7 years ago
Reply to  Shashiraj

Same error, While running in phone I got only the gridview. If I click any image in gridview its showing me error that your application have been stopped unexceptly.

Pratik Butani
7 years ago
Reply to  vineet

Push images in data folder for emulator.

Guest
Guest
7 years ago

i dont understand your reply bro”You should keep a image album
name in AppConstant.java class. Load some images in sdcard and give the
folder name in AppConstatn.java class.

In Eclipse use DDMS tool to create folder and push some images into it.” need more clear info bro, please elaborate it ravi bro….

Pratik Butani
7 years ago
Reply to  Guest

You must have to that folder in your SD card.

otherwise change folder name in which you have some photos.

AppConstant.java->PHOTO_ALBUM.

KP Ranjith
KP Ranjith
7 years ago

Bro pls tell me if there is any job vacancy for android developer fresher… pls mail me at [email protected] Thank You Bro….

Pushkar Pandey
Pushkar Pandey
7 years ago

Oops!

Hi Ravi!. By mistake i submitted the comment in the above bug report section. Please ignore that.

Thank you.

Pushkar Pandey
Pushkar Pandey
7 years ago

Hi Ravi!
Nice article. I’m a regular follower of your blog.

I have used Michael Ortiz pinch-zoom code in my project. Everything works just fine as expected with one minor issue.

My image is supposed to be a full-screen (screen-fit) which i was able to do by using scaleType:fitxy in my layout xml. But after the TouchImageView integration, my imageview is not screen-fit anymore. It seems scaleType:fitxy is getting overridden my ScaleType.MATRIX in the TouchImageView.java code.

My viewpager implementation is little different from yours. I’m reading & setting the bitmaps in the onCreateView() method of my FullScreenFragment.java which is a Fragment class being instantiated in the getItem() method of MyPagerAdapter inner class in my FullScreenActivity.java.

Also, in my FullScreenActivity.java I’m using..

myAdapter = new MyPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(myAdapter);

//(where MyPagerAdapter is a inner class in my FullScreenActivity.java )

instead of ….

adapter = new FullScreenImageAdapter(FullScreenViewActivity.this, utils.getFilePaths());
viewPager.setAdapter(adapter);

/* MyPagerAdapter inner class*/

public class MyPagerAdapter extends FragmentStatePagerAdapter {

public MyPagerAdapter(FragmentManager fm) {
super(fm);
}

@Override
public Fragment getItem(int position) {
Fragment fragment = new FullScreenFragment();
Bundle args = new Bundle();
args.putInt(“page”, (position));
args.putStringArray(“imageArray”, images);
global = new Bundle();
global.putInt(“page”, (position));
fragment.setArguments(args);
return fragment;
}

@Override
public int getCount() {
return GlobalState.slides.size(); // No. of pages.
}

}

Hope I’m clear about the question & description. I’ll really appreciate your help.

Thank you.

Guest
Guest
7 years ago

Hi Ravi,
Can i open images which is stored in Bluetooth folder in memory card(path:sdcard/bluetooth)?

Aisha
7 years ago
Reply to  Guest

Yeah. In AppConstant.java, change the directory to:

public static final String PHOTO_ALBUM = “bluetooth”;

fadil p
fadil p
7 years ago

It is working for me…great tutorial
Thanks Ravi..

iTechniqz
iTechniqz
7 years ago

See grid view type Android App

Animal Sounds For Kids

https://play.google.com/store/apps/details?id=com.itechniqz.animalsounds

RJ
RJ
7 years ago

hey Ravi, Very nice tutorial and worked fine for me. But my further more requirement is that when Image is display in a full screen and if any user tap on image one button should visible true. i just want to set up click event on it. how it should be done? can u guide me please

Pushkar
Pushkar
7 years ago

Hi Ravi! I’m facing an issue with a similar zoom functionality. Would really appreciate if you can take out time to check out my question at http://stackoverflow.com/questions/19155952/setting-boundary-limits-for-panning-dragging-a-zoomable-custom-relative-layout .

Thanks

Dami
Dami
7 years ago

Excellent tutorial!!! But, could you add a thread for the gridview loader?

Maringote
Maringote
7 years ago

can i have a question ? I’m curious how would the code look when i want to add this to tab fragment.

Maringote
Maringote
7 years ago
Reply to  Maringote

never mind, I managed it somehow 🙂

krishna
krishna
7 years ago

cannot find the code for FullScreenViewActivity.java

Guest
Guest
7 years ago

cannot find code for FullScreenViewActivity.java in this page

senthilkumar
senthilkumar
7 years ago
Reply to  Guest

you got the FullScreenViewActivity.java please sent me my mail-id— [email protected]

Kutalp*DH
Kutalp*DH
7 years ago

What about when I want to show in the application fotos from res -> drawable folder???

Green
Green
7 years ago

During trial run in actual Samsung device, get a print in the Logcat –
Not a DRM file, opening normally
buffer returned
Any ideal to solve it?

joy
joy
7 years ago

hi, where is the FullScreenViewActivity.java code?
I can not sign up, I did not receive verification email…

please help me…..

10x

Kutalp*DH
Kutalp*DH
7 years ago

What about when I want to show in the application fotos from res -> drawable folder???

PitBull
7 years ago

What about when I want to show in the application photo from res / drawable folder? please!!

Ravi Tamada
7 years ago
Reply to  PitBull

Use this tutorial logic to load images from drawable
http://www.androidhive.info/2012/02/android-gridview-layout-tutorial/

PitBull
7 years ago
Reply to  Ravi Tamada

Thank you! i go to see!

Hardik Kubavat
Hardik Kubavat
7 years ago
Reply to  Ravi Tamada

:disqus can u explain it.. how to do show images from drwable folder.. Plz.. !!

reza
reza
7 years ago

Is it possible to get the
value in viewpagers widget just like the one in listview ?

I have used the SharedPreferences to get access to widget’s inside value in
viewpager.

I have shared my problem in this link but it’s
not solved

http://stackoverflow.com/questions/19345628/getting-the-value-inside-the-widget-in-viewpager

The problem is that when getting back from the viewpager’s
“n” page (sliding to right) it return’s the value of the n+1 page. But it
should return the n-1 ‘s value. But it’s working fine for the other pages.

w3i
w3i
7 years ago

Thank you so much! You save my days of coding!

atomnet
atomnet
7 years ago

Hello,
is possible load image from server?

Thanks for tutorial.

Ravi Tamada
7 years ago
Reply to  atomnet
atomnet
atomnet
7 years ago
Reply to  Ravi Tamada

thanks for aswer 🙂
but no single image, but many image from internet folder ( no database) to insert in Gridwiew.

raj
raj
7 years ago
Reply to  atomnet

Hi atomnet ,
have you done this concept loading images from server , i am also looking for the same concept, if u have some idea plz help me

Steve
Steve
7 years ago

Ravi,
This is exactly what I have been looking for!! Thank you so much for sharing. I was hoping you could help me with “deleting the image once selected. I have this code

UPDATE **
With some help from Stack Overflow i got the String I needed
I Added the following to onCreate…

String path = adapter._imagePaths.get(viewPager.getCurrentItem());

which allowed me to create my fileDelete Method

amy
amy
7 years ago
Reply to  Steve

File file = new File(selectedFilePath);
boolean deleted = file.delete();

Vinod Rajan
Vinod Rajan
7 years ago

Hi Ravi,
Thanks for giving a nice tutorial, you have done a wonderful job for people who learning android.

I have one doubt.

How we can add title to image in the gridview.

Imran
Imran
7 years ago
Reply to  Vinod Rajan

Hi Vinod,

You can inflate your own layout inside Grid Adapter
view = inflater.inflate(R.layout.grid_image_item, parent, false);

grid_image_item contains Image and Text

Vinod Rajan
Vinod Rajan
7 years ago
Reply to  Imran

Thanks Imran,

It got worked.

Pratik Butani
7 years ago

How can i use Images from Database in ViewPager.

Steve
Steve
7 years ago

Hey Ravi,

Do you have a fix for the Panning issue when the image is zoomed? I found that if you press on the image and move up and then over it pans the image. For whatever reason, it’s acting as if it doesn’t know the state of the image is “ZOOM”

UPDATE…
After a week of searching and trial and error, i finally found a solution for Panning as well as adding and Double Tap for Zoom in and Zoom out. It’s a bit messy, not perfect, but works.

Here is the Updates TouchImageView.java file

package info.androidhive.imageslider.helper;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;

//Original
public class TouchImageView extends ImageView {

Matrix matrix;

// We can be in one of these 3 states
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;

// Remember some things for zooming
PointF last = new PointF();
PointF start = new PointF();
float saveScale = 1f;
float minScale = saveScale;
float maxScale = 10f;
float[] m;

int viewWidth, viewHeight;
static final int CLICK = 3;

protected float origWidth, origHeight;
int oldMeasuredWidth, oldMeasuredHeight;

ScaleGestureDetector mScaleDetector;
private GestureDetector gestureDetector;

Context context;

public TouchImageView(Context context) {
super(context);
sharedConstructing(context);
}

public TouchImageView(Context context, AttributeSet attrs) {
super(context, attrs);
sharedConstructing(context);
}

private void sharedConstructing(Context context) {
gestureDetector = new GestureDetector(new DoubleTapGestureListener());
super.setClickable(true);
this.context = context;
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());

matrix = new Matrix();
m = new float[9];
setImageMatrix(matrix);
setScaleType(ScaleType.MATRIX);

setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {

if (saveScale > 1f) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}

gestureDetector.onTouchEvent(event);

mScaleDetector.onTouchEvent(event);
PointF curr = new PointF(event.getX(), event.getY());

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mode == ZOOM) {
//mode = DRAG;
last.set(curr);
start.set(last);
} else {
last.set(curr);
start.set(last);
mode = DRAG;
}
break;

case MotionEvent.ACTION_MOVE:
if (mode == DRAG) {
float deltaX = curr.x – last.x;
float deltaY = curr.y – last.y;
float fixTransX = getFixDragTrans(deltaX, viewWidth,
origWidth * saveScale);
float fixTransY = getFixDragTrans(deltaY, viewHeight,
origHeight * saveScale);
matrix.postTranslate(fixTransX, fixTransY);
fixTrans();
last.set(curr.x, curr.y);
}
break;

case MotionEvent.ACTION_UP:
mode = NONE;
int xDiff = (int) Math.abs(curr.x – start.x);
int yDiff = (int) Math.abs(curr.y – start.y);
if (xDiff < CLICK && yDiff minScale) {
zoomOut();
} else {
zoomIn();
}
return true;
}

}

float oldScale = 1f;

public boolean isZoomed() {
return saveScale > minScale; // this seems to work
}

@SuppressLint(“WrongCall”)
public void zoomIn() {
//LogUtil.i(TAG, “Zooming in”);
oldScale = saveScale;
saveScale *= 6f;
matrix.setScale(saveScale, saveScale);
setImageMatrix(matrix);
onMeasure(viewHeight, viewWidth);
invalidate();

}

@SuppressLint(“WrongCall”)
public void zoomOut() {
//LogUtil.i(TAG, “Zooming out”);
saveScale = oldScale;
matrix.setScale(saveScale, saveScale);
setImageMatrix(matrix);
onMeasure(oldMeasuredHeight, oldMeasuredWidth);
invalidate();

}

public void setMaxZoom(float x) {
maxScale = x;
}

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mode = ZOOM;
return true;
}

@Override
public boolean onScale(ScaleGestureDetector detector) {
float mScaleFactor = detector.getScaleFactor();
float origScale = saveScale;
saveScale *= mScaleFactor;
if (saveScale > maxScale) {
saveScale = maxScale;
mScaleFactor = maxScale / origScale;
} else if (saveScale < minScale) {
saveScale = minScale;
mScaleFactor = minScale / origScale;
}

if (origWidth * saveScale <= viewWidth
|| origHeight * saveScale <= viewHeight)
matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2,
viewHeight / 2);
else
matrix.postScale(mScaleFactor, mScaleFactor,
detector.getFocusX(), detector.getFocusY());

fixTrans();
return true;
}
}

void fixTrans() {
matrix.getValues(m);
float transX = m[Matrix.MTRANS_X];
float transY = m[Matrix.MTRANS_Y];

float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
float fixTransY = getFixTrans(transY, viewHeight, origHeight
* saveScale);

if (fixTransX != 0 || fixTransY != 0)
matrix.postTranslate(fixTransX, fixTransY);
}

float getFixTrans(float trans, float viewSize, float contentSize) {
float minTrans, maxTrans;

if (contentSize <= viewSize) {
minTrans = 0;
maxTrans = viewSize – contentSize;
} else {
minTrans = viewSize – contentSize;
maxTrans = 0;
}

if (trans maxTrans)
return -trans + maxTrans;
return 0;
}

float getFixDragTrans(float delta, float viewSize, float contentSize) {
if (contentSize <= viewSize) {
return 0;
}
return delta;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewWidth = MeasureSpec.getSize(widthMeasureSpec);
viewHeight = MeasureSpec.getSize(heightMeasureSpec);

//
// Rescales image on rotation
//
if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight
|| viewWidth == 0 || viewHeight == 0)
return;
oldMeasuredHeight = viewHeight;
oldMeasuredWidth = viewWidth;

if (saveScale == 1) {
// Fit to screen.
float scale;

Drawable drawable = getDrawable();
if (drawable == null || drawable.getIntrinsicWidth() == 0
|| drawable.getIntrinsicHeight() == 0)
return;
int bmWidth = drawable.getIntrinsicWidth();
int bmHeight = drawable.getIntrinsicHeight();

Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);

float scaleX = (float) viewWidth / (float) bmWidth;
float scaleY = (float) viewHeight / (float) bmHeight;
scale = Math.min(scaleX, scaleY);
matrix.setScale(scale, scale);

// Center the image
float redundantYSpace = (float) viewHeight
– (scale * (float) bmHeight);
float redundantXSpace = (float) viewWidth
– (scale * (float) bmWidth);
redundantYSpace /= (float) 2;
redundantXSpace /= (float) 2;

matrix.postTranslate(redundantXSpace, redundantYSpace);

origWidth = viewWidth – 2 * redundantXSpace;
origHeight = viewHeight – 2 * redundantYSpace;
setImageMatrix(matrix);
}
fixTrans();
}
}

I hope it helps someone else.

-Steve

sara
sara
7 years ago
Reply to  Steve

Hi Steve,
Could you please help me?
I want to zoom in the axis of photo that i double click on it, how should i do it?

Kwstas Antoniou
Kwstas Antoniou
7 years ago

Its Laggy when i load image to fullscreen ?

karan shah
karan shah
7 years ago

hello sir your tutorial is so good. i really inspire from your tutorial.
i want only one thing. in this tutorial how i get images from internal
storage. means i want to get all images from raw folder ya assets
folder. i want to give images in build in my application.

416
0
Would love your thoughts, please comment.x
()
x