Responsive design

To cater for the wide variety of device specifications, we need to consider screen size and density.

These are the 5 most popular density buckets:

  • mdpi (medium) ~160dpi
  • hdpi (high) ~240dpi
  • xhdpi (extra high) ~320dpi
  • xxhdpi (extra extra high) ~ 480dpi
  • xxxhdpi (extra extra extra high) ~640dpi

Density-independent pixels (dp’s) largely overcome these variations in density by making an image of e.g. 48dp look approximately the same physical size on all screens. **Make touch targets 48dp at least** However, you should supply a range of variations of each image to cater for the difference in density e.g. 48px:72px:96px:144px:192px in their appropriate folders to avoid placing unnecessary load on the processor and distorting the image, through scaling. To read more about how to cater for these go here.

For different screen layouts/orientations we can define individual xml files to cater for the variation in available space. Like the density buckets, you can place your layout files in the appropriately named folder and Android will use the layout that corresponds to its current configuration.

At runtime, Android will check the configuration of the device and choose the appropriate resources from the relevant folders accordingly. This is why it is necessary for Android to destroy and recreate Activities on screen rotation, as all of the resources within it could be completely different. Examples of folder variations:
values-fr/ – values for the French language
values-fr-rCA/ – values for the French-Canadian dialect
layout-desk/ – device is docked
layout-stylus/ – for device screens with stylus input
drawable-xhdpi/ – drawable resources for a xhdpi density screen
layout-land/ – landscape orientation
layout-sw720dp/ – minimum smallest screen width the layout will apply to

An example resource directory structure:

res/
   layout/
      activity_main.xml
      detail_activity.xml
      list_item.xml
   layout-sw600dp/
      detail_activity.xml
      list_item.xml
   layout-sw720dp/
      list_item.xml

To create a smallest-width qualifier folder, right-click on res->New->Android Resource Directory, Resource type: layout, Available qualifiers: Smallest Screen Width, enter screen width (in dp), then ‘OK’. File names must be identical across folders to them to be overridden.

Customising RecyclerViews according to position +/- orientation

If wanting to set differing layouts for a RecyclerView in a particular ViewHolder position +/- orientation (portrait or landscape), you can do the following:

Use the values, values-port and values-land folders to set booleans (or other) to indicate your preferences for layout according to orientation e.g. in values-port\bools.xml add

<bool name="use_expanded_layout">false</bool>

to state that you do not want to use your expanded layout in portrait mode (if desired).
Overriding getViewType will give you the position of the ViewHolder by which you can conditionally set which viewType to use. In your RecyclerView.Adapter file:

private final int CONDENSED_VIEWTYPE_ID = 83; // Randomly generated unique ID to correspond to condensed layout
private final int EXPANDED_VIEWTYPE_ID = 928; // Randomly generated unique ID to correspond to expanded layout
private boolean mUseExpandedLayout; // Declare a variable to determine layout preference
private final Context mContext; // We'll need this

    public RecyclerViewAdapter(@NonNull Context context, RecyclerViewAdapterOnClickHandler clickHandler) { // In the constructor...
        ...
        mContext = context;
        mUseExpandedLayout= context.getResources().getBoolean(R.bool.use_expanded_layout); // ... get the preference from your boolean. If in portrait orientation it will take this value from the values-port\bools.xml file if it exists, otherwise the values\bools.xml file
    }

    @Override
    public int getItemViewType(int position) { // Override getViewType, which gives you the ViewHolder position
        if (!mExpandedLayout || position != 0) { // Use this position to determine which viewType to apply. Here, if the boolean is false or position is not 0...
            return CONDENSED_VIEWTYPE_ID; // ...use the condensed viewType
        }
        return EXPANDED_VIEWTYPE_ID; // Otherwise use the expanded viewType
    }

    @Override
    public RecyclerViewAdapterViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { // In the onCreateViewHolder override...

        View view;

        if (viewType == CONDENSED_VIEWTYPE_ID) { // ...if the viewType has the condensed ID...
            view = LayoutInflater
                    .from(mContext)
                    .inflate(R.layout.condensed_list_item, viewGroup, false); // ...apply the condensed layout
        } else if (viewType == EXPANDED_VIEWTYPE_ID) { // If the viewType has the expanded ID...
            view = LayoutInflater
                    .from(mContext)
                    .inflate(R.layout.expanded_list_item, viewGroup, false); // ...apply the expanded layout
        } else {
            throw new IllegalArgumentException("Invalid layout type");
        }

        return new RecyclerViewAdapterViewHolder(view); // Creates the ViewHolder with the appropriate layout attached
    }

Include layouts

If creating multiple instances of the same layout it is good practice to save that layout code in its own file and instantiate it using the tag e.g. you are designing separate portrait and landscape layouts of the same content with groups of objects such as those in a table which will remain in an identical relative layout across both designs.

Given this original layout:

<?xml version="1.0" encoding="utf-8"?>
<layout>
<ScrollView xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.constraint.ConstraintLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:text="@string/passenger_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <TextView
            tools:text="@string/passenger_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

<!-- Start of section of code defining a group of objects in a fixed layout to be duplicated across portrait and landscape -->

        <ImageView
            android:id="@+id/leftRectangle"
            android:layout_width="60dp"
            android:layout_height="80dp"
            ... />

        <ImageView
            android:id="@+id/divider"
            android:background="@color/colorPrimaryLight"
            ... />

        <ImageView
            android:id="@+id/rightRectangle"
            android:layout_width="60dp"
            android:layout_height="80dp"
            ... />

        <TextView
            android:id="@+id/textViewOriginAirport"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewDestinationAirport"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <ImageView
            android:id="@+id/imagePlane"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewFlightCode"
            tools:text="@string/flight_code"
            android:layout_width="wrap_content"
            ... />

<!-- Endof section of code defining a group of objects -->

        <TextView
            android:id="@+id/textViewBoardingTimeLabel"
            android:text="@string/boarding_time_label"
            android:layout_width="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewBoardingTime"
            tools:text="@string/boarding_time"
            android:layout_width="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewDepartureTimeLabel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewDepartureTime"
            tools:text="@string/departure_time"
            android:layout_width="wrap_content"
            ... />

    </android.support.constraint.ConstraintLayout>
</ScrollView>
</layout>

The extracted code pasted into it separate layout file (name boarding_info.xml) would look like:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" // Very important to include these tags
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <android.support.constraint.ConstraintLayout // Very important to include these tags
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/leftRectangle"
            android:layout_width="60dp"
            android:layout_height="80dp"
            ... />

        <ImageView
            android:id="@+id/divider"
            android:background="@color/colorPrimaryLight"
            ... />

        <ImageView
            android:id="@+id/rightRectangle"
            android:layout_width="60dp"
            android:layout_height="80dp"
            ... />

        <TextView
            android:id="@+id/textViewOriginAirport"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewDestinationAirport"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <ImageView
            android:id="@+id/imagePlane"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewFlightCode"
            tools:text="@string/flight_code"
            android:layout_width="wrap_content"
            ... />

    </android.support.constraint.ConstraintLayout>

</layout>

And the original file would look like:

<layout>
<ScrollView xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.constraint.ConstraintLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:text="@string/passenger_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <TextView
            tools:text="@string/passenger_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <include
            android:id="@+id/boarding_info" // Essential for binding data
            layout="@layout/boarding_info" // Only essential attribute - others added on on aligning with surrounding elements
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewBoardingTimeLabel"
            android:text="@string/boarding_time_label"
            android:layout_width="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewBoardingTime"
            tools:text="@string/boarding_time"
            android:layout_width="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewDepartureTimeLabel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            ... />

        <TextView
            android:id="@+id/textViewDepartureTime"
            tools:text="@string/departure_time"
            android:layout_width="wrap_content"
            ... />

    </android.support.constraint.ConstraintLayout>
</ScrollView>
</layout>

If binding to these layouts you will need to add in an extra reference to the include file e.g.

mBinding.textViewOriginAirport.setText("aString");

becomes

mBinding.boardingInfo.textViewOriginAirport.setText("aString"); // boardingInfo automatically generated from 'boarding_info' xml name

Landscape layouts

To efficiently utilise the space available in a landscape orientation, Android allows the defining of an alternative layout.

Create a new layout resource folder named 'layout-land' and copy your portrait xml layout file into it. Then simply edit the layout according to your preferences. If there are elements of your layout that will remain identical across portrait and landscape modes it makes sense to relocate them into their own layout files as outlined in this post.