Creating the Task Detail Page - Creating a Feature‐Rich Application - Android Application Development For Dummies, 3rd Edition (2015)

Android Application Development For Dummies, 3rd Edition (2015)

Part III. Creating a Feature‐Rich Application

Chapter 10. Creating the Task Detail Page

In This Chapter

arrow Create the TaskEditActivity and TaskEditFragment

arrow Using the FragmentManager to start fragments

arrow Creating click listeners and starting activities

arrow Saving fragment state and restoring it later

arrow Transparent status, action, and navigation bars

arrow Using the Palette library

This chapter continues the Tasks app that you started in Chapter 9. You’ve already built a rudimentary list view for the app; now it’s time to allow users to create and edit tasks.

This will require the following steps:

· Create a new activity and fragment to allow editing and updating tasks.

· Allow users to click on items in the list to open them in the editor.

Creating the TaskEditActivity

First things first, you will need a new activity to hold all this editing goodness. Right‐click the com/dummies/tasks/activity folder and select New Blank Activity. Input the settings in Table 10-1:

Table 101 Activity Settings for TaskEditActivity

Setting

Value

Activity name

TaskEditActivity

Layout name

activity_task_edit

Title

Tasks

Menu resource name

menu_task_edit

Launcher activity

No

Hierarchical parent

com.dummies.tasks.activity.­TaskListActivity

Package name

com.dummies.tasks.activity

After you’ve created the activity, delete the onCreateOptionsMenu and onOptionsItemSelected methods, as well as the res/menu/menu_task_edit.xml files. You won’t be using them.

Add the lines in bold below, and your activity should now look like this:

public class TaskEditActivity extends Activity {

public static final String EXTRA_TASKID = "taskId"; → 3

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_edit);
setActionBar((Toolbar) findViewById(R.id.toolbar)); → 9
}

}

Some quick notes on the previous code:

3 You use this constant later in the chapter, so add it in now to save yourself a little trouble later.

9 Set the action bar for the activity to the toolbar in your layout. You will add a toolbar to your layout next. You may recognize this code from Chapter 9; you did the same thing there.

Now edit res/layout/activity_task_edit.xml and set it to the following:

<?xml version="1.0" encoding="uft-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" → 3
android:layout_width="match_parent"
android:layout_height="match_parent">

<FrameLayout →7
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"/>

<Toolbar →12
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/status_bar_height"

android:layout_gravity="top"
android:id="@+id/toolbar"/>
</FrameLayout>

Some comments about the previous code:

3 A view group that holds the fragment and your toolbar. Order is important: The first item in the FrameLayout is drawn first, and the last item drawn last. This means that the views at the top of the FrameLayout will be under the later views.

7 This FrameLayout is an “anchor” into which the fragment is placed. It takes up the entire screen. Unlike activity_task_list, you do not use the <fragment> tag here because you do not want Android to auto‐instantiate the fragment for you. Instead, you manually instantiate it so that you can pass in parameters to the non‐default constructor (specifically, the ID of the task to be edited).

12 This is the Toolbar view, which is placed at the top of the screen and in front of the fragment. Most of its styling settings are defined in the AppTheme.TransparentActionBar style in the styles.xml (which you set up later in this chapter). But you do need to add a little bit of buffer at the top of the screen to account for the status bar, because the status bar is translucent and content of the page has shifted up by a few pixels to slide under the status bar. To do this, create a new dimension named status_bar_heightin your dimens.xml file and set it to 25dp.

Linking the List View to the Edit View

So you have a fancy new activity. Good for you. Now the question is, how do you get to it?

Because this activity will be used to edit tasks, you want to reach it from the list view you created in Chapter 9. To do that, you need to create an OnClickListener to listen to clicks on the list view (the RecyclerView), which then starts up the TaskEditActivity.

To do so, first open up TaskListAdapter.java and add code in bold to the end of onBindViewHolder:

@Override
public void onBindViewHolder(ViewHolder viewHolder, final int i) { → 2
final Context context = viewHolder.titleView.getContext();

viewHolder.titleView.setText(fakeData[i]);

// set the thumbnail image
Picasso.with(context)
.load(getImageUrlForTask(i))
.into(viewHolder.imageView);

// Set the click action
viewHolder.cardView.setOnClickListener( →13
new View.OnClickListener() {
@Override
public void onClick(View view) {
((OnEditTask) context).editTask(i); →17
}
});
}

Here’s what this code does:

2 The i parameter is used inside an inner class on line 17, so Java requires that you declare it final. Same thing for the context parameter on the next line.

13 Here you set the OnClickListener on the cardView by calling cardView.setOnClickListener.

17 The OnClickListener is going to ask the context (also the activity) to edit the task by calling editTask on the activity. The editTask method does not exist yet, but you will create it and the OnEditTask interface in the next section. Technically, you don’t need an interface to use an OnClickListener, but using one here will be handy in Chapter 16 when you begin adapting your app for tablets.

Now that you have the OnClickListener set up, you need to create the OnEditTask interface so that it can be implemented by the TaskListActivity.

Create a new package named com.dummies.tasks.interfaces and add a new interface to it called OnEditTask. Edit the file to contain the following code:

public interface OnEditTask {
/**
* Called when the user asks to edit or insert a task.
*/
public void editTask(long id);
}

Now if you go back to TaskListAdapter.java, it should compile fine. However, it won’t run yet because you cast context to the OnEditTask interface, but your context does not yet implement OnEditTask. So modify TaskListActivity.java with the following changes:

public class TaskListActivity extends Activity implements OnEditTask {

. . .

/**
* Called when the user asks to edit or insert a task.
*/
@Override
public void editTask(long id) {
// When we are asked to edit a reminder, start the
// TaskEditActivity with the id of the task to edit.
startActivity(new Intent(this, TaskEditActivity.class)
.putExtra(TaskEditActivity.EXTRA_TASKID, id));
}
}

Now run your app and try clicking on one of the items in your list view. You should then see a blank activity on your screen. You can press the Back button to go back to the list and try again with another item in the list.

Creating the TaskEditFragment

The edit fragment is going to do all the heavy lifting for editing tasks. It is the part of the app that knows how to display a detailed view of the current task and allow the user to edit it.

To create the edit fragment, first create the layout.

Creating the layout

Create a new file named res/layout/fragment_task_edit.xml and put the following in it:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" → 2
android:layout_width="match_parent"
android:layout_height="match_parent" >

<RelativeLayout →6
android:layout_width="match_parent"
android:layout_height="wrap_content" >

<ImageView →10
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:actionBarSize"
android:adjustViewBounds="true"
android:layout_alignParentTop="true"
/>

<EditText →19
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/gutter" →23
android:layout_marginEnd="@dimen/gutter"
android:layout_below="@id/image" →25
android:hint="@string/title"/> →26

<TextView →28
style="@android:style/TextAppearance.Medium" →29
android:id="@+id/task_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title" →33
android:layout_alignEnd="@id/title" →34
android:layout_marginEnd="3dp"/> →35

<TextView →37
style="@android:style/TextAppearance.Medium"
android:id="@+id/task_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/task_time" →42
android:layout_alignBottom="@id/task_time" →43
android:layout_marginEnd="10dp"/> →44

<EditText →46
android:id="@+id/notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@id/title"
android:layout_marginEnd="@dimen/gutter"
android:layout_below="@id/task_time"
android:gravity="top" →53
android:hint="@string/notes"
android:minLines="5"/> →55
</RelativeLayout>
</ScrollView>

Some additional information about the listing above:

2 You place the entire edit fragment inside a scroll view. If you don’t do this, the user can’t see the bottom of the screen on small devices or when she is in landscape mode. Most screens should be wrapped inside a scroll view, except views like RecyclerView that have their own built‐in scrolling.

6 A RelativeLayout lets you position things directly relative­ to each other. It gives more control than something like a LinearLayout, which only allows you to position things in a line, one after another. To use a RelativeLayout, you utilize positioning commands such as layout_below, layout_alignStart,­ and layout_parentStart. You’ll see examples of these commands in the child views of this layout. For more information about RelativeLayout, visithttp://d.android.com/guide/topics/ui/layout/relative.html.

10 This is the same image used on the list view, but here you show the full image instead of cropping it. When you downloaded the image, you made sure it was a size that fully filled the screen horizontally. You enable adjustViewBounds so that the ImageViewshrinks or expands itself to properly fit the image after it’s loaded. You set the minHeight to the height of the action bar, so that if the image isn’t available yet, the action bar won’t cover up the title field.

19 Allows the user to edit the task’s title. I provide hint text (“Title”) which indicates what field the user is editing.

23 Every well‐designed page should have some margin on its left and right sides. This is called the gutter. This line tells Android to place a gutter of 10dp (defined in dimens.xml) on the left of the EditText. The next line adds the same gutter on the right.

You could also use marginLeft and marginRight rather than marginStart and marginEnd. However, it’s better practice to use marginStart and marginEnd, because layouts that use start and end automagically work in locales where text is laid out right‐to‐left rather than left‐to‐right. Examples of right‐to‐left languages are Hebrew and Arabic. You might think you’ll never translate your app to one of these languages, but it’s better to follow the best practice just in case you ever find your app wildly popular in Israel or Saudi Arabia! See Chapter 17 for more information about using right‐to‐left languages.

25 Positions the title directly below the image in the layout. All the layout parameters relate to the Layout view that this TextView is a child of, in this case a RelativeLayout.

26 The hint text for the EditText displays a hint to users concerning what kind of information they can input into this field. For example, a hint might say “Phone number” or “(xxx) xxx‐xxxx” for a field expecting a phone number in the U.S. The hint text appears in the EditText when the field is empty.

28 The time button.

29 The default text size is relatively small. Because the date and time TextViews are going to be clickable, you want them a little larger than normal. To do this, you change the style of the ­textview to use android:style/TextAppearance.Medium, which is a standard Android style for TextViews. The default is android:style/TextAppearance.DeviceDefault, but there is also Large, Small, and many others. See http://d.android.com/reference/android/R.style.html#TextAppearance for an exhaustive list.

33‐34 Positions the time button directly below the title, and aligns it to the right‐hand side of the title as well. This puts the time on the far right side of the screen.

35 Adds a little bit of extra padding to the right of this view to make the text line up with the horizontal line above it.

37 The date button. You’ll place this to the left of the time button.

42‐43 Positions the date button to the left of the time button, and aligns the bottom of the date and time so they line up.

44 Adds a little padding to the right of this EditText to put a little space between the date and the time text. Without this space, the two sets of text look very crowded.

46 Allows the user to edit the task’s notes. You’ll place this below the title. You’ll also give it a minimum of 5 lines of height to accommodate longer notes. You set the gravity to "top" to make the text align with the top of the text field because it looks better.

53 The default gravity for a TextView (as well as an EditText) is center. This looks a little funny for a multiline TextView, so change the gravity to "top". You may recall using the layout_gravity parameter in Chapter 4. layout_gravity tells Android how to position the current view inside its parent, whereas the gravity parameter tells Android how to position the content inside the current view.

55 Make sure this EditText is at least 5 lines high. It expands to more lines for extra‐long notes.

You used a couple of new strings in the layout, so open up strings.xml and add them there:

<string name="title">Title</string>
<string name="notes">Notes</string>

Creating the fragment

Now that you’ve created the layout, it’s time to create the fragment. Create a new Java file in com/dummies/tasks/fragment named TaskEditFragment.java, and add the following to it:

public class TaskEditFragment extends Fragment {

public static final String DEFAULT_FRAGMENT_TAG = "taskEditFragment"; → 3

// Views
View rootView;
EditText titleText;
EditText notesText;
ImageView imageView;

long taskId; →11

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Bundle arguments = getArguments(); →17
if (arguments != null) {
taskId = arguments.getLong(TaskEditActivity.EXTRA_TASKID, 0L);
}
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

View v = inflater.inflate(R.layout.fragment_task_edit, →27
container, false);

rootView = v.getRootView(); →30
titleText = (EditText) v.findViewById(R.id.title);
notesText = (EditText) v.findViewById(R.id.notes);
imageView = (ImageView) v.findViewById(R.id.image);

return v;
}
}

The class in the previous code does essentially two things. It knows how to

· Inflate the layout and find the views in the layout.

· Read the task ID from the fragment arguments.

Here are more details on the previous code:

3 The “name” that you’ll usually use to identify this fragment. You need this constant later in the chapter when you use the FragmentManager to add the fragment to the activity.

11 The ID of the task being edited. When editing existing tasks, the ID is the ID of the task. When creating a new task, the ID is initially 0, but it is set to the new ID of the task after the task is saved.

17 Sets the task ID from the intent arguments, if available. Fragments do not get their arguments from their constructors like normal Java objects. Instead, you must get and set their arguments using the getArguments and setArguments methods.

27 Inflates the layout and sets the container. The layout is the view that you will return.

30 From the layout, gets a few views that you’re going to work with.

Those are the basics of creating a fragment that can show the details of a task. However, there’s one more very important step that you must not leave out. You must remember to persist your in‐memory state whenever the fragment is destroyed, and use that persisted state (if it exists) when your fragment starts up.

Saving your state

Android activities can be killed at any time to save memory when they’re in the background. If the user returns to the activity, it may be re‐created again. When it’s destroyed, the Android OS automatically saves the current state of any views (such as EditTexts) that the user may have changed into the outState bundle. When it’s re‐created, those values are automatically set for you in onCreate from the savedInstanceState bundle.

However, although Android can save the state of views for you automatically, it cannot do that for non‐views. So any time you have any state that you or the user may modify in your ­fragments or activities, it’s UP TO YOU to make sure you save them properly! To do this, just save the value to the outState bundle, and then read it back in again from the ­savedInstanceState bundle in ­onCreate.

DO NOT FORGET TO DO THIS!

If you forget, your app will appear to work fine. You won’t immediately notice anything wrong. But any time you rotate your phone, or leave an app running for awhile in the background and then later return to it, you may experience random crashes and unexpected behavior. It won’t happen every time, and it will be very difficult to track down.

To make these situations easier to discover, you can enable the “Don’t keep activities” option in the developer options for your phone. This tells Android to destroy each activity as soon as it goes to the background, as happens when your phone runs low on memory. If you hit the Back button to return to the activity, it is re‐created from its outState bundle. See Chapter 5 to refresh your memory on how to access the developer options for your phone.

Saving your fragment’s state involves two things:

· Overriding onSaveInstanceState to save any of your fields that may have changed while the fragment was running.

· Checking for any saved instance state when the activity is created in onCreate.

Go back to your TaskEditFragment.java and add the lines in bold:

public class TaskEditFragment extends Fragment {

static final String TASK_ID = "taskId"; →3

. . .
long taskId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

. . .

if (savedInstanceState != null) { →13
taskId = savedInstanceState.getLong(TASK_ID);
}
}

@Override →18
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);

// This field may have changed while our activity was
// running, so make sure we save it to our outState bundle so
// we can restore it later in onCreate.
outState.putLong(TASK_ID, taskId);
}
}

3 The name of the entry that you’ll use to store the taskId when the fragment needs to save its state.

13 Restores the taskId from savedInstanceState, if available.

18 onSaveInstanceState is called whenever Android needs to destroy your fragment, most likely to free up memory. However, it may need to resurrect your fragment again later. You are responsible for saving any fields that may have changed into the bundle so that they can be used later when the activity is resurrected. See the “Saving your state” sidebar for more information.

Finally, there’s one more thing you need to do in this fragment. Because the fragment needs the ID of the task that it is going to edit, you need to create a method that can create a new fragment for a given ID. To do this, add the following static factory method to theTaskEditFragment class:

public static TaskEditFragment newInstance(long id) {
TaskEditFragment fragment = new TaskEditFragment();
Bundle args = new Bundle();
args.putLong(TaskEditActivity.EXTRA_TASKID, id);
fragment.setArguments(args);
return fragment;
}

You’ll use this method in the next section.

But first, your page needs an image. Add the following to onCreateView:

// Set the thumbnail image
Picasso.with(getActivity())
.load(TaskListAdapter.getImageUrlForTask(taskId))
.into(imageView);

You Put the Fragment in the Activity and Shake It All Up

Now, you have an activity that you can reach by clicking on an item in the list, and you have a fragment that displays the details of an item, but you do not yet have any way to see the fragment from the activity. What you need to do is put the fragment in the activity.

You may recall from Chapter 9 that this was super‐easy to do. All you needed to do was put a <fragment> tag in your activity’s layout, and everything worked magically.

Things are a little more complicated this time around.

In Chapter 9, the fragment did not take any parameters, so it was easy to instantiate. The fragment in this chapter, on the other hand, does need a parameter. Specifically, it needs to know the ID of the task that’s being edited. Because of this difference, you cannot just inflate the fragment in the layout. You must create the fragment programmatically using the FragmentManager.

Open up TaskEditActivity and add the lines in bold:

public class TaskEditActivity extends Activity {

. . .

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_edit);
setActionBar((Toolbar) findViewById(R.id.toolbar));

long id = getIntent().getLongExtra(TaskEditActivity.EXTRA_TASKID,0L); → 11
Fragment fragment = TaskEditFragment.newInstance(id);

String fragmentTag = TaskEditFragment.DEFAULT_FRAGMENT_TAG; →14

if (savedInstanceState == null) →16
getFragmentManager().beginTransaction().add(
R.id.container,
fragment,
fragmentTag).commit();
}
}

In brief, you are getting the ID of the task from the activity’s intent extras, using that ID to create a new TaskEditFragment, and then using the FragmentManager to add the fragment to your activity in a fragment transaction. Some comments about the code are

11 Creates a new edit fragment for the specified task ID.

14 The tag that you’ll use to add the fragment to the activity. This allows you to reference this fragment from other fragments, such as the Date and Time picker dialog fragments, which you will create in Chapter 12.

16 Adds the fragment if it has not already been added to the FragmentManager. If you don’t do this, a new fragment will be added every time this method is called (such as on orientation change). The FragmentManager saves previously added fragments to thesavedInstanceState. So if savedInstanceState is null, you know there were no previous fragments. The fragment is attached as a child of the container view.

Updating the Styles

You’re close to being able to view your handiwork. You can run it now, but there will be some bugs because there are a couple of changes you need to make to your activity’s style.

This activity is a bit different from the list activity, so it should have a ­different style. Because the page is a detail view, you should remove as much of the app’s distracting “chrome” from the page as possible, so that users can focus on their beautiful content. Things like the action bar, the status bar, and the navigation bar are all going to be reduced in prominence. You wouldn’t want to do this on every page of your app because that chrome can give users a visual indication of where they are in the app and what app they’re using, but it can be a powerful technique if used ­judiciously.

Open styles.xml and add the following to the bottom of the file:

<!-- A variation of the AppTheme, but with a transparent (clear)
ActionBar. Also has a translucent (grey) status and navigation
bar. -->
<style name="AppTheme.TransparentActionBar" parent="AppTheme"> →4
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowActionBarOverlay">true</item> →6
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:actionBarStyle">@style/TransparentActionBar</item> → 8
</style>

<!-- The TransparentActionBar style used by
AppTheme.TransparentActionBar -->
<style name="TransparentActionBar" parent="android:Theme.Material">
<item name="android:background">@android:color/transparent</item> → 14
</style>

About the previous code:

4 This style definition is for activities that want a transparent action bar. It also uses a translucent (slightly opaque) status and navigation bar. Notice that it inherits from the AppTheme you created in Chapter 9.

6 Normally the action bar is at the top of the page, and all the other content (such as the image) is arranged below it. However, because the action bar is transparent, you want to move the image all the way up to the top of the screen so that it is under the action bar. Setting the action bar to windowActionBarOverlay=true is the way to do that.

8 The way to make the action bar transparent is to set its style, so set the action bar style to a style that you will create on line 14.

14 Sets the background color of the action bar style to ­transparent.

Next, update the following lines in your AndroidManifest.xml:

<activity
android:theme="@style/AppTheme.TransparentActionBar"
android:name="com.dummies.tasks.activity.TaskEditActivity"
android:label=""
android:parentActivityName=
"com.dummies.tasks.activity.TaskListActivity">
. . .

Now run your app! You should be able to click on any item in the list and have it open a blank edit fragment like Figure 10-1:

image

Figure 101: The blank Edit activity and fragment.

A Special Bonus

There’s a fun library called Palette that scans an image and picks out a few key colors. Try using this library to add some dynamic color to your fragment.

First, add the library to your dependencies in build.gradle:

compile 'com.android.support:palette-v7:21.0.3'

Then, update your call to Picasso in TaskEditFragment.java to be like the following:

Picasso.with(getActivity())
.load(TaskListAdapter.getImageUrlForTask(taskId))
.into(
imageView, new Callback() { →4
@Override
public void onSuccess() { →6
Activity activity = getActivity();

if (activity == null) →9
return;

// Set the colors of the activity based on the
// colors of the image, if available
Bitmap bitmap = ((BitmapDrawable) imageView
.getDrawable())
.getBitmap(); →16
Palette palette →17
= Palette.generate(bitmap, 32);
int bgColor = palette.getLightMutedColor(0);

if (bgColor != 0) {
rootView.setBackgroundColor(bgColor); → 22
}
}

@Override
public void onError() {
// do nothing, we'll use the default colors
}
});

What is this code doing?

4 Like before, you’re still telling Picasso to load the image “into” the imageView. However, this time you’re also adding a callback, which is invoked when the image is done loading. This callback is where you do the magic if you are inspecting the image for colors.

6 The callback has two methods: onSuccess and onError. If the image successfully loaded, onSuccess is called so that you can inspect the image. If there was an error, you do nothing.

9 Because Picasso downloads images in the background, you can’t be sure that the user didn’t close the activity while the images were loading. If he did, you will bomb out, so do a sanity check to be sure.

16 You’ll get the image from the imageView because it’s not passed into the callback for you directly.

17 Uses the Palette library to generate a color palette for the image. From that palette, you’ll pick out the “light muted color” to use as the background for your activity.

22 If Palette can find a color for you, then use it to set your background. Palette is usually but not always successful at finding colors.

For more information about the Palette library, visit https://d.android.com/reference/android/support/v7/graphics/Palette.html.