Sunday, March 2, 2014

Making a Gallery

    During my last summer of college, I had an interview for a possible internship that asked me how I would allow users to see a series of images quickly before allowing them to see a better quality one. Given that I knew a lot less then than I do now (which still isn't much :)), I gave an answer that described how it would work with a view pager, without knowing what a view pager was. While this was close, it didn't cover the 'quick' aspect and didn't cover any of the implementation details. Since I didn't get the job (which in hindsight is awesome, as I ended up in gorgeous Boulder, Colorado, with a great team rather than heading down to Los Angeles), I decided to at least work out how I would solve this problem.  As with all of my posts, the source code for this project is available on my GitHub.

    For this project I am going to use online images so that their URLs can be passed to the app from a JSON stream with some additional data. This JSON stream is then parsed using GSON into a Gallery model object that contains an ArrayList of Image objects. Each Image object contains a caption for the image, the URL for the higher resolution version of the image, and a thumbnail for the image. The thumbnails are displayed in a GridView, and the higher resolution images with captions are displayed in a separate activity with a ViewPager. The final product with very little styling (mind you, if this were to be a production app then more styling would be a necessity) will look like this:


    As can be seen in the first image, the main activity is simply a grid of images. Instead of using a standard ImageView, I have a SquareImageView class that takes the images and displays them in a square in order to keep the rows uniform. This is done by calling setMeasuredDimension to make the image height equal the width when the view calls onMeasure

SquareImageView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int dimension = getDefaultSize( getSuggestedMinimumWidth(), widthMeasureSpec );
    setMeasuredDimension(dimension, dimension);
}

    The feed is pulled down in MainActivity using Volley and GSON to put the JSON data into the model objects:

MainActivity.java
private void loadFeed() {
    String feedUrl = getString( R.string.feed_url );
    GsonRequest<Gallery> request = new GsonRequest<Gallery>( Request.Method.GET, 
        feedUrl, Gallery.class, successListener(), errorListener() );
    Volley.newRequestQueue( getApplicationContext() ).add( request );
}
public Response.Listener successListener()
    {
        return new Response.Listener<Gallery>()
        {
            @Override
            public void onResponse( Gallery gallery ) {
                mGallery.setImages( gallery.getImages() );
                mGallery.setDescription( gallery.getDescription() );
                setupUI();
            }
        };
    }

   setupUI() adds all of the images from the gallery into an ArrayAdapter for the GridView, and adds a click listener to open an activity for viewing the full resolution images. The adapter for the grid uses the viewholder pattern and the Picasso library to load the thumbnail images into the grid, as can be seen in GalleryGridAdapter.java. When an image is clicked in the grid, all images are passed to the next activity as well as the clicked position, allowing the ImageDetailsActivity to display the ViewPager at the correct location

MainActivity.java
mGridView.setOnItemClickListener( new AdapterView.OnItemClickListener()
 {
    @Override
    public void onItemClick(AdapterView<?> adapterView, 
      View view, int position, long id) {
       Intent intent = new Intent( getApplicationContext(), ImageActivity.class );
       intent.putExtra( ImageActivity.EXTRA_IMAGE_LIST, 
           (ArrayList) mGallery.getImages() );
       intent.putExtra( ImageActivity.EXTRA_CUR_IMAGE, position );
       startActivity( intent );
 }

ImageDetailsActivity.java
if( getIntent() == null || getIntent().getExtras() == null )
    return;

List<Image> tmpList = getIntent().getExtras()
        .getParcelableArrayList( EXTRA_IMAGE_LIST );
mCurrentImagePosition = getIntent().getExtras().getInt( EXTRA_CUR_IMAGE, 0 );
mAdapter = new ImageStateViewPager( getSupportFragmentManager(), tmpList );

    ImageActivity takes the images and loads them into a FragmentStatePagerAdapter which returns a new instance of ImageDetailsFragment with passed Image object from getItem.

ImageStateViewPager.java
@Override
    public Fragment getItem( int position ) {
        return( position < 0 || position > ( mImageList.size() - 1 ) ) ? null :
            ImageFragment.newInstance( mImageList.get( position ) );
    }

    This fragment displays a progress spinner until Picasso has loaded the external image, and displays a caption that can have visibility toggled by tapping on the image. By using a ViewPager, the user is able to swipe left or right to view additional images while only keeping a max of three images in memory at a time.

    And with that, we have a simple gallery component for Android. There's a lot of modifications that can be made to this, such as adding progress spinners for the grid as the initial feed is loaded, and spinners for each image object as Picasso loads them into the grid, as well as styles to show overlays and change spacing in the grid. Overall the gallery is a useful component for displaying graphical content to your users, and makes a great addition to any Android developer's arsenal of UI components.