Lesson Tuesday

In Android, we can create alternative resources, such as layouts, and our device will automatically select the correct resource based on a built-in set of naming conventions. Alternative resources support different device configurations, including screen sizes and orientations.

As we saw in the previous lesson, when a user visits our list activities while in landscape mode, we want to display the list of restaurants on the left and the detail view on the right, like this:

landscape-detail-layout-my-restaurants

Thankfully, we've already refactored the necessary activities into fragments so we can easily call in multiple fragments into a single activity. In this lesson, we'll create alternative resource layouts for our restaurant detail and "Saved Restaurants" areas of our application specially-formatted to display when the phone is in landscape orientation.

Required Reading

Before we begin, read more about Alternative Resources in the Android Developer's Guides here.

Creating Alternative Resource Layout Files

To begin, we will need to create separate layout files for Android to use when the phone is in landscape mode. Just like creating an ordinary layout file, right click on the layout folder and select New > Layout resource file:

creating-new-layout-resource

First we'll create an alternate landscape-orientation layout for our existing activity_restaurants layout. When naming an alternative resource file, always name the file exactly as the filename of the layout we will be replacing. So, let's also name this new layout activity_restaurants.

In the Available qualifiers panel on the left of the New Resource menu, scroll down and select Orientation:

orientation-in-available-qualifiers-panel

Add it to the Chosen qualifiers by selecting it, and hitting the >> button, then select Landscape from the Screen orientation dropdown:

landscape-orientation-in-layout

This new layout will automatically be inflated when the phone's orientation changes to landscape! Follow this exact same process to make an alternative resource landscape-orientation layout for activity_saved_restaurant_list too.

Once done, the left-hand sidebar in Android should list two layouts under each layout name, like so:

multiple-alternative-resource-layout-files

Writing Alternative Resource XML Layouts

activity_restaurants

Now, let's add our restaurant list fragment and a placeholder for our detail fragment into our new landscape activity_restaurants layout:

activity_restaurants.xml(land)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:id="@+id/LinearLayout1"
              android:showDividers="middle"
              android:baselineAligned="false"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent" >

    <fragment
        android:id="@+id/fragmentItemsList"
        android:name="com.epicodus.myrestaurants.ui.RestaurantListFragment"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="3"
        tools:layout="@layout/fragment_restaurant_list" />

    <View android:background="@color/colorAccent"
          android:layout_width="2dp"
          android:layout_height="wrap_content"/>

    <FrameLayout
        android:id="@+id/restaurantDetailContainer"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="6">

    </FrameLayout>

</LinearLayout>
  • We use a View element to add a vertical divider between our list and detail views, as we can see in the image at the top of this lesson.

  • We use something called a FrameLayout to act as a placeholder for our RestaurantDetailFragment. A FrameLayout is used when blocking out an area on the screen to display a single item. It should hold only one child view.

  • When the RestaurantDetailFragment is rendered (whether by the application rendering the first restaurant in the list by default, or the user selecting a restaurant) it will replace the FrameLayout.

activity_saved_restaurant_list

We'll add the SavedRestaurantListFragment and a placeholder FrameLayout to our activity_ saved_restaurant_list layout as well:

activity_saved_restaurant_list.xml(land)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:showDividers="middle"
              android:baselineAligned="false"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent" >

    <fragment
        android:id="@+id/fragmentItemsList"
        android:name="com.epicodus.myrestaurants.ui.SavedRestaurantListFragment"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="3"
        tools:layout="@layout/fragment_restaurant_list" />

    <View android:background="@color/colorAccent"
          android:layout_width="2dp"
          android:layout_height="wrap_content"/>

    <FrameLayout
        android:id="@+id/restaurantDetailContainer"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="6">

    </FrameLayout>

</LinearLayout>

Changing Layouts Based on Device Orientation

Next, we need to programmatically replace the temporary FrameLayout with a RestaurantDetailFragment when the user selects a restaurant; but only when the phone is in landscape mode. First, we'll do this in the "Find Restaurants" area of our application, and afterwards we'll add this functionality to the "Saved Restaurants" area.

Find Restaurants

To do this, we will add a conditional to the RestaurantViewHolder (which is actually a subclass of the RestaurantListAdapter) that will create a new instance of the RestaurantDetailFragment when a particular restaurant is selected if the device is currently in landscape-orientation:

First, we'll need a new member variable to represent the orientation of the device:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
       ...
      private int mOrientation;
    ...
...

Next, we'll check for the orientation of the device in the RestaurantViewHolder's constructor, and create a fragment if it is in landscape:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
       ...
     public RestaurantViewHolder(View itemView) {
       ...
       // Determines the current orientation of the device:
        mOrientation = itemView.getResources().getConfiguration().orientation;

        // Checks if the recorded orientation matches Android's landscape configuration.
        // if so, we create a new DetailFragment to display in our special landscape layout: 
        if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
            createDetailFragment(0);
        }
       ...
    ...

Here, we determine the orientation and call a method named createDetailFragment() if the device is in landscape orientation. The 0 argument will default to displaying the first restaurant's details when the list activity is first created.

Let's create this method now:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
       ...

    // Takes position of restaurant in list as parameter:
    private void createDetailFragment(int position) {
        // Creates new RestaurantDetailFragment with the given position:
        RestaurantDetailFragment detailFragment = RestaurantDetailFragment.newInstance(mRestaurants, position);
        // Gathers necessary components to replace the FrameLayout in the layout with the RestaurantDetailFragment:
        FragmentTransaction ft = ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction();
        //  Replaces the FrameLayout with the RestaurantDetailFragment:
        ft.replace(R.id.restaurantDetailContainer, detailFragment);
        // Commits these changes: 
        ft.commit();
    }
...

Here, we call a built-in Android interface called FragmentManager, which is responsible for interacting with Fragment objects. We then call beginTransaction() to open up the capability to make changes to this activity, as described in the method's documentation. Finally, we instruct Android to replace the FrameLayout in our layout (which we gave the id restaurantDetailContainer) with a new RestaurantDetailFragment, and commit these changes.

This will eventually result in a new RestaurantDetailFragment being rendered on the right-side of our layout:

restaurantdetailfragment

However, the way we're currently calling the method would only display the first restaurant's details when the fragment first loads. Let's also ensure users are able to select any restaurant from the list and see its details:

RestaurantListAdapter.java
public class RestaurantListAdapter extends RecyclerView.Adapter<RestaurantListAdapter.RestaurantViewHolder> {
    ...
    public class RestaurantViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        ...
        public RestaurantViewHolder(View itemView) {
            ...
            itemView.setOnClickListener(this);
        }

        ...

        @Override
        public void onClick(View v) {
            // Determines the position of the restaurant clicked:
            int itemPosition = getLayoutPosition();
           if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
                createDetailFragment(itemPosition);
            } else {
                Intent intent = new Intent(mContext, RestaurantDetailActivity.class);
                intent.putExtra(Constants.EXTRA_KEY_POSITION, itemPosition);
                intent.putExtra(Constants.EXTRA_KEY_RESTAURANTS, Parcels.wrap(mRestaurants));
                mContext.startActivity(intent);
            }
        }
   ...

Here, when the user clicks on a restaurant from the list, we use getLayoutPosition() to determine which restaurant was selected. We check the orientation of the device again. If it's in landscape we create a new RestaurantDetailFragment by calling createDetailFragment() and specifying which restaurant's details to display with mPosition.

This means that the page will always default to showing the details of the first restaurant in the list; but as soon as another restaurant is selected, it will display their details instead.

As you can see above, we're also including the position and list of restaurants in the else block of onClick(), using keys from our Constants class. Let's add these key values to our Constants class now:

Constants.java
...
    public static final String EXTRA_KEY_POSITION = "position";
    public static final String EXTRA_KEY_RESTAURANTS = "restaurants";
...

Now that the RestaurantDetailFragment must be able to quickly switch between multiple restaurants' details in our new orientation-mode, we must pass it multiple arguments. Instead of handing it a single restaurant, we'll need to provide it all restaurants, and the specific position of the one we currently want to display.

Let's make these changes now. First, we'll declare mPosition and mRestaurants, and provide them as parameters to our newInstance() method. Within the method, we'll bundle our new arguments:

RestaurantDetailFragment.java
...
   public class RestaurantDetailFragment extends Fragment implements View.OnClickListener {
    ...
    private ArrayList<Restaurant> mRestaurants;
    private int mPosition;

    public static RestaurantDetailFragment newInstance(ArrayList<Restaurant> restaurants, Integer position) {
        RestaurantDetailFragment restaurantDetailFragment = new RestaurantDetailFragment();
        Bundle args = new Bundle();

        args.putParcelable(Constants.EXTRA_KEY_RESTAURANTS, Parcels.wrap(restaurants));
        args.putInt(Constants.EXTRA_KEY_POSITION, position);

        restaurantDetailFragment.setArguments(args);
        return restaurantDetailFragment;
    }
...

We'll also need to change the onCreate() method. When our fragment is created, it will need access to the parameters we've provided:

RestaurantDetailFragment.java
...
@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mRestaurants = Parcels.unwrap(getArguments().getParcelable(Constants.EXTRA_KEY_RESTAURANTS));
        mPosition = getArguments().getInt(Constants.EXTRA_KEY_POSITION);
        mRestaurant = mRestaurants.get(mPosition);
    }
...
  • First, we retrieve the parceled mRestaurants from the parameters we passed into newInstance() by utilizing the key in our Constants class.

  • Next, we retrieve the mPosition variable, which represents the specific position of the restaurant we'd like to display.

  • We get the specific restaurant at the position indicated, and define it as mRestaurant. mRestaurant is then used later on in onCreateView() and when we assign our click listeners. We don't need to make any further changes to that logic.

We just need to make sure both parameters for new RestaurantDetailFragments are included when we call newInstance() in our RestaurantPagerAdapter, too:

RestaurantPagerAdapter.java
...
    @Override
    public Fragment getItem(int position) {
        return RestaurantDetailFragment.newInstance(mRestaurants, position);
    }
...

If we run our app, navigate to the RestaurantListActivity and then change the layout to landscape, we should now see that it displays both the RestaurantListFragment and the RestaurantDetailFragment. Let's make sure our app behaves the same way when we navigate to the SavedRestaurantListActivity, too.

Saved Restaurants

This process will be similar to what we just did, but rather than adding these changes to the ViewHolder, we will add it to our adapter since that is where we have access to our ArrayList of restaurants.

Let's start by adding the orientation member variable:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...
    private int mOrientation;
...

Then, inside of the populateViewHolder() method, we will set the orientation. If the phone is currently in landscape mode, we will create a detail fragment:

FirebaseRestaurantListAdapter.java
public class FirebaseRestaurantListAdapter extends FirebaseRecyclerAdapter<Restaurant, FirebaseRestaurantViewHolder>  implements ItemTouchHelperAdapter {
    ...

      @Override
    protected void populateViewHolder(final FirebaseRestaurantViewHolder viewHolder, Restaurant model, int position) {
        viewHolder.bindRestaurant(model);

        mOrientation = viewHolder.itemView.getResources().getConfiguration().orientation;
        if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
            createDetailFragment(0);
        }

        viewHolder.mRestaurantImageView.setOnTouchListener(new View.OnTouchListener() {
            ... 
        });

        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
           ...
        });

    }

    private void createDetailFragment(int position) {
        // Creates new RestaurantDetailFragment with the given position:
        RestaurantDetailFragment detailFragment = RestaurantDetailFragment.newInstance(mRestaurants, position);
        // Gathers necessary components to replace the FrameLayout in the layout with the RestaurantDetailFragment:
        FragmentTransaction ft = ((FragmentActivity) mContext).getSupportFragmentManager().beginTransaction();
        //  Replaces the FrameLayout with the RestaurantDetailFragment:
        ft.replace(R.id.restaurantDetailContainer, detailFragment);
        // Commits these changes:
        ft.commit();
    }
}

Finally, we need to update our existing itemView click listener:

FirebaseRestaurantListAdapter.java
...
@Override
    protected void populateViewHolder(final FirebaseRestaurantViewHolder viewHolder, Restaurant model, int position) {
        ...
        viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int itemPosition = viewHolder.getAdapterPosition();
                if (mOrientation == Configuration.ORIENTATION_LANDSCAPE) {
                    createDetailFragment(itemPosition);
                } else {
                    Intent intent = new Intent(mContext, RestaurantDetailActivity.class);
                    intent.putExtra(Constants.EXTRA_KEY_POSITION, itemPosition);
                    intent.putExtra(Constants.EXTRA_KEY_RESTAURANTS, Parcels.wrap(mRestaurants));
                    mContext.startActivity(intent);
                }
            }
        });
    }

More Alternative Layouts

Finally, let’s also add alternative layout files for our restaurant_list_item, restaurant_list_item_drag, and fragment_restaurant_detail.

In these list item layouts, we will change the layout so that it better displays in a narrower column when the phone is in landscape. Follow the same process we did earlier, and add Orientation landscape as a qualifier to both:

restaurant_list_item_drag.xml(land)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" >

        <ImageView
            android:layout_width="130dp"
            android:layout_height="100dp"
            android:id="@+id/restaurantImageView"
            android:src="@drawable/waffles"
            android:scaleType="centerCrop"/>

        <ImageView
            android:id="@+id/dragIcon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignLeft="@id/restaurantImageView"
            android:layout_alignBottom="@id/restaurantImageView"
            android:gravity="bottom|left"
            android:src="@drawable/ic_reorder_white_24dp" />

    </RelativeLayout>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/restaurantNameTextView"
            android:textSize="14dp"
            android:text="Restaurant Name"
            android:textColor="@color/colorPrimary"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="One Cuisine Type"
            android:id="@+id/categoryTextView"
            android:textSize="10dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:textStyle="italic"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rating"
            android:visibility="gone"
            android:id="@+id/ratingTextView"
            android:textColor="@color/colorAccent"/>
    </LinearLayout>
</LinearLayout>
restaurant_list_item.xml(land)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <ImageView
        android:layout_width="130dp"
        android:layout_height="100dp"
        android:id="@+id/restaurantImageView"
        android:src="@drawable/waffles"
        android:scaleType="centerCrop"/>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/restaurantNameTextView"
            android:textSize="14dp"
            android:text="Restaurant Name"
            android:textColor="@color/colorPrimary"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="One Cuisine Type"
            android:id="@+id/categoryTextView"
            android:textSize="10dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:textStyle="italic"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rating"
            android:visibility="gone"
            android:id="@+id/ratingTextView"
            android:textColor="@color/colorAccent"/>
    </LinearLayout>
</LinearLayout>
  • We set the visibility for the ratingTextView to "gone" to avoid crowding the smaller list item area we have in landscape mode. We don't remove it entirely to avoid errors in our ViewHolder.

And finally, the alternate layout for our restaurant detail fragment:

fragment_restaurant_detail.xml(land)
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             tools:context="com.epicodus.myrestaurants.ui.RestaurantDetailFragment">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary">

        <LinearLayout
            android:orientation="horizontal"
            android:layout_weight="3"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:layout_weight="1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:id="@+id/restaurantImageView"
                android:src="@drawable/waffles"
                android:scaleType="centerCrop" />

            <LinearLayout
                android:layout_weight="1"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:layout_marginLeft="10dp"
                    android:layout_weight="1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="View on Yelp"
                    android:id="@+id/websiteTextView"
                    android:drawableLeft="@drawable/ic_exit_to_app_white_24dp"
                    android:drawablePadding="4dp"
                    android:textColor="@color/colorTextIcons"
                    android:textSize="12sp"
                    android:textStyle="bold"
                    android:gravity="center" />

                <TextView
                    android:layout_marginLeft="20dp"
                    android:layout_weight="1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="(503) 223-1282"
                    android:id="@+id/phoneTextView"
                    android:drawableLeft="@drawable/ic_local_phone_white_24dp"
                    android:drawablePadding="4dp"
                    android:textColor="@color/colorTextIcons"
                    android:textSize="12sp"
                    android:gravity="center"/>

                <TextView
                    android:layout_marginLeft="20dp"
                    android:layout_marginRight="20dp"
                    android:layout_weight="1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="123 SW Best Ever Ave. Portland, Oregon, 97222"
                    android:drawableLeft="@drawable/ic_map_white_24dp"
                    android:drawablePadding="4dp"
                    android:id="@+id/addressTextView"
                    android:textColor="@color/colorTextIcons"
                    android:textSize="12sp"
                    android:textStyle="bold"
                    android:gravity="center_vertical"/>

            </LinearLayout>

        </LinearLayout>

        <LinearLayout
            android:orientation="horizontal"
            android:layout_weight="5"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:gravity="center"
            android:padding="10dp">

            <LinearLayout
                android:orientation="vertical"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Restaurant Name"
                    android:id="@+id/restaurantNameTextView"
                    android:textSize="20sp"
                    android:textColor="@color/colorPrimaryDark"
                    android:textStyle="bold" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Brunch, American"
                    android:id="@+id/cuisineTextView"
                    android:textColor="@color/colorSecondaryText"
                    android:textStyle="italic" />

            </LinearLayout>

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="4.5/5"
                    android:id="@+id/ratingTextView"
                    android:textColor="@color/colorAccent"
                    android:textStyle="bold"
                    android:layout_marginLeft="20dp"
                    android:layout_centerVertical="true"
                    android:layout_alignParentRight="true"/>

            </RelativeLayout>

        </LinearLayout>

        <Button
            android:layout_weight="6"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="Save Restaurant"
            android:id="@+id/saveRestaurantButton"
            android:layout_alignParentBottom="true"
            android:background="@color/colorAccent"
            android:textColor="@color/colorTextIcons"
            android:textSize="15sp" />
    </LinearLayout>
</FrameLayout>

Let’s run our app and change the orientation of the phone using the arrows on the side of the emulator. We should now see the new layout we created appears automatically!


Example GitHub Repo for MyRestaurants

Terminology


  • Alternative Resources: Resources that Android can choose from based on different device configurations such as screen sizes or orientations.

Tips


  • When naming an alternative resource file, always name the file exactly as the filename of the layout we will be replacing.

Examples


Additional Resources