Android custom gallery with the custom image view

This application is used to create custom gallery view with albums (buckets) similar to mobile gallery. Display the images as slide show with double tap zoom functionality and pinch zoom feature using custom ImageView in android.

How to fetch and display images from android mobile gallery with albums?
In this document, we will discuss about custom gallery view and custom image view. Custom image view will support for pinch zoom and double tap zoom functionality.

screenshot_2016-10-20-13-15-08_nexus6p-portrait          screenshot_2016-10-20-13-15-28_nexus6p-portrait

Features currently achieved are,

  • Fetching gallery albums (buckets) and its images.
  • Sliding gallery view.
  • Integrating pinch zoom feature and double tap zoom functionality.

Prerequisites:
Android studio
Basic knowledge of RecyclerView and View Pager

Getting started
Let’s get started by creating an android application and follow the steps below.

Step 1. Add permission to manifest file.
Add read permission in AndroidManifest.xml.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Step 2. Define retrieving image source.
Retrieve image from external storage in Gallery Picker Adapter(i.e, recycler adapter) as shown below

public static final Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;

The below code will help you to search the images everywhere on the “primary” external storage.

public static final String[] projections = 
        {MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DATA,
        MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
        MediaStore.Images.Media.BUCKET_ID,
        MediaStore.Images.Media.DISPLAY_NAME};

MediaStore.Images.Media._ID :Each image has an ID associated with it,which can be used to getting the thumbnail of that image.
MediaStore.Images.Media.DATA: It is full Path of the file.
MediaStore.Images.Media.BUCKET_DISPLAY_NAME: It is used to display bucket name of the image.
MediaStore.Images.Media.BUCKET_ID: The bucket id of the image.
MediaStore.Images.Media.DISPLAY_NAME: It is the name of the file.
Sort images according to DATA with descending order.

public static String sortOrder = MediaStore.Images.Media.DATA + " DESC";

Step 3. Implement Loader Manager.
Here implement LoaderManager.LoaderCallbacks<Cursor> in Gallery Activity to get gallery images.

 @Override
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
        //create and return CursorLoader. that will take care of creating a Cursor for the data being displayed by uri. and projections and sort the images (showed in step 2)
        return new CursorLoader(getApplicationContext(),
                GalleryPickerAdapter.uri,
                GalleryPickerAdapter.projections,
                null,
                null,
                GalleryPickerAdapter.sortOrder);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {      
        //set data to adapter from PhotosData (shown in step 4) by passing cursor
        //1st param true that will check to load buckets in PhotosData.getData method
        adapter.setData(PhotosData.getData(true, cursor));
    }
}

Step 4. Get Images from LoaderManager.
Below function is used to set gallery albums(buckets) and its images return from LoaderManager showed in step 3. Here we use cursor data to get all image details.

public static List<PhotosModel> getData(boolean home, Cursor cursor) {
    boolean dir = home;//used in GalleryPickerAdpater to load either image on buckets
    List<PhotosModel> photos = new ArrayList<>();
    List<String> bucketIds = new ArrayList<>();
    List<String> imagePaths = new ArrayList<>();
    String[] projections = GalleryPickerAdapter.projections;

    //Using cursor data get all image details as we discussed in step 2 
    while (cursor.moveToNext()) {
        String imageName = cursor.getString(cursor.getColumnIndex(projections[4]));
        String imageBucket = cursor.getString(cursor.getColumnIndex(projections[2]));
        String bucketId = cursor.getString(cursor.getColumnIndex(projections[3]));
        String imagePath = cursor.getString(cursor.getColumnIndex(projections[1]));
        PhotosModel model;

        //if it is true add buckets into PhotosModel
        if (home) {
            model = new PhotosModel(bucketId, imageBucket, imagePath, null);

            //check to overcome duplicate bucketIds
            if (!bucketIds.contains(bucketId)) {
                photos.add(model);
                bucketIds.add(bucketId);
            }
        } else {
            //used to add images into PhotosModel
            model = new PhotosModel(bucketId, imageBucket, imagePath, imageName);

            //check to overcome duplicate image path
            if (!imagePaths.contains(imagePath)) {
                photos.add(model);
                imagePaths.add(imagePath);
            }
        }
    }
    return photos;
}

Step 5. Setting up images in GalleryPickerAdapter
Here we will set gallery albums and its images to RecyclerView.

@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
    final PhotosModel model = data.get(position);
            //to get image pass image path to BitmapDecoder.decodeBitmapFromFile(showed in next step)
            Bitmap thumb = BitmapDecoder.decodeBitmapFromFile(model.getImagePath(), 180, 180);
            holder.iv_grid.setImageBitmap(thumb);
    //if true display bucket name or image name to text view
    //if not change the visibility of text view to gone
    if (PhotosData.dir) {
        holder.tv_grid.setText(model.getImageName() == null ? model.getImageBucket() : model.getImageName());
    } else {
        holder.tv_grid.setVisibility(View.GONE);
    }

here, display the images while clicking item of RecyclerView. If we click on albums, display all images in grid view.

View itemView = holder.itemView;
itemView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

        //if true view image as grid view
        if (PhotosData.dir) {
            Intent intent = new Intent(context,ImagesGridActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.putExtra("POS",position);
            context.startActivity(intent);
        } else {

            //add all images to new array list,used to view image as slide show
            ArrayList<String> paths = new ArrayList<>();
            for (int i = 0; i < data.size(); i++) {
                paths.add(data.get(i).getImagePath());
                Log.d("Paths", paths.get(i));
            }
            Intent intent = new Intent(context, ImageViewActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.putStringArrayListExtra("paths", paths);
            intent.putExtra("Position",position);
            context.startActivity(intent);
        }
    }
});

Image Grid Activity is similar to Gallery Activity(step 3). Here instead of passing 3rd parameter as null in CursorLoader need to pass selection data query. It will help to display selected bucket images as shown below

GalleryPickerAdapter.projections[3] + " = "" + GalleryPickerAdapter.data.get(position).getBucketId() + """,

Image View Activity is used to view image as slide show using ViewPager. Set image position using addOnPageChangeListener at public void onPageSelected(int position) as shown below.

@Override
   public void onPageSelected(int position) {
       //set current image position using position increment with 1
       currentCount.setText((position+1)+"");
   }

Here we use custom ImageView (i.e, TouchImageView) for double tap zoom and Pinch zoom functionality. Pass Image path ArrayList to ViewPager adapter extend with PagerAdapter to view image as slide show.

pageradapter = new ViewPagerAdapter(context,list);//list of image path

inside adapter set the bitmap images to the custom image view as shown below.

@Override
public Object instantiateItem(ViewGroup container, int position) {
    View itemView = mLayoutInflater.inflate(R.layout.pager_item, container, false);

    //Custom ImageView 
    TouchImageView imageView = (TouchImageView) itemView.findViewById(R.id.pager_image);

    //set Images to custom imageview
    imageView.setImageBitmap(BitmapFactory.decodeFile(list.get(position)));
    container.addView(itemView);
    return itemView;
}

following xml file contains TouchImageView. It will help to zooming functionality.

<!--TouchImageView package name-->
<com.designstring.customgallery.TouchImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/pager_image"
    android:layout_margin="5dp"
    />

Step 6. “DecodeBitMapFromFile” function.
Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface. Hence we use loading large bitmaps efficiently to image view in recycler grid items.

public static Bitmap decodeBitmapFromFile(String imagePath,
                                          int reqWidth,
                                          int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(imagePath, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(imagePath, options);
}
public static int calculateSampleSize(BitmapFactory.Options options,
                                      int reqHeight,
                                      int reqWidth) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and
        // keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

Conclusion
Final result will look like this

      

Download final project : here