The Android platform offers a wide range of storage options for use within your apps. In this tutorial series, we are going to explore some of the data storage facilities provided by the Android SDK by building a simple project: an ASCII art editor.
In the first part of this series we created our user interface elements, including a main Activity with a text-field for users to enter their characters and a configuration Activity in which the user will be able to choose color settings. In this installment, we are going to implement this user configuration choice as well as allowing users to export their artworks as PNG images, with the resulting files saved to the external Pictures directory on the SD card. In the final two parts of the series we will be working with an SQLite database to save, load, and delete the user's artwork.
The outline for this series is as follows:
- Building the User Interface
- Image Export & User Configuration
- Database Creation & Querying
- Saving and Deleting ASCII Pictures
Step 1: Respond to Settings Button Click
Let's start with the user configuration settings. In your app's main Activity, before the onCreate method, add a variable to represent the editable text-field:
private EditText textArea;
You will need to add an import:
import android.widget.EditText;
Inside onCreate, after you set the content view, retrieve a reference to the text-field so that we will be able to apply the display settings:
textArea = (EditText)findViewById(R.id.ascii_text);
This is the ID we gave the editable text-field in the XML layout file. Next, retrieve a reference to the Settings button and detect clicks:
Button setBtn = (Button)findViewById(R.id.set_colors_btn); setBtn.setOnClickListener(this);
You will need the following import added:
import android.widget.Button;
Extend your Activity class declaration opening line to handle clicks as follows:
public class MainActivity extends Activity implements OnClickListener {
Alter the class name if you chose a different one. This requires another import:
import android.view.View.OnClickListener;
Now your class must provide the onClick method, so add it after the onCreate method:
public void onClick(View v) { }
This method is going to handle various button clicks, so we will be adding code to it throughout the series. You will need the following import:
import android.view.View;
Inside the onClick method, we need to tailor what happens to whatever button has been clicked, as we will be handling more than one:
if(v.getId()==R.id.set_colors_btn) { }
This is the Settings button we included in our main layout file last time.
Step 2: Start the Settings Activity
We created the Activity to handle user configuration choices last time, so let's start it running now from our main Activity, inside the onClick "if" statement block:
Intent colorIntent = new Intent(this, ColorChooser.class); this.startActivityForResult(colorIntent, COLOR_REQUEST);
You will need this import:
import android.content.Intent;
We start an Intent for the Activity we created to handle user color scheme choice. In that Activity, we are going to let the user choose an option and return the result to the main Activity, which is why we use startActivityForResult. We will retrieve the result in the onActivityResult method which we will add next. But first, we need to be able to identify which Activity we are returning from, so we pass a constant as the second parameter to the startActivityForResult method. Add the constant at the top of your class declaration, before the onCreate method:
private final int COLOR_REQUEST=1;
Now add the onActivityResult method after the onClick method:
protected void onActivityResult(int requestCode, int resultCode, Intent data) { }
Inside this class, we will handle data returned from the Settings chooser Activity (and later from the Activity in which the user chooses a saved picture to load). Inside the method, check that we are returning from the Color Chooser Activity and that we have a valid result:
if (requestCode == COLOR_REQUEST) { if(resultCode == RESULT_OK){ } }
When the user clicks the Settings button, the chooser Activity will start. The user will choose a color scheme and the chooser Activity will finish, returning data representing the user choice to the main Activity, since this is where the chooser Activity was started from. On returning to the main Activity, the onActivityResult method will execute, so we can implement the user choice here (which we will do soon).
Step 3: Detect User Settings Choices
Now let's turn our attention to the chooser Activity in which the user can make a color scheme selection. Open your "ColorChooser" Activity. The onCreate method should already be complete. Remember that when we added the Image Buttons representing each color scheme to the "color_choice" XML layout file, we specified "setColors" as their onClick attribute and included a tag representing the two HEX colors for text and background - take a look at the "color_choice.xml" markup now to check this. When users click the Image Buttons, Android will execute the specified method in the Activity hosting the layout. Add the method to your "ColorChooser" Activity after onCreate:
public void setColors(View view){ }
Add the following imports to the class:
import android.view.View; import android.content.Intent;
Inside the "setColors" method, first retrieve the tag from the View that has been clicked:
String tagInfo = (String)view.getTag();
The tag has the following format: "#000000 #ffffff" - split the two colors into a String array:
String[] tagColors = tagInfo.split(" ");
Now we want to pass this data back to the main Activity so that we can apply the color scheme settings to the user interface elements there. To do this we use an Intent:
Intent backIntent = new Intent();
Add the two colors as extra data:
backIntent.putExtra("textColor", tagColors[0]); backIntent.putExtra("backColor", tagColors[1]);
The first color represents the text and the second is for the background. Set the return Intent result:
setResult(RESULT_OK, backIntent);
Return to the Activity that called this one by finishing:
finish();
The two HEX color Strings will be passed to the onActivityResult method in the main Activity class.
Step 4: Apply the Color Scheme
Back in your main Activity onActivityResult method, inside the two "if" statement blocks we added to detect returns from the chooser Activity, retrieve the Strings we passed back:
String chosenTextColor = data.getStringExtra("textColor"); String chosenBackColor = data.getStringExtra("backColor");
Let's use a helper method to set the colors so that we can carry out the same process elsewhere:
updateColors(chosenTextColor, chosenBackColor);
Add the helper method after the onActivityResult method:
private void updateColors(String tColor, String bColor){ }
Inside this method, we can now set the colors for the editable text-field text and background:
textArea.setTextColor(Color.parseColor(tColor)); textArea.setBackgroundColor(Color.parseColor(bColor));
This requires another import:
import android.graphics.Color;
Step 5: Update the Shared Preferences
We've updated the appearance of the text-field to suit the user's choice, but we want to remember the choice so that each time they run the app it will be observed. Let's add an instance variable at the top of the class so that we can refer to the Shared Preferences anywhere:
private SharedPreferences asciiPrefs;
Add the import if necessary:
import android.content.SharedPreferences;
In onCreate, after the existing code, get the Shared Preferences, using a name of your choice (you must use the same name each time you retrieve the preferences in your app):
asciiPrefs = getSharedPreferences("AsciiPicPreferences", 0);
Back in the onActivityResult method, after you call the helper method, get the Shared Preferences Editor:
SharedPreferences.Editor prefsEd = asciiPrefs.edit();
Pass the data representing the user's choice and commit it to the app's preferences:
prefsEd.putString("colors", ""+chosenTextColor+" "+chosenBackColor); prefsEd.commit();
We specify a name for the preference data item and pass the two Strings concatenated into one, with a space character between them. This is a typical use of Shared Preferences, which are intended for key-value pairs of primitive type values, so are commonly used for configuration settings.
Step 6: Check the Shared Preferences
We want the user to see their chosen color scheme whenever they run the app in the future, so we need to check their choice when the Activity starts. In the onCreate method, after retrieving the Shared Preferences:
String chosenColors = asciiPrefs.getString("colors", "");
We pass the key String we used when saving the user choice. The user may not yet have saved a preference, so apply the color settings inside a conditional statement:
if(chosenColors.length()>0){ String[] prefColors = chosenColors.split(" "); updateColors(prefColors[0], prefColors[1]); }
We split the color String again and call the helper method to apply the settings.
Step 7: Handle Export Button Clicks
Remember that we included an Export button for users to save their artworks as image files. In your main Activity's onCreate method, detect clicks on it:
Button saveImgBtn = (Button)findViewById(R.id.export_btn); saveImgBtn.setOnClickListener(this);
Now add an "else if" inside the onClick method:
else if(v.getId()==R.id.export_btn) { saveImg(); }
This is going to be another helper method so add it to your class file after the "updateColors" method:
private void saveImg(){ }
Step 8: External Storage Availability
The Android system runs on many different devices, and we can't take anything for-granted about what facilities the user has. Before you attempt to write anything to external storage, you must first check that it is available. Inside the new helper method, add a check on the user's storage state using the Environment:
String state = Environment.getExternalStorageState();
You need an import for this:
import android.os.Environment;
Now add a conditional test on the result, so that we can tailor what happens next:
if (Environment.MEDIA_MOUNTED.equals(state)) { } else { }
The "if" block will export the image to external storage, with the "else" block handling situations in which there is no storage available. In such a case all we want to do is write an error message to the user letting them know we can't export the image - inside the "else" block:
Toast.makeText(this.getApplicationContext(), "Sorry - you don't have an external" + " storage directory available!", Toast.LENGTH_SHORT).show();
Add the required import:
import android.widget.Toast;
Step 9: Export the Image File
Now let's handle cases in which external storage is available. Add the following imports for the file output operation:
import java.io.File; import java.io.FileOutputStream; import java.util.Date; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat;
Back in the helper method for saving pictures, inside the "if" block, retrieve a reference to the Pictures directory:
File picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
Now we have the first part of the path we will use to create the picture file. The image is going to contain the visible content of the editable text-field, including the background color - i.e. it will display what you can see looking at the app screen when you export it. To achieve this we use the cache for the View:
textArea.setDrawingCacheEnabled(true); textArea.buildDrawingCache(true); Bitmap bitmap = textArea.getDrawingCache();
This sets up the editable text-field View to be drawable, then saves its current appearance as a Bitmap using the drawing cache.
Now we need to give our file a name, so let's use the current date and time to make each one unique, appending some informative text and the image file extension:
Date theDate = new Date(); String fileName = "asciipic"+theDate.getTime()+".png";
Here we specify the filename, now let's create the file using it together with the path to the Pictures directory:
File picFile = new File(picDir+"/"+fileName);
Since we are going to do some file I/O we need try and catch blocks:
try { } catch (Exception e) { e.printStackTrace(); }
This allows the program to keep running if something goes wrong with the input/ output operations. Inside the try block, create the file and pass it to an output stream:
picFile.createNewFile(); FileOutputStream picOut = new FileOutputStream(picFile);
Compress the Bitmap and write it to the output stream, saving the result as a boolean:
boolean worked = bitmap.compress(CompressFormat.PNG, 100, picOut);
Output a message to the user depending on the result of the write operation:
if(worked){ Toast.makeText(getApplicationContext(), "Image saved to your device Pictures " + "directory!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getApplicationContext(), "Whoops! File not saved.", Toast.LENGTH_SHORT).show(); }
Now close the file:
picOut.close();
We can free the resources being used for the drawing cache, after the catch block:
textArea.destroyDrawingCache();
That's the file output operation complete. The user can view their exported artwork as an image by browsing their device Pictures folder at any time.
Tip:If you're running your app on the emulator in Eclipse, you can set it up to contain external storage by editing the AVD and entering the amount of storage space in the SD Card "Size" field. Once you have the AVD running, run your app on it and export an image. In Eclipse, open the DDMS perspective from the Window, Open Perspective menu. Select the device and browse to the Pictures directory (mnt/sdcard/Pictures). You should see any pictures written by the app in this folder. To pull one from the virtual device, select it and click the "Pull a file from the device" button to save and view it on your computer.
Conclusion
That's the user configuration and image export part of the app complete. If you run the app now you should be able to set the color scheme and export images to the SD card (as long as there is one present). Check the source code download if you're unsure about anything. We've used external storage and Shared Preferences in this tutorial. In the next parts we will build an SQLite database, using inserts, queries and updates on it to handle the user saving, loading, deleting and editing their ASCII artworks.
Comments