Writing a Real Android App from Scratch: Part 4/9 – Camera, Gallery, and Custom Action Bar
Welcome to part 4 of the Writing a Real Android App from Scratch series.
At this point, our app is capable of getting user’s current location and displaying the human-readable address for that location. Now, we need a way to add details to that location. The details, in our case, will contain a description, a category, and a picture. We already have the address so the user doesn’t have to enter any. Adding a description and a category is easy – we just need a text view, and a spinner view. Selecting a picture from gallery or taking a picture with the camera is more difficult, and more engaging. We have to lot to cover in this part – Gallery, camera, custom Action Bar, and other small details. I could have covered all of these one by one breaking into different tutorial parts but these are related, and we want to get one feature done in one part. So, this part the longest part of this tutorial series. Let’s get started.
Pre-requisite:
Read previous parts of this series:
Do some readings on Intents and Intent Filters.
Read this Google+ post from Roman Nurik about implementing an ActionBar with Done+Discard menu items.
Clone this project from GitHub; start with the
v0.3.1
tag:git checkout –b add_details v0.3.1
Plan of Action:
Hook up the ‘tag button’ to take the user to a different activity.
We don’t want to make
CurrentFragment
decide what to do after user selects the tag button because in the future we will be saving the details to a database, and we don’t want to litter all our fragments with database related code. We will letDefaultActivity
decide what it wants to do with the address thatCurrentFragment
provides.Add a layout for adding details as well as add the corresponding activity.
- Add support for picking up a gallery picture and for taking picture with a camera.
- Add a way to discard or accept the details.
Hooking up the tag button:
At the end of previous part, we left hooking up the tag button that takes you to a different activity for adding details, and a picture. As discussed before, we want DefaultActivity
to handle whatever it wants to do with the address. This means we will callback the DefaultActivity
with the reverse geocoded address after the user selects the button. When I said callback, you might have guessed it already – we are going to use an interface that has just one public method – onAddressAvailable()
and takes an Address as a parameter. But wait! We already have that interface – ReverseGeocodingListener
, which declares the exact same method we are after. The only problem is its name — doesn’t sound quite right for what we want to do. Let’s change it.
1. Rename ReverseGeocodingListener
class name to AddressResultListener
. Don’t forget to change the name of the file itself as well as all the references to this interface in CurrentFragment
, and ReverseGeocodingService
classes.
2. Make DefaultActivity
implement AddressResultListener
interface:
1 //file: src/main/java/com.ashokgelal.tagsnap/DefaultActivity.java
2 public class DefaultActivity extends SherlockFragmentActivity implements AddressResultListener {
3 ...
4 @Override
5 public void onAddressAvailable(Address address) {
6 }
7 ...
8 }
As we know, the root activity of our app is DefaultActivity
and CurrentFragment
is a child of it. The DefaultActivity
knows about the CurrentFragment
but the reverse is not true. How can we then callback the DefaultActivity
? By enforcing the parent activity to implement the AddressResultListener
interface, which we already did in step 1 and 2 above. Now, inside the CurrentFragment
we need to make sure that the parent, whoever it may be, needs to implement the AddressResultListener
interface. The best place to do this is inside an overridden onAttach()
method.
3. Override onAttach()
method in CurrentFragment
:
1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
2 ...
3 private AddressResultListener mAddressResultListener;
4
5 @Override
6 public void onAttach(Activity activity) {
7 super.onAttach(activity);
8 try {
9 mAddressResultListener = (AddressResultListener) activity;
10 } catch (ClassCastException e) {
11 throw new ClassCastException(activity.toString() + " must implement AddressResultListener");
12 }
13 }
14 ...
Finally, we can now hook up the tag button. All we need to set the action listener on the button.
4. Inside onActivityCreated()
method, add the following line right before where we check whether savedInstanceState
is null or not:
1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
2 ...
3 public void onActivityCreated(Bundle savedInstanceState) {
4 ...
5 mTagButton.setOnClickListener(this);
6 ...
7 }
8 ...
5. Make CurrentFragment
implement View.OnClickListener
interface, and then override onClick():
1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
2 public class CurrentFragment extends SherlockFragment implements LocationResultListener, AddressResultListener, View.OnClickListener {
3 ...
4 @Override
5 public void onClick(View view) {
6 if(mAddressResultListener != null && mLastKnownAddress != null)
7 mAddressResultListener.onAddressAvailable(mLastKnownAddress);
8 }
9 ...
10 }
That’s all we need to pass back the address to DefaultActivity
. Before we leave CurrentFragment
let’s finish off something that we haven’t yet talked about – stopping the Location Managers that we instantiated in the previous part. Imagine a scenario where we have fired up the LastLocationFetcher
timer and waiting for the last known location. During this waiting time the user decides to leave the app or switch to a different app. In this case, we need to make sure that we stop the timer and unregister from receiving any updates from either of the two radios.
6. In LocationService
class, add a stop()
method:
1 //file: src/main/java/com.ashokgelal.tagsnap/services/LocationService.java
2 ...
3 public void stop() {
4 if (mTimer != null)
5 mTimer.cancel();
6 mLocationManager.removeUpdates(mGpsLocationListener);
7 mLocationManager.removeUpdates(mNetworkLocationListener);
8 }
9 ...
If you want, you can also refactor out the two onLocationChanged()
methods inside the LocationService()
constructor to call the stop()
method. Eventually, we need to call this stop()
method from CurrentFragment
.
7. In CurrentFragment
class, override onStop()
method:
1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
2 ...
3 @Override
4 public void onStop() {
5 super.onStop();
6 if (mLocationService != null)
7 mLocationService.stop();
8 }
9 ...
That’s all for hooking up the tag button.
Adding details for current address:
We need an activity and a view for letting user add details and a picture. Let’s call this activity DetailsActivity
. When the DefaultActivity
receives an Address
in onAddressAvailable()
callback, we want to start the new DetailsActivity
passing the address itself. When DetailsActivity
is finished, we get back to the DefaultActivity
again where we will extract the description, picture url, category information out of intent data. The recommended way of passing the data between different activities is putting each field into intent itself using key-value pairs. But this is ugly, as we need to remember the exact magic key strings when retrieving the values. A better approach is to have a model class and pass an instance of that model class instead. But this comes with another problem – you cannot just put any object to an Intent
. All the primitive types are supported and few others. However, you can let your model class implement Parcelable to be able to put it to intent. We will go this route. First we need a model class; let’s call it TagInfo
.
8. Create a new class TagInfo
, and make it implement Parcelable
. You also need to override few methods from Parcelable
:
1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
2 public class TagInfo implements Parcelable{
3 @Override
4 public int describeContents() {
5 return 0;
6 }
7
8 @Override
9 public void writeToParcel(Parcel parcel, int i) {
10 }
11 }
We will now add required fields as well as the setters and getters.
9. Modify TagInfo
class to add the following fields, getters, setters:
1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
2 public class TagInfo implements Parcelable{
3 ...
4
5 private String mDescription;
6 private String mCategory;
7 private Uri mPictureUri;
8 private String mAddress1;
9 private String mAddress2;
10 private double mLatitude;
11 private double mLongitude;
12 private long mId;
13
14 public long getId() {
15 return mId;
16 }
17
18 public void setId(long id) {
19 mId = id;
20 }
21
22 public String getDescription() {
23 return mDescription;
24 }
25
26 public void setDescription(String description) {
27 this.mDescription = description;
28 }
29
30 public String getCategory() {
31 return mCategory;
32 }
33
34 public void setCategory(String category) {
35 this.mCategory = category;
36 }
37
38 public Uri getPictureUri() {
39 return mPictureUri;
40 }
41
42 public void setPictureUri(Uri pictureUri) {
43 this.mPictureUri = pictureUri;
44 }
45
46 public void setPictureUri(String uri) {
47 setPictureUri(Uri.parse(uri));
48 }
49
50 public String getAddress1() {
51 return mAddress1;
52 }
53
54 public void setAddress1(String address1) {
55 this.mAddress1 = address1;
56 }
57
58 public String getAddress2() {
59 return mAddress2;
60 }
61
62 public void setAddress2(String address2) {
63 this.mAddress2 = address2;
64 }
65
66 public double getLatitude() {
67 return mLatitude;
68 }
69
70 public void setLatitude(double latitude) {
71 this.mLatitude = latitude;
72 }
73
74 public double getLongitude() {
75 return mLongitude;
76 }
77
78 public void setLongitude(double longitude) {
79 this.mLongitude = longitude;
80 }
81 ...
10. Add 3 constructors; the one that takes a Parcel
data is required to be able to parcel this class back and forth.
1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
2 ...
3 public TagInfo(long id) {
4 mId = id;
5 }
6
7 public TagInfo() {
8 this(-1);
9 }
10
11 public TagInfo(Parcel data) {
12 setId(data.readLong());
13 setDescription(data.readString());
14 setCategory(data.readString());
15 setPictureUri(data.readString());
16 setAddress1(data.readString());
17 setAddress2(data.readString());
18 setLatitude(data.readDouble());
19 setLongitude(data.readDouble());
20 }
21 ...
Again, notice the last constructor. We are passed in the Parcel
data during deserialization of this class. The deserialization order has to match with that of the serialization, which we will do next.
11. Modify describeMethods()
method and writeToParcel()
methods to:
1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
2 ...
3 @Override
4 public int describeContents() {
5 // just returning the hash code should be enough
6 return hashCode();
7 }
8
9 @Override
10 // the orders of 'writing' should match that of 'reading'
11 public void writeToParcel(Parcel parcel, int flags) {
12 parcel.writeLong(getId());
13 parcel.writeString(getDescription());
14 parcel.writeString(getCategory());
15 if (getPictureUri() == null)
16 parcel.writeString("");
17 else
18 parcel.writeString(getPictureUri().getPath());
19 parcel.writeString(getAddress1());
20 parcel.writeString(getAddress2());
21 parcel.writeDouble(getLatitude());
22 parcel.writeDouble(getLongitude());
23 }
24 ...
We are almost done. The final piece that is missing is a static Creator
for creating a Parcelable
data of type TagInfo
and another type of an array of TagInfo
12. Add a static final Creator
field at the end of the class:
1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
2 ...
3 public static final Creator<TagInfo> CREATOR = new Creator<TagInfo>() {
4 @Override
5 public TagInfo createFromParcel(Parcel parcel) {
6 return new TagInfo(parcel);
7 }
8
9 @Override
10 public TagInfo[] newArray(int size) {
11 return new TagInfo[size];
12 }
13
14 };
15
16 ...
Now an object of this class can be sent back and forward from one activity to another as a Parcel.
Adding Details:
We want to pass an instance of TagInfo
to DetailsActivity
, which will be responsible for setting the description, category, and a picture uri for the TagInfo
object, and then send it back.
13. Add a new layout details.xml
, and replace the content with:
1 <!-- file: res/layout/details.xml -->
2 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="wrap_content"
4 android:layout_height="wrap_content"
5 android:id="@+id/scrollView">
6
7 <RelativeLayout
8 android:orientation="vertical"
9 android:layout_width="fill_parent"
10 android:layout_height="wrap_content">
11
12 <EditText
13 android:id="@+id/description"
14 android:layout_width="match_parent"
15 android:layout_height="wrap_content"
16 android:layout_alignParentLeft="true"
17 android:layout_alignParentTop="true"
18 android:layout_marginTop="5dip"
19 android:hint="@string/description"/>
20
21 <TextView
22 android:id="@+id/categoryHeader"
23 style="@style/HeaderText"
24 android:text="@string/category"
25 android:layout_alignLeft="@+id/description"
26 android:layout_below="@+id/description"/>
27
28 <View
29 style="@style/HeaderTextSeparator"
30 android:layout_below="@+id/categoryHeader"
31 android:id="@+id/categorySeparator"/>
32
33 <Spinner
34 android:layout_width="match_parent"
35 android:layout_height="wrap_content"
36 android:id="@+id/category"
37 android:layout_alignParentLeft="true"
38 android:layout_below="@+id/categoryHeader"
39 android:entries="@array/categories_array"
40 android:layout_marginTop="4dip"
41 android:layout_marginLeft="4dip"
42 android:spinnerMode="dialog"
43 android:layout_marginRight="4dip"/>
44
45 <RelativeLayout
46 android:layout_width="match_parent"
47 android:layout_height="wrap_content"
48 android:layout_alignLeft="@+id/description"
49 android:layout_below="@+id/category"
50 android:layout_marginTop="10dip"
51 android:id="@+id/relativeLayout">
52
53 <ImageView
54 android:layout_width="170dip"
55 android:layout_height="128dip"
56 android:id="@+id/previewImage"
57 android:src="@drawable/photo_preview"
58 android:layout_marginTop="10dip"
59 android:layout_marginLeft="10dip"/>
60
61 <ImageButton
62 android:layout_width="wrap_content"
63 android:layout_height="wrap_content"
64 android:id="@+id/cameraButton"
65 android:src="@drawable/add_from_camera"
66 android:layout_toLeftOf="@+id/galleryButton"
67 android:layout_alignParentTop="false"
68 android:layout_centerVertical="true"
69 android:visibility="gone"/>
70
71 <ImageButton
72 android:layout_width="wrap_content"
73 android:layout_height="wrap_content"
74 android:id="@+id/galleryButton"
75 android:src="@drawable/add_from_gallery"
76 android:layout_alignParentTop="false"
77 android:baselineAlignBottom="false"
78 android:layout_centerVertical="true"
79 android:layout_alignParentRight="true"
80 android:layout_marginRight="10dip"/>
81 </RelativeLayout>
82
83 <TextView
84 style="@style/HeaderText"
85 android:id="@+id/locationHeader"
86 android:text="@string/location"
87 android:layout_width="wrap_content"
88 android:layout_height="wrap_content"
89 android:layout_alignLeft="@+id/relativeLayout"
90 android:layout_below="@+id/relativeLayout"/>
91
92 <View
93 style="@style/HeaderTextSeparator"
94 android:layout_below="@+id/locationHeader"
95 android:id="@+id/locationSeparator"/>
96
97 <TextView
98 android:id="@+id/addressHeader"
99 style="@style/LocationHeaderText"
100 android:text="@string/address"
101 android:layout_alignLeft="@+id/locationHeader"
102 android:layout_below="@+id/locationHeader"/>
103
104 <TextView
105 android:id="@+id/detailsAddress1"
106 style="@style/LocationDetailsText"
107 android:layout_below="@+id/addressHeader"
108 android:layout_alignLeft="@+id/addressHeader"/>
109
110 <TextView
111 android:id="@+id/detailsAddress2"
112 style="@style/LocationDetailsText"
113 android:layout_alignLeft="@+id/categoryHeader"
114 android:layout_below="@+id/detailsAddress1"/>
115
116 <View
117 style="@style/LocationTextSeparator"
118 android:layout_below="@+id/detailsAddress2"
119 android:id="@+id/addressSeparator"/>
120
121 <TextView
122 style="@style/LocationHeaderText"
123 android:id="@+id/latitudeHeader"
124 android:text="@string/latitude"
125 android:layout_width="wrap_content"
126 android:layout_height="wrap_content"
127 android:layout_alignLeft="@+id/addressHeader"
128 android:layout_below="@+id/addressSeparator"/>
129
130 <TextView
131 style="@style/LocationDetailsText"
132 android:id="@+id/detailsLatitude"
133 android:layout_width="wrap_content"
134 android:layout_height="wrap_content"
135 android:layout_alignLeft="@+id/addressHeader"
136 android:layout_below="@+id/latitudeHeader"/>
137
138 <View
139 style="@style/LocationTextSeparator"
140 android:layout_below="@+id/detailsLatitude"
141 android:id="@+id/latitudeSeparator"/>
142
143 <TextView
144 style="@style/LocationHeaderText"
145 android:id="@+id/longitudeHeader"
146 android:text="@string/longitude"
147 android:layout_width="wrap_content"
148 android:layout_height="wrap_content"
149 android:layout_alignLeft="@+id/addressHeader"
150 android:layout_below="@+id/latitudeSeparator"/>
151
152 <TextView
153 style="@style/LocationDetailsText"
154 android:id="@+id/detailsLongitude"
155 android:layout_width="wrap_content"
156 android:layout_height="wrap_content"
157 android:layout_alignLeft="@+id/addressHeader"
158 android:layout_below="@+id/longitudeHeader"/>
159
160 <View
161 style="@style/LocationTextSeparator"
162 android:layout_below="@+id/detailsLongitude"
163 android:id="@+id/longitudeSeparator"/>
164
165 </RelativeLayout>
166 </ScrollView>
Here we just have a bunch of widgets for allowing our user to input details as well as text views for displaying address details.
14. Open strings.xml
and add following resources:
1 <!-- file: res/values/strings.xml -->
2 <string name="category">Category</string>
3 <string name="address">Address</string>
4 <string name="description">Description</string>
5 <string name="location">Location</string>
6 <string name="latitude">Latitude</string>
7 <string name="longitude">Longitude</string>
15. We also need few pre-made categories. For now, let’s have 4 categories. You can add more if you want. Add an string array resource to strings.xml
file:
1 <!-- file: res/values/strings.xml -->
2 <string-array name="categories_array">
3 <item>Musuem</item>
4 <item>Park</item>
5 <item>Theatre</item>
6 <item>Historial Landmark</item>
7 </string-array>
16. Create a new file styles.xml
under res/values/
and replace the content with:
1 <!-- file: res/values/styles.xml -->
2 <resources>
3 <style name="HeaderText">
4 <item name="android:textSize">14sp</item>
5 <item name="android:layout_width">match_parent</item>
6 <item name="android:layout_height">wrap_content</item>
7 <item name="android:layout_marginLeft">12dip</item>
8 <item name="android:layout_marginTop">15dip</item>
9 <item name="android:textAllCaps">true</item>
10 <item name="android:textColor">#999999</item>
11 <item name="android:clickable">false</item>
12 </style>
13
14 <style name="HeaderTextSeparator">
15 <item name="android:layout_width">match_parent</item>
16 <item name="android:layout_height">1dip</item>
17 <item name="android:background">#55CCCCCC</item>
18 <item name="android:layout_alignParentLeft">true</item>
19 <item name="android:layout_marginLeft">4dip</item>
20 <item name="android:layout_marginRight">4dip</item>
21 <item name="android:clickable">false</item>
22 </style>
23
24 <style name="LocationHeaderText">
25 <item name="android:layout_width">wrap_content</item>
26 <item name="android:layout_height">wrap_content</item>
27 <item name="android:layout_marginTop">5dip</item>
28 <item name="android:textSize">12sp</item>
29 <item name="android:clickable">false</item>
30 </style>
31
32 <style name="LocationDetailsText">
33 <item name="android:layout_width">wrap_content</item>
34 <item name="android:layout_height">wrap_content</item>
35 <item name="android:textSize">11sp</item>
36 <item name="android:textColor">#777777</item>
37 <item name="android:clickable">false</item>
38 </style>
39
40 <style name="LocationTextSeparator">
41 <item name="android:layout_width">match_parent</item>
42 <item name="android:layout_height">0.1dip</item>
43 <item name="android:background">#555555</item>
44 <item name="android:layout_alignParentLeft">true</item>
45 <item name="android:layout_margin">4dip</item>
46 <item name="android:clickable">false</item>
47 </style>
48 </resources>
Here, to keep our code DRY, we have added a bunch of styles. Now, we have all the required resources for details activity, which we are going to add next.
17. Create a class DetailsActivity
and make it extend SherlockActivity
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 public class DetailsActivity extends SherlockActivity {
3
4 }
18. Override onCreateMethod()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 @Override
4 public void onCreate(Bundle savedInstanceState) {
5 super.onCreate(savedInstanceState);
6 setContentView(R.layout.details);
7 setupDefaultValuesFromBundle();
8 }
9 ...
19. Add a setupDefaultValuesFromBundle()
method:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 private void setupDefaultValuesFromBundle() {
4 Bundle extras = getIntent().getExtras();
5 mDescriptionTextView = (TextView) findViewById(R.id.description);
6 mCategorySpinner = (Spinner) findViewById(R.id.category);
7 mPreviewImage = (ImageView) findViewById(R.id.previewImage);
8 mCurrentTaginfo = extras.getParcelable("taginfo");
9
10 mDescriptionTextView.setText(mCurrentTaginfo.getDescription());
11
12 String cat = mCurrentTaginfo.getCategory();
13 if (cat != null && !cat.equals("")) {
14 ArrayAdapter<String> adapter = (ArrayAdapter<String>) mCategorySpinner.getAdapter();
15 mCategorySpinner.setSelection(adapter.getPosition(cat));
16 }
17
18 Uri uri = mCurrentTaginfo.getPictureUri();
19 if (uri != null) {
20 File file = new File(uri.getPath());
21 if (file.exists()) {
22 mSelectedPictureUri = uri;
23 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
24 }
25 }
26
27 ((TextView) findViewById(R.id.detailsAddress1)).setText(mCurrentTaginfo.getAddress1());
28 ((TextView) findViewById(R.id.detailsAddress2)).setText(mCurrentTaginfo.getAddress2());
29 ((TextView) findViewById(R.id.detailsLatitude)).setText(String.valueOf(mCurrentTaginfo.getLatitude()));
30 ((TextView) findViewById(R.id.detailsLongitude)).setText(String.valueOf(mCurrentTaginfo.getLongitude()));
31 }
32 ...
Here, all we are doing is setting the default values for each UI fields.
Although it is very rare nowadays, some of the Android devices still might not have a ‘physical’ camera for taking pictures. Our app needs to be aware of that. And so, if a camera is not available, we will hide the button for firing up camera intent. Let’s setup these two buttons – one for brining up the gallery and another for brining up the camera, if present.
20. Add a new method setupImageButtons()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 private void setupImageButtons() {
4 if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
5 ImageButton cameraButton = (ImageButton) findViewById(R.id.cameraButton);
6 cameraButton.setVisibility(View.VISIBLE);
7 cameraButton.setOnClickListener(new View.OnClickListener() {
8 @Override
9 public void onClick(View view) {
10 takePictureWithCamera();
11 }
12 });
13 }
14 ImageButton galleryButton = (ImageButton) findViewById(R.id.galleryButton);
15 galleryButton.setOnClickListener(new View.OnClickListener() {
16 @Override
17 public void onClick(View view) {
18 pickPictureFromGallery();
19 }
20 });
21 }
22 ...
First we check if we have camera feature — getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
, if so we set the visibility of the cameraButton to VISIBLE
, and set the onClickListener. This button is hidden by default (check the res/layout/details.xml
file).
21. Call setupImageButtons()
from onCreate()
after calling setupDefaultValuesFromBundle()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 @Override
4 public void onCreate(Bundle savedInstanceState) {
5 ...
6 setupDefaultValuesFromBundle();
7 setupImageButtons();
8 }
9 ...
We are not going to write our own gallery app, or a camera app. Instead, we will request the built-in gallery to help us pick one picture for the user, which will be done using an intent of type Intent.ACTION_PICK
, and listen for the result. This is not only convenient on our side, but different apps might have registered for handling the same type of intent. In which case, users will be able to use app of their choice to select an image. This painless integration of apps is one of the many features that make Android platform different and better than others.
Taking a picture from a camera is no different – we just need to fire an intent of type MediaStore.ACTION_IMAGE_CAPTURE
and wait for the result. Let’s implement both of these features one by one.
22. Add a new method – takePictureWithCamera()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 private static final int CAMERA_REQUEST = 0;
4
5 private void takePictureWithCamera() {
6 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
7 mSelectedPictureUri = getOutputMediaFile();
8 intent.putExtra(MediaStore.EXTRA_OUTPUT, mSelectedPictureUri);
9 startActivityForResult(intent, CAMERA_REQUEST);
10 }
11 ...
Here we started an activity with an intent of capturing an image. Later, we will be firing up another activity with an intent of picking a picture. To differentiate between these two different requests when the result is available, we need to pass an int ID; that’s what the CAMERA_REQUEST
is.
We also need a path and a filename to save the picture captured from camera. For this purpose, we need a helper function that creates a uri to a file.
23. Add a new method getOutputMediaFile()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 private static Uri getOutputMediaFile() {
4 // get the Tagsnap directory
5 File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
6 Environment.DIRECTORY_PICTURES), "Tagsnap");
7
8 // Create the storage directory if it does not exist
9 if (!mediaStorageDir.exists()) {
10 if (!mediaStorageDir.mkdirs()) {
11 return null;
12 }
13 }
14
15 // Create a media file name
16 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
17 File mediaFile = new File(mediaStorageDir.getPath() + File.separator +
18 "IMG_" + timeStamp + ".jpg");
19
20 return Uri.fromFile(mediaFile);
21 }
22 ...
Before starting to listen for result from camera intent, we will first add a method for firing ‘pick’ from gallery intent.
24. Add a new method – pickPictureFromGallery()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 private static final int GALLERY_REQUEST = 1;
4 private void pickPictureFromGallery() {
5 Intent intent = new Intent(Intent.ACTION_PICK);
6 intent.setType("image/*");
7 startActivityForResult(intent, GALLERY_REQUEST);
8 }
9 ...
It’s same deal here. We fired up an intent of type ACTION_PICK
set the type of file to image type, and start the activity with a request code of 1 — GALLERY_REQUEST
. Now we are ready to listen for results from one the above intents.
- Override
onActionResult()
method:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 @Override
4 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
5 super.onActivityResult(requestCode, resultCode, data);
6 if (resultCode == RESULT_OK) {
7 if (requestCode == CAMERA_REQUEST) {
8 // set the picture
9 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
10 } else if (requestCode == GALLERY_REQUEST) {
11 // parse the picture path with some cursor management
12 Uri image = data.getData();
13 String[] filePathColumn = {MediaStore.Images.Media.DATA};
14 Cursor cursor = getContentResolver().query(image, filePathColumn, null, null, null);
15 cursor.moveToFirst();
16 int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
17 String filePath = cursor.getString(columnIndex);
18 cursor.close();
19
20 mSelectedPictureUri = Uri.fromFile(new File(filePath));
21 // set the picture
22 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
23 }
24 }
25 }
26 ...
If the result is coming back from the capture intent, then we just set the bitmap for our preview image widget. If the result is coming back from the pick intent, we have to do some extra work to find the correct path to the image that user selected, and set the image. It looks good but we have minor issue with this onActivityResult()
method.
Notice in the line:
mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
we are decoding the file from mSelectedPictureUri
that we had saved before firing up the capture intent. The intent brings up a totally new activity – camera’s capture activity. This means the DetailsActivity
gets killed, and when it is recreated, the mSelectedPictureUri
will be no more available. We need to make sure to save this uri, in onSaveInstanceState()
, and restore it in onCreate()
. But be careful! If we are returning from the gallery activity then mSelectedPictureUri
will be null so we have to take care of that too.
26. Override onSaveInstanceState()
method:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 @Override
4 protected void onSaveInstanceState(Bundle outState) {
5 super.onSaveInstanceState(outState);
6 outState.putParcelable("camera_output_path", mSelectedPictureUri);
7 }
8 ...
27. At the end of the onCreate()
method, append:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 public void onCreate(Bundle savedInstanceState) {
4 ...
5 if (savedInstanceState != null) {
6 mSelectedPictureUri = savedInstanceState.getParcelable("camera_output_path");
7 if(mSelectedPictureUri != null)
8 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
9 }
10 }
11 ...
Except than adding the DONE and DISCARD menu items, we are almost done here. But first we need to set permission for using the camera in AndroidManifest.xml
file.
28. In AndroidManifest.xml
file, add the following permission right after the <uses-sdk>
element:
1 <!-- file: AndroidManifest.xml -->
2 <uses-permission android:name="android.permission.CAMERA"/>
Allowing user to DONE/DISCARD changes with a custom ActionBar
I strong recommend you to read Roman Nurik’s post on Google+ before adding the DONE/DISCARD menu items to a custom ActionBar.
29. Start by adding a new actionbar_custom_view_done.xml
layout:
1 <!-- file: res/layout/actionbar_custom_view_done.xml -->
2 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 style="?android:actionButtonStyle"
4 android:id="@+id/actionbar_done"
5 android:layout_width="0dp"
6 android:layout_height="match_parent"
7 android:layout_weight="1">
8
9 <TextView style="?android:actionBarTabTextStyle"
10 android:layout_width="wrap_content"
11 android:layout_height="wrap_content"
12 android:layout_gravity="center"
13 android:paddingRight="20dp"
14 android:drawableLeft="@drawable/ done"
15 android:drawablePadding="8dp"
16 android:gravity="center_vertical"
17 android:text="@string/done" />
18 </FrameLayout>
This layout has a TextView that will be used as a DONE
menu button.
30. Add two new string resources to strings.xml
:
1 <!-- file: res/values/strings.xml -->
2 ...
3 <string name="done">Done</string>
4 <string name="discard">Discard</string>
5 ...
31. Add a new menu resource file, discard.xml
:
1 <!-- file: res/menu/discard.xml -->
2 <menu xmlns:android="http://schemas.android.com/apk/res/android">
3 <item
4 android:id="@+id/discard"
5 android:title="@string/discard"
6 android:icon="@drawable/discard"
7 android:showAsAction="never"/>
8 </menu>
Now, we need to setup this custom action bar, and the discard menuitem in DetailsActivity
.
32. Overrde onCreateOptionsMenu()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 @Override
4 public boolean onCreateOptionsMenu(Menu menu) {
5 super.onCreateOptionsMenu(menu);
6 getSupportMenuInflater().inflate(R.menu.discard, menu);
7 return true;
8 }
33. Override onOptionsItemSelected()
method:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 @Override
4 public boolean onOptionsItemSelected(MenuItem item) {
5 switch (item.getItemId()) {
6 case R.id.discard:
7 setResult(RESULT_CANCELED);
8 finish();
9 return true;
10 }
11 return super.onOptionsItemSelected(item);
12 }
13 ...
When user selects discard menu item, we need to finish this activity after setting result to RESULT_CANCELED
. This way any activity waiting for the result of this activity can act accordingly.
The last task remaining is to setup our custom action bar, and listen to done button’s click event.
34. Add a new method setupActionBar()
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 private void setupActionBar() {
4 LayoutInflater inflater = (LayoutInflater) getSupportActionBar().getThemedContext().getSystemService(LAYOUT_INFLATER_SERVICE);
5 final View customActionBarView = inflater.inflate(R.layout.actionbar_custom_view_done, null);
6
7 customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener(new View.OnClickListener() {
8 @Override
9 public void onClick(View view) {
10 Intent data = new Intent();
11 mCurrentTaginfo.setDescription(mDescriptionTextView.getText().toString());
12 mCurrentTaginfo.setCategory(mCategorySpinner.getSelectedItem().toString());
13 mCurrentTaginfo.setPictureUri(mSelectedPictureUri);
14 data.putExtra("taginfo", mCurrentTaginfo);
15 setResult(RESULT_OK, data);
16 finish();
17 }
18 });
19
20 // this is what makes the custom actionbar
21 final ActionBar actionBar = getSupportActionBar();
22 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE);
23 actionBar.setCustomView(customActionBarView);
24 }
25 ...
When the user selects DONE
button, we create a new intent, set the description, category, and picture uri for the current taginfo object. We then put this updated taginfo back in the intent, and finish the activity with result set to OK.
35. Call the setupActionBar()
from the onCreate()
method right after setContentView(R.layout.details)
:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3 public void onCreate(Bundle savedInstanceState) {
4 ...
5 setupActionBar();
6 ...
7 }
8 ...
We are done with the DetailsActivity
but we haven’t yet called it from the DefaultActivity
.
36. Modify onAddressAvailable()
method to:
1 //file: src/main/java/com.ashokgelal.tagsnap/main/DefaultActivity.java
2 ...
3 @Override
4 public void onAddressAvailable(Address address) {
5 Intent intent = new Intent(this, DetailsActivity.class);
6 TagInfo tagsnap = new TagInfo();
7 if (address.getMaxAddressLineIndex() > 0)
8 tagsnap.setAddress1(address.getAddressLine(0));
9
10 tagsnap.setAddress2(String.format("%s, %s, %s", address.getLocality(), address.getAdminArea(), address.getCountryName()));
11 tagsnap.setLatitude(address.getLatitude());
12 tagsnap.setLongitude(address.getLongitude());
13 intent.putExtra("taginfo", tagsnap);
14 startActivityForResult(intent, ADD_DETAILS_REQUEST);
15 }
16 ...
Here we create an explicit intent for firing up the DetailsActivity
, which depends on an instance of TagInfo
class. So, we create an instance of TagInfo
class, put it in the intent, start the DetailsActivity
for result. We won’t be handling the result coming back from the DetailsActivity
in this part.
If you try to run the app and select the button for adding details, you will get a ActivityNotFoundException
. If you read the error message carefully, it tells you what’s wrong – we have only defined our DetailsActivity
class, but haven’t yet declared about this activity to Android system. Every activities in Android that intent to run at some point, should be declared in AndroidMainifest.xml
file.
37. In AndroidManifest.xml
, just before the closing </application>
tag, add:
1 <!-- file: AndroidManifest.xml -->
2 ...
3 <activity
4 android:name=".DetailsActivity"
5 android:label="@string/details"/>
6 ...
38. Add a new string resource in strings.xml file:
1 <!-- file: res/values/strings.xml -->
2 ...
3 <string name="details">Details</string>
4 ...
Run the app and try it. Our app is now able to offer a new activity for adding description, a category, and a picture. We won’t do anything with this information for now. We will save these details to a local SQLite Database in the next part of this series.
You can follow me on Twitter, or add me on Google+.