Layout xml merge tag

If you have a section of layout that you wish to reuse, it can be added using the <include> tag. Typically the code you are including would need a root view e.g. LinearLayout. However, if that root view would result in redundant code e.g. a vertical LinearLayout before the <include> tag immediately followed by the LinearLayout inside the reusable code, then instead of providing a root view for your reusable code then use the <merge> tag.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include layout="@layout/reusable_layout"/>

    <include layout="@layout/reusable_layout"/>

    <TextView android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/hello"
              android:padding="10dp" />

    ...

</LinearLayout>

 

 

// This is the block of reusable code
<merge xmlns:android="http://schemas.android.com/apk/res/android"> // No need for a root view here if it is to be nested inside a ViewGroup anyway. Putting a vertical LinearLayout here would produce a redundant ViewGroup

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/add"/>

    <Button
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/delete"/>

</merge>

 

Create Fragments

Add fragments to your app.

Create a new class for our Fragment:

public class AppFragment extends Fragment {

    public AppFragment() { // Empty constructor
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // Essential
        View rootView = inflater.inflate(R.layout.our_fragment, container, false); // Use a custom layout file (see below)

        ImageView imageView = rootView.findViewById(R.id.ourImageView); // Here we find a View within the layout

        imageView.setImageResource(R.drawable.anImage); // And allocate an image to it

        return rootView;
    }
}

Our layout file for the Fragment, our_fragment.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:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/ourImageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="An Image"
        tools:srcCompat="@drawable/anImage" /> // Placeholder image
</LinearLayout>

Create an instance of our Fragment in the Activity and use FragmentManager to add it to the layout for the Activity:

        AppFragment aFragment = new AppFragment(); // New instance of our custom Fragment

        FragmentManager fragmentManager = getSupportFragmentManager();

        fragmentManager.beginTransaction()
                .add(R.id.fragment_container, aFragment) // Adds the new fragment to an element of the Activity layout (see below)
                .commit();

Our layout file for the Activity, activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    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.example.android.app.MainActivity">

    <LinearLayout
        android:id="@+id/main_activity_linear_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <FrameLayout android:id="@+id/fragment_container" // View that will contain the Fragment
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:scaleType="centerInside"/>

    </LinearLayout>
</ScrollView>

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.

Data Binding Library

Links the UI to actual data without having to use findViewById for every item.

In build.gradle, add 'dataBinding.enabled = true':

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion "28.0.3"
    ...
    dataBinding.enabled = true;
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
}

Create a plain old Java object to act as a placeholder for the data:

/**
 * BoardingPassInfo is a simple POJO that contains information about, you guessed it, a boarding
 * pass! Normally, it is best practice in Java to declare member variables as private and provide
 * getters, but we are leaving these fields public for ease of use.
 */
public class BoardingPassInfo {

    public String passengerName;
    public String flightCode;
    public String originCode;
    public String destCode;

    public Timestamp boardingTime;
    public Timestamp departureTime;
    public Timestamp arrivalTime;

    public String departureTerminal;
    public String departureGate;
    public String seatNumber;

    public int barCodeImageResource;

    public long getMinutesUntilBoarding() {
        long millisUntilBoarding = boardingTime.getTime() - System.currentTimeMillis();
        return TimeUnit.MILLISECONDS.toMinutes(millisUntilBoarding);
    }
}

Add layout as the root tag to the UI (in this case, activity_main.xml file). Android will automatically create a Binding class for any layout with these tags:

<?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" />
            
            ...

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

In your Activity, create a Binding instance, and point the instance to the correct content view using DatabindingUtil:

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding mBinding; // ActivityMainBinding is automatically generated based on the activity_main.xml file name

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); // Passes in the Activity and sets the content view to the correct layout file
        BoardingPassInfo boardingPass = generateBoardingPassInfo(); // Whatever method to produce an instance of your placeholder class populated with data
        displayBoardingPassInfo(boardingPass); // Passes the placeholder instance to the method below
    }

    private void displayBoardingPassInfo(BoardingPassInfo info) {

        mBinding.textViewPassengerName.setText(info.passengerName);
        mBinding.textViewFlightCode.setText(info.flightCode);
        mBinding.textViewOriginAirport.setText(info.originCode);
        mBinding.textViewDestinationAirport.setText(info.destCode);
        mBinding.textViewTerminal.setText(info.departureTerminal);
        mBinding.textViewGate.setText(info.departureGate);
        mBinding.textViewSeat.setText(info.seatNumber);

        SimpleDateFormat formatter = new SimpleDateFormat("hh:mm a", Locale.getDefault());
        String boardingTime = formatter.format(info.boardingTime);
        mBinding.textViewBoardingTime.setText(boardingTime);
        String departureTime = formatter.format(info.departureTime);
        mBinding.textViewDepartureTime.setText(departureTime);
        String arrivalTime = formatter.format(info.arrivalTime);
        mBinding.textViewArrivalTime.setText(arrivalTime);

        long totalMinutesUntilBoarding = info.getMinutesUntilBoarding();
        long hoursUntilBoarding = TimeUnit.MINUTES.toHours(totalMinutesUntilBoarding);
        long minutesMinusHoursUntilBoarding = totalMinutesUntilBoarding - (hoursUntilBoarding*60);
        String hoursAndMinutesUntilBoarding = hoursUntilBoarding + ":" + minutesMinusHoursUntilBoarding;
        mBinding.textViewBoardingInCountdown.setText(hoursAndMinutesUntilBoarding);
    }
}

ud851-Exercises-student\Lesson11-Completeing-The-UI\T11.02