Fragments walkthrough – landscape

The Fragments Walkthrough – Part 1 demonstrated how to create and use fragments in an Android app that targets the smaller screens on a phone. The next step in this walkthrough is to modify the application to take advantage of the extra horizontal space on tablet – there will be one activity that will always be the list of plays (the TitlesFragment) and PlayQuoteFragment will be dynamically added to the Activity in response to a selection made by the user:

App running on tablet

Phones that are running in landscape mode will also benefit from this enhancement:

App running on an Android phone in landscape mode

Updating the app to handle landscape orientation

The following modifications will build upon the work that was done in the Fragments Walkthrough - Phone

  1. Create an alternate layout to display both the TitlesFragment and PlayQuoteFragment.
  2. Update TitlesFragment to detect if the device is displaying both fragments simultaneously and change behavior accordingly.
  3. Update PlayQuoteActivity to close when the device is in landscape mode.

1. Create an alternate layout

When Main Activity is created on an Android device, Android will decide which layout to load based on the orientation of the device. By default, Android will provide the Resources/layout/activity_main.axml layout file. For devices that load in landscape mode Android will provide the Resources/layout-land/activity_main.axml layout file. The guide on Android Resources contains more details on how Android decides what resource files to load for an application.

Create an alternate layout that targets Landscape orientation by following the steps described in the Alternate Layouts guide. This should add a new layout resource file to the project, Resources/layout/activity_main.axml:

After creating the alternate layout, edit the source of the file Resources/layout-land/activity_main.axml so that it matches this XML:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/two_fragments_layout"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:name="FragmentSample.TitlesFragment"
        android:id="@+id/titles"
        android:layout_weight="1"
        android:layout_width="0px"
        android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/playquote_container"
            android:layout_weight="1"
            android:layout_width="0px"
            android:layout_height="match_parent"
             />
</LinearLayout>

The root view of the activity is given the resource ID two_fragments_layout and has two sub-views, a fragment and a FrameLayout. While the fragment is statically loaded, the FrameLayout acts as a "placeholder" that will be replaced at run-time by the PlayQuoteFragment. Each time a new play is selected in the TitlesFragment, the playquote_container will be updated with a new instance of the PlayQuoteFragment.

Each of the sub-views will occupy the full height of their parent. The width of each subview is controlled by the android:layout_weight and android:layout_width attributes. In this example, each subview will occupy 50% of width provide by the parent. See Google's document on the LinearLayout for details about Layout Weight.

2. Changes to TitlesFragment

Once the alternate layout has been created, it is necessary to update TitlesFragment. When the app is displaying the two fragments on one activity, then TitlesFragment should load the PlayQuoteFragment in the parent Activity. Otherwise, TitlesFragment should launch the PlayQuoteActivity which host the PlayQuoteFragment. A boolean flag will help TitlesFragment determine which behavior it should use. This flag will be initialized in the OnActivityCreated method.

First, add an instance variable at the top of the TitlesFragment class:

bool showingTwoFragments;

Then, add the following code snippet to OnActivityCreated to initialize the variable:

var quoteContainer = Activity.FindViewById(Resource.Id.playquote_container);
showingTwoFragments = quoteContainer != null &&
                      quoteContainer.Visibility == ViewStates.Visible;
if (showingTwoFragments)
{
    ListView.ChoiceMode = ChoiceMode.Single;
    ShowPlayQuote(selectedPlayId);
}

If the device is running in landscape mode, then the FrameLayout with the resource ID playquote_container will be visible on the screen, so showingTwoFragments will be initialized to true. If the device is running in portrait mode, then playquote_container will not be on the screen, so showingTwoFragments will be false.

The ShowPlayQuote method will need to change how it displays a quote – either in a fragment or launch a new activity. Update the ShowPlayQuote method to load a fragment when showing two fragments, otherwise it should launch an Activity:

void ShowPlayQuote(int playId)
{
    selectedPlayId = playId;
    if (showingTwoFragments)
    {
        ListView.SetItemChecked(selectedPlayId, true);

        var playQuoteFragment = FragmentManager.FindFragmentById(Resource.Id.playquote_container) as PlayQuoteFragment;

        if (playQuoteFragment == null || playQuoteFragment.PlayId != playId)
        {
            var container = Activity.FindViewById(Resource.Id.playquote_container);
            var quoteFrag = PlayQuoteFragment.NewInstance(selectedPlayId);

            FragmentTransaction ft = FragmentManager.BeginTransaction();
            ft.Replace(Resource.Id.playquote_container, quoteFrag);
            ft.Commit();
        }
    }
    else
    {
        var intent = new Intent(Activity, typeof(PlayQuoteActivity));
        intent.PutExtra("current_play_id", playId);
        StartActivity(intent);
    }
}

If the user has selected a play that is different from the one that is currently being displayed in PlayQuoteFragment, then a new PlayQuoteFragment is created and will replace the contents of the playquote_container within the context of a FragmentTransaction.

Complete code for TitlesFragment

After completing all the previous changes to TitlesFragment, the complete class should match this code:

public class TitlesFragment : ListFragment
{
    int selectedPlayId;
    bool showingTwoFragments;

    public override void OnActivityCreated(Bundle savedInstanceState)
    {
        base.OnActivityCreated(savedInstanceState);
        ListAdapter = new ArrayAdapter<string>(Activity, Android.Resource.Layout.SimpleListItemActivated1, Shakespeare.Titles);

        if (savedInstanceState != null)
        {
            selectedPlayId = savedInstanceState.GetInt("current_play_id", 0);
        }

        var quoteContainer = Activity.FindViewById(Resource.Id.playquote_container);
        showingTwoFragments = quoteContainer != null &&
                                quoteContainer.Visibility == ViewStates.Visible;
        if (showingTwoFragments)
        {
            ListView.ChoiceMode = ChoiceMode.Single;
            ShowPlayQuote(selectedPlayId);
        }
    }

    public override void OnSaveInstanceState(Bundle outState)
    {
        base.OnSaveInstanceState(outState);
        outState.PutInt("current_play_id", selectedPlayId);
    }

    public override void OnListItemClick(ListView l, View v, int position, long id)
    {
        ShowPlayQuote(position);
    }

    void ShowPlayQuote(int playId)
    {
        selectedPlayId = playId;
        if (showingTwoFragments)
        {
            ListView.SetItemChecked(selectedPlayId, true);

            var playQuoteFragment = FragmentManager.FindFragmentById(Resource.Id.playquote_container) as PlayQuoteFragment;

            if (playQuoteFragment == null || playQuoteFragment.PlayId != playId)
            {
                var container = Activity.FindViewById(Resource.Id.playquote_container);
                var quoteFrag = PlayQuoteFragment.NewInstance(selectedPlayId);

                FragmentTransaction ft = FragmentManager.BeginTransaction();
                ft.Replace(Resource.Id.playquote_container, quoteFrag);
                ft.AddToBackStack(null);
                ft.SetTransition(FragmentTransit.FragmentFade);
                ft.Commit();
            }
        }
        else
        {
            var intent = new Intent(Activity, typeof(PlayQuoteActivity));
            intent.PutExtra("current_play_id", playId);
            StartActivity(intent);
        }
    }
}

3. Changes to PlayQuoteActivity

There is one final detail to take care of: PlayQuoteActivity is not necessary when the device is in landscape mode. If the device is in landscape mode the PlayQuoteActivity should not be visible. Update the OnCreate method of PlayQuoteActivity so that it will close itself. This code is the final version of PlayQuoteActivity.OnCreate:

protected override void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);

    if (Resources.Configuration.Orientation == Android.Content.Res.Orientation.Landscape)
    {
        Finish();
    }

    var playId = Intent.Extras.GetInt("current_play_id", 0);
    var playQuoteFrag = PlayQuoteFragment.NewInstance(playId);
    FragmentManager.BeginTransaction()
                    .Add(Android.Resource.Id.Content, playQuoteFrag)
                    .Commit();
}

This modification adds a check for the device orientation. If it is in landscape mode, then PlayQuoteActivity will close itself.

4. Run the application

Once these changes are complete, run the app, rotate the device to landscape mode (if necessary), and then select a play. The quote should be displayed on the same screen as the list of plays:

App running on an Android phone in landscape mode

App running on an Android tablet