Tuesday 18 November 2014

Build an ASCII Art Editor: Save and Delete ASCII Pictures

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.
This is the final part in a tutorial series on creating an ASCII art editor app for Android. In the first three parts, we created the user interface, implemented saving user ASCII pictures as image files, and set up a database to store saved pictures in. This allowed users to choose from the list of saved pictures to load back in for viewing, exporting, and editing. In this tutorial, we will facilitate saving pictures to the database, deleting pictures previously saved, and starting new pictures.

This tutorial series on Creating a Simple ASCII Art Editor is in four parts:

Building the User Interface
Image Export & User Configuration
Database Creation & Querying
Saving and Deleting ASCII Pictures
Step 1: Detect Button Clicks

We will be working entirely in the application's main Activity class this time. Add the following imports at the top of the class:

import <span class="skimlinks-unlinked">java.text.SimpleDateFormat</span>;
import <span class="skimlinks-unlinked">java.util.Calendar</span>;

import <span class="skimlinks-unlinked">android.app.AlertDialog</span>;
import android.content.ContentValues;
import android.content.DialogInterface;
We will handle clicks on the save, delete, and new buttons. In your main onCreate method, set the class up to handle clicks:

Button saveASCIIBtn = (Button)findViewById(R.id.save_btn);
saveASCIIBtn.setOnClickListener(this);
Button newBtn = (Button)findViewById(R.id.new_btn);
newBtn.setOnClickListener(this);
Button deleteBtn = (Button)findViewById(R.id.delete_btn);
deleteBtn.setOnClickListener(this);
We added these buttons to the layout files earlier in the series. In the onClick method, after the existing code, add to your chain of conditional blocks for these additional three buttons:

//user has clicked new button
else if(v.getId()==R.id.new_btn) {
        
}
//user has clicked save button
else if(v.getId()==R.id.save_btn) {  

}
//user has clicked delete button
else if(v.getId()==R.id.delete_btn) {
            
}
We will add code to each of these blocks to implement the functionality.

Step 2: Create New Pictures

Let's start with the easiest function, users pressing the new button. In the conditional block in onClick for the new button, reset the text-field to an empty string ready for user input:

1
textArea.setText("");
Remember that we used a variable to keep track of the ID of the currently displayed picture if it has been loaded from the database - reset it too:

1
currentPic=-1;
Step 3: Save the Current Picture
Let's turn to the conditional block in onClick for the save button. When the user presses the save button, there are two possibilities. Either they are saving a new picture not yet stored in the database or they are saving a picture loaded from the database, then edited. If the user is saving a picture loaded from the database, rather than saving a new entry in the database, we will update the existing record.

First get the content of the Edit Text:

String enteredTxt = textArea.getText().toString();
To model the data we want to commit to the database, either as an insert for a new picture or an update for an existing one, we create a Content Values object:

ContentValues picValues = new ContentValues();
The new data will include the text from the text-field, so add it to the Content Values object, using the table column name we defined last time, stored as a public variable in the database helper class:

<span class="skimlinks-unlinked">picValues.put(ImageDataHelper.ASCII_COL</span>, enteredTxt);
We are going to use a string including the current date and time for the picture name, so build that next:

Date theDate = Calendar.getInstance().getTime();
SimpleDateFormat dateFormat = new SimpleDateFormat("<span class="skimlinks-unlinked">yyyy-MM-dd_hh.mm.ss</span>");
String fileName = dateFormat.format(theDate);
This is what the user will see in the list of saved pictures. Add it to the Content Values using the same technique:

1
<span class="skimlinks-unlinked">picValues.put(ImageDataHelper.CREATED_COL</span>, fileName);
Get a reference to the database:

1
SQLiteDatabase savedPicsDB = imgData.getWritableDatabase();
Now we need to tailor what happens to whether the current picture is new or not. Add a conditional statement:

if(currentPic<0){
                
}
else{
                
}
The variable will be less than zero if the current picture is not already in the database (as we set it to -1). If the currently displayed picture has been loaded from the database, this variable will have the picture ID from the database stored in it, in which case the else will execute. Inside the if block, we will save the picture as a new database record:

1
long insertNum = savedPicsDB.insert("pics", null, picValues);
This is an insert statement because it is a new record. We pass the table name and the Content Values we created. The middle parameter is for a column name, but we do not need it. We retrieve the result of the insert as a long value, which is the ID of the newly inserted record. Update the variable so that any new edits saved will be written to the same database record:

currentPic=(int)insertNum;
Now output a confirmation message to the user, providing the insertion was a success:

if(insertNum>=0)
    Toast.makeText(getApplicationContext(), "Image saved to database!",
        Toast.LENGTH_SHORT).show();
Now let's turn to the else, for updating a picture already stored in the database:

int savedNum = savedPicsDB.update("pics",
    picValues,
    ImageDataHelper.ID_COL+"=?",
    new String[]{""+currentPic});
This time we use an update statement, passing the table name, Content Values and where details. The where part of the statement indicates the ID column and the value to match in it, specifying the current picture ID, so that the correct record is updated. The method expects a string array for the last parameter, even where there is only one value as in this case. Confirm the update to the user:

if(savedNum>0)
    Toast.makeText(getApplicationContext(), "Image saved to database!",
        Toast.LENGTH_SHORT).show();
We are updating the picture name as well as content, but you can opt to leave the name as a reflection of when the picture was originally created if you prefer. After the else, close the connections:

<span class="skimlinks-unlinked">savedPicsDB.close</span>();
<span class="skimlinks-unlinked">imgData.close</span>();

Step 4: Delete the Current Picture

Now let's implement deleting the current picture. If the current picture has been loaded from the database, we will delete its record. Otherwise we will just empty the text-field. In the conditional section of the onClick method for the delete button, add a test for this as follows:

t.makeText(getApplicationContext(), "Picture deleted",
        Toast.LENGTH_SHORT).show();
Still inside the Dialog Interface click listener onClick method, reset the picture ID variable, empty the text-field and close the database connections:

currentPic=-1;
textArea.setText("");
<span class="skimlinks-unlinked">savedPicsDB.close</span>();
<span class="skimlinks-unlinked">imgData.close</span>();
Now after the block in which you set the positive button, set the negative button:

confirmBuilder.setNegativeButton("No", new DialogInterface.OnClickListener() {
    public void onClick(DialogInterface dialog, int id) {
        dialog.cancel();
    }
});
In this case we simply cancel the dialog. Now we can go ahead and show it:

AlertDialog alert = confirmBuilder.create();
<span class="skimlinks-unlinked">alert.show</span>();

Now to complete the deletion section of the Activity onClick method, turn to your else statement for when the current picture has not been loaded from the database. In this case we will simply empty the Edit Text:

1
textArea.setText("");
Step 5: Tidy Up!

We have closed our database connections in each block we used to query, insert, delete or update records. However, in case the user exits the app while a connection is open, we should make sure all connections are closed. Add the onDestroy method to your Activity:

1
@Override
public void onDestroy() {

}
Inside it, close the database helper, then call the superclass method:

1
2
<span class="skimlinks-unlinked">imgData.close</span>();
super.onDestroy();
Now you can test your app! Check that it correctly saves new pictures, updates existing pictures and deletes pictures on user request by saving a few then experimenting with them.

Conclusion

The simple ASCII art editor app is now complete. When you run the app, you should be able to enter text characters, save pictures, export them as image files, load, edit and delete previously saved pictures as well as configuring the display colors. The source code download contains all of the Java and XML files we have worked on during the series.

There are lots of ways you could enhance this app if you want to explore it further. For example, you could improve the styling of the user interface. You could check whether the user wants to overwrite a stored picture before updating an existing database record, giving them the option of saving a new picture instead. A particularly productive enhancement would be to extend the code to use content providers and/or fragments to load data and target tablet devices effectively. You could also improve the picture saving process, for example by allowing the user to choose a name for each picture they save.

In this series we have introduced a few of the basic processes involved in data storage on Android. We have focused on local storage, i.e. data stored on the user device. Another technique commonly used in Android apps is retrieving data over the Internet, which is also a task worth exploring. The Android platform facilitates a wide range of data storage and management options. You should now have basic familiarity with some of those most commonly used, giving you a solid foundation for approaching data in your future projects.

No comments:

Post a Comment