With the ability to run Android apps on Chromebooks, a huge ecosystem of apps and vast new functionality is now available to users. While this is great news for developers, certain app optimizations are required to meet usability expectations and to make for an excellent user experience.

One important, yet often overlooked, area is resizing. Android apps on Chrome OS should expect to be rotated, maximized, and "free-form" resized multiple times throughout their lifecycle. This code lab will guide you through implementing some best practices to make your app resize robustly and easily.

What you will build

You will build an Android app that demonstrates best practices for resizing. Your app will:

Manage state

Adapt to different window size

What you'll need

Clone the repository from GitHub

git clone https://github.com/googlecodelabs/resizing-chromeos

...or download a zip file of the repository and extract it

Download Zip

Import Project

Try the App

What do you think?

The technical implementation of the app could be working perfectly, but the user experience is not ideal.

What is happening with:

When you first load the app, you will notice that the app takes 5 seconds to fetch the reviews. This is hardcoded into our DataProvider and simulates a realistic network delay.

DataProvider.kt

fun fetchData(listener: Listener) {
    mainHandler.postDelayed({ listener.onSuccess(appData) },
            TimeUnit.SECONDS.toMillis(5))
}

What happens when you resize or rotate the app?

Unnecessary network fetches are hard on your server and provide a poor user experience. This is particularly true for users with slow or limited data access.

Once the reviews have been fetched the first time, store them in a ViewModel so they survive configuration changes. Use LiveData for elements that will have a UI component so that they can be automatically updated the ViewModel changes.

Create the ViewModel class - a ViewModel contains getters/setters using a singleton pattern. The DataProvider class is the simulated network data fetcher.

MainViewModel.kt

class MainViewModel : ViewModel() {

    private val internalProductName = MutableLiveData<String>()
    private val internalIsDescriptionExpanded = MutableLiveData<Boolean>()
    private val internalAppData = MutableLiveData<AppData>()
    private val reviewProvider = DataProvider()

    val productName: LiveData<String>
        get() = internalProductName

    fun setProductName(newName: String) {
        internalProductName.value = newName
    }

    val isDescriptionExpanded: LiveData<Boolean>
        get() = internalIsDescriptionExpanded

    fun setDescriptionExpanded(newState: Boolean) {
        internalIsDescriptionExpanded.value = newState
    }

    val appData: LiveData<AppData>
        get() = internalAppData

    init {
        reviewProvider.fetchData(object : DataProvider.Listener {
            override fun onSuccess(appData: AppData) {
                internalAppData.postValue(appData)
            }
        })
    }
}

You will notice that the fetchData logic has moved into the ViewModel so you can delete the reviewProvider variable and the fetchData call in onCreate.

Now delete your member variables in MainActivity isDescriptionExpanded, productName, and appData. Instead of holding a local copy, we will use the ViewModel to get and set this data.

Create a lateinit member variable for the ViewModel:

MainActivity.kt

private lateinit var viewModel : MainViewModel

In your onCreate, initialize the reference to the ViewModel:

MainActivity.kt (onCreate)

viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

Now update your data calls to use the ViewModel:

MainActivity.kt

if (viewModel.productName.value.equals(""))
    viewModel.setProductName(getString(R.string.label_product_name))
//Expand/collapse button for product description
buttonExpand.setOnClickListener { _ ->
    viewModel.appData.value?.let {
        toggleExpandButton();
    }
}
private fun getDescriptionText(appData: AppData?): String {
    if (null != appData)
        return if (viewModel.isDescriptionExpanded.value == true) appData.description else appData.shortDescription
    else
        return ""
}
private fun toggleExpandButton() {
    //Invert isDescriptionExpanded
    viewModel.setDescriptionExpanded(viewModel.isDescriptionExpanded.value == false)
}

Delete the updateDescription() method as we will replace its functionality in the next section.

Observers

Using ViewModel as above would allow the application to store the data and prevent extraneous network fetches. Attaching observers, however, makes this even more powerful and will allow for the UI to be updated automatically if the data changes

MainActivity.kt (onCreate)

//Add Observer to review data
viewModel.appData.observe(this, Observer<AppData> { appData ->
    handleReviewsUpdate(appData)
})

//Add Observer to the description expand/collapse button
viewModel.isDescriptionExpanded.observe(this, Observer<Boolean> {
    if (true == it)
        buttonExpand.text = getString(R.string.button_collapse)
    else
        buttonExpand.text = getString(R.string.button_expand)
     textProductDescription.text = getDescriptionText(viewModel.appData.value)
})

Try it out! Now what happens on resize and rotation?

Want to learn more? Check out the Android Lifecycles Codelab for a more detailed exploration of Architecture Components.

If you are new to ViewModel and Architecture Components, you are likely wondering about onSaveInstanceState. Doesn't that handle UI state across configuration changes? Does ViewModel replace it? Do I have to use both? What about saving user data?

Summary

State data in this app

All three can and should be retained in our ViewModel, as we did in the step above.

The reviews are too large complex to be efficiently stored in onSaveInstanceState. Store only the name and expanded/collapsed state here. If the application process is destroyed, they will need to be fetched again using the product name.

onSaveInstanceState uses Bundles to save and restore information. Create constant keys for the data you want to store to facilitate storage and retrieval:

MainActivity.kt

companion object {
    private const val KEY_PRODUCT_NAME = "KEY_PRODUCT_NAME"
    private const val KEY_EXPANDED = "KEY_EXPANDED"
}

Now set up restoration (onCreate) and saving (OnSaveInstanceState):

MainActivity.kt (onCreate)

//Restore savedInstanceState variables
savedInstanceState?.getString(KEY_PRODUCT_NAME)?.let { viewModel.setProductName(it) }
savedInstanceState?.getBoolean(KEY_EXPANDED)?.let { viewModel.setDescriptionExpanded(it) }

MainActivity.kt (onSaveInstanceState)

override fun onSaveInstanceState(outState: Bundle?) {
    super.onSaveInstanceState(outState)
    outState?.putBoolean(KEY_EXPANDED, viewModel.isDescriptionExpanded.value == true)
    outState?.putString(KEY_PRODUCT_NAME, viewModel.productName.value)
}

Want to know more about onSaveInstanceState and ViewModel? This blog post is a great resource.

The app is maintaining state nicely now. This is excellent for speed and data usage. Let's take a look at the layout. On a phone in portrait mode, it looks fine, but what happens in landscape or when it expands to fill a Chromebook screen?

This is unacceptable in a production application, let's do better and aim to support different windows sizes. Create four different layouts that correspond to:

Copy the code from activity_main.xml into the three new layouts to provide you with a good base to work from.

If this is new to you, have a look at supporting multiple screen sizes documentation. And keep the following in mind:

Need some ideas? Here is an implementation of the wide-screen landscape orientation:

layout-w800dp-land/activity_main.xml

<android.support.v4.widget.NestedScrollView 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/scrollMain"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.MainActivity">

    <android.support.constraint.ConstraintLayout
        android:id="@+id/constraintMain"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:descendantFocusability="blocksDescendants">

        <android.support.constraint.Guideline
            android:id="@+id/guidelineRight"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.4" />

        <android.support.constraint.Guideline
            android:id="@+id/guidelineImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.2" />

        <android.support.constraint.Guideline
            android:id="@+id/guidelineSplit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.45" />

        <android.support.constraint.Guideline
            android:id="@+id/guidelineSuggested"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.1" />

        <ImageView
            android:id="@+id/imageProduct"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:adjustViewBounds="true"
            android:contentDescription="@string/access_product_image"
            android:maxHeight="300dp"
            android:maxWidth="300dp"
            android:scaleType="fitCenter"
            app:layout_constraintEnd_toStartOf="@id/guidelineImage"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/whizzbuckling" />

        <TextView
            android:id="@+id/textProductName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:text="@string/label_product_name"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
            app:layout_constraintEnd_toStartOf="@+id/progressLoadingReviews"
            app:layout_constraintStart_toEndOf="@+id/imageProduct"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="@string/label_product_name" />

        <TextView
            android:id="@+id/textProductCompany"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="16dp"
            android:text="@string/label_product_company"
            android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
            app:layout_constraintEnd_toStartOf="@+id/progressLoadingReviews"
            app:layout_constraintStart_toEndOf="@+id/imageProduct"
            app:layout_constraintTop_toBottomOf="@+id/textProductName"
            tools:text="@string/label_product_company" />

        <Button
            android:id="@+id/buttonPurchase"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:layout_marginTop="8dp"
            android:paddingEnd="32dp"
            android:paddingStart="32dp"
            android:text="@string/button_purchase"
            app:layout_constraintEnd_toStartOf="@id/guidelineSplit"
            app:layout_constraintStart_toStartOf="@+id/textProductName"
            app:layout_constraintTop_toBottomOf="@+id/textProductCompany" />

        <ProgressBar
            android:id="@+id/progressLoadingReviews"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            app:layout_constraintEnd_toStartOf="@id/guidelineSplit"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/textProductDescription"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            app:layout_constraintEnd_toStartOf="@id/guidelineSplit"
            app:layout_constraintStart_toStartOf="@+id/textProductName"
            app:layout_constraintTop_toBottomOf="@id/buttonPurchase"
            tools:text="Product Description" />

        <Button
            android:id="@+id/buttonExpand"
            style="@style/Widget.AppCompat.ActionButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:padding="16dp"
            android:text="@string/button_expand"
            android:textAllCaps="true"
            android:textColor="@color/colorAccent"
            app:layout_constraintEnd_toEndOf="@+id/textProductDescription"
            app:layout_constraintTop_toBottomOf="@+id/textProductDescription" />

        <android.support.constraint.Barrier
            android:id="@+id/barrierHorizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="bottom"
            app:constraint_referenced_ids="buttonExpand,imageProduct"
            tools:layout_editor_absoluteY="16dp" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerReviews"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="8dp"
            app:layoutManager="android.support.v7.widget.GridLayoutManager"
            app:spanCount="2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="@id/guidelineSplit"
            app:layout_constraintTop_toTopOf="parent"
            tools:listitem="@layout/list_item_review" />

        <TextView
            android:id="@+id/textSuggested"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="8dp"
            android:text="@string/label_suggested"
            android:textAlignment="viewStart"
            android:textAppearance="@style/TextAppearance.AppCompat.Title"
            app:layout_constraintEnd_toStartOf="@+id/guidelineSplit"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/barrierHorizontal"
            />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerSuggested"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:orientation="vertical"
            app:layoutManager="android.support.v7.widget.GridLayoutManager"
            app:layout_constraintEnd_toStartOf="@+id/guidelineSplit"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textSuggested"
            app:spanCount="3"
            tools:listitem="@layout/list_item_suggested" />
    </android.support.constraint.ConstraintLayout>
</android.support.v4.widget.NestedScrollView>

Test your layouts by free-form resizing windows to be large, small, portrait, and landscape. Flip the Chromebook to tablet mode and try both portrait and landscape orientations.

Your app can handle multiple screen and window sizes and orientation and your state data is seamlessly surviving configuration changes. How can we make this even better?

An up-and-coming ConstraintLayout library allows you to easily add snazzy animated transitions between multiple layout files. We have included a preview version of this library in the libs directory of the codelab source, feel free to test it out. Keep in mind more features and documentation are coming soon.

In your app's gradle file, comment out the regular constraint-layout library, and enable the preview library.

build.gradle (Module: app)

//implementation 'com.android.support.constraint:constraint-layout:1.1.0'
implementation files('libs/constraint-debug.aar')

Layout manager

To achieve the animation magic, we need to rethink our layout files a bit. As we are going to take over some of the layout logic from the framework, we are not going to rely on resource filename qualifiers.

Move all of your layout files into the main layout directory and rename them in an identifiable way:

Let's animate! First, we are going to move the NestedScrollView out of our layout files into a master layout shell. This is because the constraint animation library requires the parent view that is swapped during a change to be a ConstraintLayout.

Currently this works with layout files and not resource ids. Using include in the shell allows us to keep the scrolling functionality. Create a new layout called activity_main_shell.xml:

activity_main_shell.xml

<android.support.v4.widget.NestedScrollView 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/scrollMain"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="4dp">

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

</android.support.v4.widget.NestedScrollView>

Change the setContentView line to point to the shell file.

MainActivity.kt (onCreate)

setContentView(R.layout.activity_main_shell)

Before we look at the individual layouts, let's set up the overall management logic. Create a res/xml folder and add an XML resource file called constraint_states.xml that will determine when to show each of the layout files:

constraint_states.xml

<?xml version="1.0" encoding="utf-8"?>
<ConstraintLayoutStates xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <State
        android:id="@+id/constraintStatePortrait"
        app:constraints="@layout/activity_main">

        <Constraints
            app:constraints="@layout/activity_main"
            app:region_widthLessThan="395dp" />

        <Constraints
            app:constraints="@layout/activity_main_w400"
            app:region_widthMoreThan="400dp"
            />
    </State>

    <State
        android:id="@+id/constraintStateLandscape"
        app:constraints="@layout/activity_main_land">

        <Constraints
            app:constraints="@layout/activity_main_land"
            app:region_widthLessThan="595dp" />

        <Constraints
            app:constraints="@layout/activity_main_w600_land"
            app:region_widthMoreThan="600dp"
            />
    </State>
</ConstraintLayoutStates>

This file allows us to define an arbitrary number of states. Each state has a series of boundary cases based on height and width that determines which layout file will be loaded. In this case, we mirror our original layout configuration to have two states, layout and portrait, with two different cases for each state, based on a width boundary.

Things to Note:

Layout Files

Now take a look at all four of your layout files. For each, remove the NestedScrollView and be sure to include the xmlns, app, and tools attributes in the opening ConstraintLayout tag.

To do this, copy and paste the following code into the top of each file, replacing the NestedScrollView and ConstraintLayout tag, then remove the NestedScrollView closing tag at the end of the file.

activity_main.xml

<android.support.constraint.ConstraintLayout
    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/constraintMain"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:descendantFocusability="blocksDescendants">

Requesting Configuration Changes

Now that we have our layout files set up, we need to attach them to configuration changes as we are no longer using the built-in mechanism.

First request to be notified of configuration changes in the manifest:

AndroidManifest.xml (activity tag)

android:configChanges=
    "screenSize|smallestScreenSize|orientation|screenLayout"

Next, override onConfigurationChange to change our layout state when the app is resized:

MainActivity.kt

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    configurationUpdate(newConfig)
}

private fun configurationUpdate(configuration: Configuration) {
    if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
        constraintMain.setState(R.id.constraintStateLandscape, configuration.screenWidthDp, configuration.screenHeightDp)
    else
        constraintMain.setState(R.id.constraintStatePortrait, configuration.screenWidthDp, configuration.screenHeightDp)
}

So, whenever the screen-size changes, we set our state based on if the device/window is landscape or portrait, and pass in the current width and height. These values will be compared to the constraint states we configured in constraint_states.xml.

Lastly, because we will be manually handling layout changes, on first run we need to make sure that we are loading the correct layout file. Add the following to the end of onCreate:

MainActivity.kt (onCreate)

//On first load, make sure we are showing the correct layout
configurationUpdate(resources.configuration)

ConstraintsChangedListener

Now for the final step - adding a listener for when the constraint state changes and configuring the animation between states. Attach the constraint_states.xml file to the main ConstraintLayout and then attach a ConstraintsChangedListener:

MainActivity.kt (onCreate)

//Set up constraint layout animations
constraintMain.setLayoutDescription(R.xml.constraint_states)
constraintMain.setOnConstraintsChanged(object : ConstraintsChangedListener() {

    override fun preLayoutChange(state: Int, layoutId: Int) {
        progressLoadingReviews.visibility = if (viewModel.appData.value == null) VISIBLE else INVISIBLE

        val changeBounds = ChangeBounds()
        changeBounds.duration = 600
        changeBounds.interpolator = AnticipateOvershootInterpolator(0.2f)

        TransitionManager.beginDelayedTransition(constraintMain, changeBounds)

        when (layoutId) {
            R.layout.activity_main -> {
                val reviewLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
                recyclerReviews.layoutManager = reviewLayoutManager

                val suggestionLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.HORIZONTAL, false)
                recyclerSuggested.layoutManager = suggestionLayoutManager
            }

            R.layout.activity_main_land -> {
                val reviewLayoutManager = GridLayoutManager(baseContext, 2)
                recyclerReviews.layoutManager = reviewLayoutManager

                val suggestionLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.HORIZONTAL, false)
                recyclerSuggested.layoutManager = suggestionLayoutManager
            }

            R.layout.activity_main_w400 -> {
                val reviewLayoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
                recyclerReviews.layoutManager = reviewLayoutManager

                val suggestionLayoutManager = GridLayoutManager(baseContext, 2)
                recyclerSuggested.layoutManager = suggestionLayoutManager
            }

            R.layout.activity_main_w600_land -> {
                val reviewLayoutManager = GridLayoutManager(baseContext, 2)
                recyclerReviews.layoutManager = reviewLayoutManager

                val suggestionLayoutManager = GridLayoutManager(baseContext, 3)
                recyclerSuggested.layoutManager = suggestionLayoutManager
            }
        }
    }

    override fun postLayoutChange(stateId: Int, layoutId: Int) {
        //Request all layout elements be redrawn
        constraintMain.requestLayout()
    }
})

Try it out before we discuss what is happening. You should see animations between layout changes similar to the gif above. How does it look?

Let's talk about the animations first. We are using TransistionManager to animate between layouts. This allows you to easily configure and use a number of prebuilt Transitions like Fade, Slide, and Explode. You can even chain or combine transition create a variety of effects.

Here, we use the ChangeBounds effect which provides a nice transition when views are moved and resized. Try out a few different durations (eg. 100, 1000, 2000, and 5000). Next, try out some different tension values for the AnticipateOvershootInterpolator. Try 0f, 1f, 1.5f, and 2f - make sure the duration is long enough so you can get a feel for how it works. Find a combo that feels right to you.

Swapping LayoutManagers

Take a moment to look at the when statement above. The different layouts in our sample implementation have different layout managers for the RecyclerViews. Because constraint state changes only affect constraint sets, we need to manually replace the layout managers during a state change. In preLayoutChange we check the layoutId to determine which layout file is being shown, and update the LayoutManger accordingly. If your view does not have differently configured LayoutManagers for your Views, you can remove this when section.

Lastly, in postLayoutChange we request that all layout elements are remeasured and redrawn with constraintMain.requestLayout(). This may not be required in future versions of the library.

As much as possible, apps should implement ConstraintLayout and aim for fluid layouts. Developers should almost never be retrieving window coordinates and making layout calculations based on them.

In the Chrome OS environment with multiple applications open and the potential for multiple monitors, if you have a valid use case for manually calculating layout coordinates it is crucial that window coordinates are used and not screen coordinates.

To illustrate this, let's add a PopupWindow to the Purchase button that is 50% of the screen width and height and shows it in the middle of the screen.

MainActivity.kt

buttonPurchase.setOnClickListener(View.OnClickListener {
    val textPopupMessage = TextView(this)
    textPopupMessage.gravity = Gravity.CENTER
    textPopupMessage.text = getString(R.string.popup_purchase)
    TextViewCompat.setTextAppearance(textPopupMessage, R.style.TextAppearance_AppCompat_Title)

    val layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)
    val framePopup = FrameLayout(this)
    framePopup.layoutParams = layoutParams
    framePopup.setBackgroundColor(getColor(R.color.background_floating_material_light))

    framePopup.addView(textPopupMessage)

    //Get window size
    val displayMetrics = Resources.getSystem().displayMetrics
    val screenWidthPx = displayMetrics.widthPixels
    val screenHeightPx = displayMetrics.heightPixels

    //Popup should be 50% of window size
    val popupWidthPx = screenWidthPx / 2
    val popupHeightPx = screenHeightPx / 2

    //Place it in the middle of the window
    val popupX = (screenWidthPx / 2) - (popupWidthPx / 2)
    val popupY = (screenHeightPx / 2) - (popupHeightPx / 2)

    //Show the window
    val popupWindow = PopupWindow(framePopup, popupWidthPx, popupHeightPx, true)
    popupWindow.elevation = 10f
    popupWindow.showAtLocation(scrollMain, Gravity.NO_GRAVITY, popupX, popupY)
})

When the app is full-screen, which many mobile-minded developers assume will always be the case, things look ok.

But what happens if the application is not fullscreen?

Not so good. If your UI depends heavily on screen coordinates, it quickly becomes unusable in a multi-window environment. If the user has multiple monitors, hard-coding coordinates gets even more complicated.

In this case, the line causing the issue is where we get the screen metrics:

val displayMetrics = Resources.getSystem().displayMetrics

The correct way to do this is to get the window metrics:

val displayMetrics = resources.displayMetrics

Much better.

You did it! Great work! You have now implemented some best practices for allowing Android apps to resize well on Chrome OS and other multi-window, multi-screen environments.

Sample Source Code

Clone the repository from GitHub

git clone https://github.com/googlecodelabs/resize-codelab

...or download the repository as a Zip file

Download Zip