Tutorial:Camera and Gallery Demo

From Notes

Jump to: navigation, search

This tutorial will focus on using Android's built-in Camera and Gallery API to take share photos. We will create two Java classes:

  • CameraGalleryDemo will be our Activity class. It will handle interactions with the user via a simple menu.
  • ImageAdapter will manage the insertion of images into the Gallery.

Contents

Some Limitations

  • Currently, for Android 1.5 (cupcake), this demo will only work on a phone device. There seems to be a problem with the emulator's camera functionality in 1.5.
  • Also, this demo uses MediaStore.ACTION_IMAGE_CAPTURE, Android's built-in API for taking and saving Camera images See Android Reference Manual. However, there seems to be an outstanding issue with saving full-sized images in 1.5. See the discussion at: this issues site.

Concepts

This demo/tutorial will make use of several key Android concepts. It will illustrate how to start the Camera Activity, one of Android's many implicit or standard activities. These allow a developer to re-use existing functionality. It illustrates how take pictures with the built-in Camera and save and retrieve the photos from the Android MediaStore, one of its many ContentProviders.

For more on these topics see the References, below.

The Project Resources

Create a new project named CameraGalleryDemo, using the same name as the name of the main Activity class. We will need a few icons to make our menus. Copy the following images into your project's /res/drawable folder: File:Androidmarker.png, File:Camera icon 32x32.png. The first will serve as a placeholder in the main view, to be replaced by the camera image when a photo is taken. The second will be used for the menu items.

The main menu will consist of three items:

  • Big picture -- take a full size photo and save it.
  • Small picture -- take a small photo and save it.

Here's the XML code for main.xml, which should go in your /res/menu folder:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/big_picture" 
	android:title="Big Image" 
	android:icon="@drawable/camera_icon_32x32">
	</item>
<item android:id="@+id/small_picture" 
	android:title="Small Image" 
	android:icon="@drawable/camera_icon_32x32">
	</item>
</menu>

User Interface

The user interface will consist of a main View with three elements:

  • A TextView that says Camera Demo.
  • An ImageView that will store the last photo taken with the camera.
  • A Gallery that will store all the photos on the phone's SD card.

The following XML code should be placed in /layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:textSize="28px"
    android:textColor="@color/green"
    android:text="Camera Demo"
    />

<ImageView android:id="@+id/thumbnail"
            android:src="@drawable/androidmarker"
            android:adjustViewBounds="true"
            android:layout_width="64px"
            android:layout_height="64px" 
            android:layout_gravity="right"	
             /> 	
<Gallery 
		android:id="@+id/picturesTaken" 
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"/>
</LinearLayout>

Note that if you want green text, you have to define it in your /res/values/strings.xml file:

<color name="green">#00FF00</color>

The Activity Class

The Activity class will require the following fields and constants, most of which will be explained as we develop the code:

private static final String TAG = "CameraGalleryDemo";
private static final int CAMERA_ACTIVITY = 0;

private Cursor mCursor;
private Gallery mGallery;
private ImageView mImageView;
private Intent mIntent;

The onCreate() method will set up the main View and display the Gallery, which will consist of all the photos stored on the Camera's sdcard. We will describe the displayGallery() method later. For now you can comment-out that line of the program.

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	mGallery = (Gallery)findViewById(R.id.picturesTaken);
	mImageView = (ImageView)findViewById(R.id.thumbnail);
	displayGallery();
}

The onResume() method is called when control returns to this Activity from a sub-Activity (or some other Activity). It resets the ImageView and reloads the Gallery. This prevents crashes in case the user has deleted an image in the sub-activity.

/**
 * Reloads the Gallery to prevent crashes in case the user
 *  has deleted an image in a sub-Activity.
 */
@Override
protected void onResume() {
	super.onResume();
	mImageView.setImageResource(R.drawable.androidmarker);
	displayGallery();
}	

Next, the onCreateOptionsMenu() method will build the App's menu from the resource file:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
	MenuInflater inflater = getMenuInflater();
	inflater.inflate(R.menu.main, menu);
	return true;
}

A Utility Method

Here's a method that allows you to display a brief and temporary message in the View using a Toast widget:

private void showToast(Context mContext, String text) {
	Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
}

Taking and Saving a Photo

The onMenuItemSelected() menu will handle the two menu items defined in the menu resource, which would give it the following basic structure:

@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
	mIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);	

	switch(item.getItemId()) {
	case R.id.small_picture:
		startActivityForResult(mIntent, CAMERA_ACTIVITY);
		break;
	}
	case R.id.big_picture:
		mIntent.putExtra(MediaStore.EXTRA_OUTPUT, 
				MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()); 
		startActivityForResult(mIntent, CAMERA_ACTIVITY);
		break;

	return true;	
}

As this code shows, it is very simple to start the CAMERA_ACTIVITY. You create an intent with the built-in MediaStore.ACTION_IMAGE_CAPTURE value. Then you start the Activity. This is an example of an implicit or standard intent--you don't need to provide an Activity class in the startActivity() method. Android will figure that out.

Note the difference between the small picture and big picture. Here's what the Android documentation says about this:

Standard Intent action that can be sent to have the camera application capture an image and return it.

The caller may pass an extra EXTRA_OUTPUT to control where this image will be written. If the
 EXTRA_OUTPUT is not present, then a small sized image is returned as a Bitmap object in the 
extra field. This is useful for applications that only need a small image. If the EXTRA_OUTPUT 
is present, then the full-sized image will be written to the Uri value of EXTRA_OUTPUT.

However, as we noted above, there is apparently a problem in 1.5--namely, a small images seems to be returned in either case. Nevertheless, we will provide the code that should work, as the documentation suggests.

The onActivityResult() method is used to take appropriate action when the Camera Activity finishes and returns the captured image:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
	super.onActivityResult(requestCode, resultCode, intent);
	if (resultCode == RESULT_CANCELED) {
		showToast(this,"Activity cancelled");
		return;
	}
	switch (requestCode) {		
	case CAMERA_ACTIVITY: 
		Bitmap bm = (Bitmap) intent.getExtras().get("data");
		mImageView.setImageBitmap(bm); // Display image in the View
		
		Bundle b = mIntent.getExtras();
		if (b != null && b.containsKey(MediaStore.EXTRA_OUTPUT)) { // large image?
			showToast(this,"Large image");
		        // Shouldn't have to do this ... but
			MediaStore.Images.Media.insertImage(getContentResolver(), bm, null, null);
		} else {
			showToast(this,"Small image");
			MediaStore.Images.Media.insertImage(getContentResolver(), bm, null, null);
		}
		break;
	}
	displayGallery();
}

Unless the Activity was canceled, we begin by retrieving the bitmap from the returned Intent. We display the bitmap in the View. Android will automatically scale the image to fit the size of the Image View.

We then save the image using the static MediaStore.Images.Media.insertImage() method. This saves the image and creates two thumbnails for it--a mini thumbnail (320x240) and a micro (50x50). (Footnote: See this post for a discussion of the thumbnail policy in Android, pre- and post-cupcake.)

The image and thumbnails are stored, respectively, in the following ContentProviders on the sdcard. The bitmaps themselves are stored at the locations shown in parentheses:

MediaStore.Images.Media.EXTERNAL_CONTENT_URI     (Location: /sdcard/DCIM/Camera)
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI  (Location: /sdcard/DCIM/.thumbnails)

You can see the image and thumbnail files while the App is running by using adb to start a shell and then listing the contents of the MediaStore. From within the tools directory in your android directory:

$ ./adb shell
$ ls sdcard/DCIM/.thumbnails
1244214202827.jpg
1244214203057.jpg
1244214219383.jpg
1244214219533.jpg
$ ls sdcard/DCIM/Camera
1244214202496.jpg
1244214219111.jpg

Displaying the Gallery

We are going to retrieve the Gallery images from the MediaStore, one of the Android ContentProviders. The specific Provider we use is android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI. (See Android Reference Manual. This is where the images themselves have been stored. So we will be retrieving the images themselves. It seems like we should rather be retrieving the thumbnails. But until we figure out how to select one of the two thumbnails per image, we will do it this way.

To retrieve the images from the ContentProvider we use the Activity.managedQuery() method. (See Android Reference Manual. This is a wrapper around query(android.net.Uri, String[], String, String[], String) that gives the resulting Cursor to call startManagingCursor(Cursor) so that the activity will manage its lifecycle for you. Its parameters and return values are:

  • uri The URI of the content provider to query.
  • projection List of columns to return.
  • selection SQL WHERE clause.
  • selectionArgs The arguments to selection, if any ?s are pesent
  • sortOrder SQL ORDER BY clause.
  • Returns The Cursor that was returned by query().

In this case we are interested in three columns: the ID of the thumbnail (_ID), the ID of the corresponding image (IMAGE_ID), and the KIND (mini or micro). In addition to these columns, we also have to specify that we want it to select only those thumbnails whose KIND = MINI_KIND. That goes into the third parameter. The remaining parameters can be set to null.

private void displayGallery() {
	Uri uri = MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI; // Where images are stored
	String[] projection = {
		MediaStore.Images.ImageColumns._ID,  // The columns we want
		MediaStore.Images.Thumbnails.IMAGE_ID,  
		MediaStore.Images.Thumbnails.KIND };
	String selection = MediaStore.Images.Thumbnails.KIND + "="  + // Select only mini's
	        MediaStore.Images.Thumbnails.MINI_KIND;
	mCursor = this.managedQuery(uri, projection, selection, null, null);	
	if (mCursor != null) { 
		mCursor.moveToFirst();
		ImageAdapter adapter = new ImageAdapter(mCursor, this);
		Log.i(TAG, "displayGallery(), adapter = " + adapter.getCount());
		mGallery.setAdapter(adapter);
		mGallery.setOnItemClickListener(this);
	} else 
		showToast(this, "Gallery is empty.");
}	

The query returns a Cursor, an object that stores the results of a query in a table-like structure, with rows and columns (See Android Reference Manual.) We will pass this structure to the ImageAdapter which will handle the insertion of the images into the Gallery. Note that we create an onItemClickListener() for the Gallery. This allows us to expand images when they are clicked on. Make sure to implement OnItemClickListener in order for this to work.

The OnItemClickListener

Recall that when we created the Gallery, we assigned it an OnItemClickListener to handle clicks on the images in the Gallery. The method to handle that task is here:

/**
 *  Called when the user clicks on a thumbnail in the Gallery. It retrieves the
 *  associated image and starts an ACTION_VIEW activity.
 *  @param arg0 is the Adapter used by the Gallery, the calling object
 *  @param arg1 is the thumbnail's View
 *  @param position is the thumbnail's position in the Gallery
 *  @param arg3 is unused
 */
public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
    try {
	mCursor.moveToPosition(position);
	long id = mCursor.getLong(mCursor.getColumnIndexOrThrow(MediaStore.Images.Thumbnails.IMAGE_ID));
	//create the Uri for the Image 
	Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id+"");
	Intent intent = new Intent(Intent.ACTION_VIEW);
	intent.setData(uri);
	startActivity(intent);
    } catch (CursorIndexOutOfBoundsException e) {
	Log.i(TAG, "CursorIndexOutOfBoundsException " + e.getStackTrace());
}

This method will be called by the Gallery object whenever the user clicks on one of its images. It id passrf the position of the thumbnail in the Gallery. It looks up the IMAGE_ID and sets the image's Uri and then starts an ACTION_VIEW activity. This is the most common of Android's standard or implicit Activities. It is a generic action that you can use on a piece of data to get the most reasonable action to occur. For example, when used on a mailto:URI, it will bring up a compose window. When used on an image URI, as in this case, it will bring up an image viewer. (See reference.)


The ImageAdapter

The ImageAdapter manages the insertion of images into the Gallery. ImageAdapter is a subclass of android.widget.BaseAdapter and implements the android.widget.Adapter interface. Its getView() method displays the data at the specified position--in this case, the image at the specified row of the cursor. Within the method you can either create a View manually or inflate it from an XML layout file. When the View is inflated, the parent View (GridView, ListView...) will apply default layout parameters unless you use inflate(int, android.view.ViewGroup, boolean) to specify a root view and to prevent attachment to the root.

The parameters and return values of this method include (from Android Reference Manual):

  • position -- The position of the item within the adapter's data set of the item whose view we want.
  • convertView -- The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view.
  • parent -- The parent that this view will eventually be attached to
  • Returns -- A View corresponding to the data at the specified position.

In this specific case, getView() will be called repeatedly for each row of the Cursor we passed the Adapter and will assign each image to an ImageView within the Gallery. Create a class ImageAdapter in the same package with superclass of BaseAdapter. You will see that there are a few methods that must be implemented, one of which is the getView() method. Change the method so it looks like this:

   public View getView(int position, View convertView, ViewGroup parent) {
	Log.i(TAG, "Get view = " + position);
	ImageView i = new ImageView(mContext);
    	mCursor.requery();
    	  	
    	if (convertView == null) {
    	    mCursor.moveToPosition(position);
    	    int id = mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
    	    Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ""+id);
    	    Log.i(TAG, "Image Uri = " + uri.toString());
    	    i.setImageURI(uri);
    	    i.setScaleType(ImageView.ScaleType.FIT_XY);
    	    i.setLayoutParams(new Gallery.LayoutParams(136, 136));
    	    i.setBackgroundResource(mGalleryItemBackground);
    	}
    	return i;
    }

Make sure to also change the other methods that have to be implemented. getItem() and getItemId() should both return position, and getCount() should return mCursor.getCount()

We retrieve the ID of the image from the Cursor and construct its Uri. We then put the image into the ImageView, which is then sized appropriately for the Gallery.

Of course, you will also need a constructor:

public ImageAdapter(Cursor cursor, Context c) {
    mContext = c;
    mCursor = cursor;
    // See res/values/attrs.xml for the  defined values here for styling
    TypedArray a = mContext.obtainStyledAttributes(R.styleable.Gallery1);
    mGalleryItemBackground = a.getResourceId(
            R.styleable.Gallery1_android_galleryItemBackground, 0);
    a.recycle();
	Log.i(TAG , "ImageAdapter count = " + getCount());
}

Make sure to import all necessary classes and create all necessary fields. Finally, you will see that you have errors pertaining to R.styleable. In order to get rid of those errors, you need to create a new file in res/values called attrs.xml with this code in it:

<?xml version="1.0" encoding="UTF-8"?>
<!--
 Copyright (C) 2008 ZXing authors

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
<resources>
  <declare-styleable name="ViewfinderView">
  </declare-styleable>
      <declare-styleable name="TogglePrefAttrs">
        <attr name="android:preferenceLayoutChild" />
    </declare-styleable>
    
    <!-- These are the attributes that we want to retrieve from the theme
         in view/Gallery1.java -->
    <declare-styleable name="Gallery1">
        <attr name="android:galleryItemBackground" />
    </declare-styleable>
    
     <declare-styleable name="LabelView">
        <attr name="text" format="string" />
        <attr name="textColor" format="color" />
        <attr name="textSize" format="dimension" />
    </declare-styleable>
</resources>

This will allow you to style the page. Now you can run it!

Running the App

The source code is here: Media:camera-gallery.zip. Download the zip file. Unzip it and then use it to create a new Android project.

Challenges

  1. Figure out how to save a full-size image. (This may require some kind of Android fix??)
  2. Figure out how to distinguish the images saved with this app from the other images on the Phone. Then just display this App's images in the Gallery. (See Tutorial:Camera/Gallery_Part_II.)

References

  1. Intents and Intent Filters.
  2. ContentProviders.
  3. MediaStore.ACTION_IMAGE_CAPTURE.
  4. android.widget.BaseAdapter
  5. Activity.managedQuery().
  6. Adapter.getView().
  7. Cursor.
  8. ACTION_VIEW Activity.
Personal tools
NSF K-12