From 1719038a00f136795bc3fc433cf6382022c71756 Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:44:36 -0600 Subject: [PATCH] fix(ApiDemos): modernize window insets dispatch in SamplesBaseActivity and fix SaveStateDemo - Safely apply system window insets in SamplesBaseActivity (Kotlin & Java) to root containers without double-padding child views - Add save_state_demo.xml layout with constraint bindings and wire up SaveStateDemoActivity - Clean up null safety and listener callbacks in OnMapAndViewReadyListener --- .../example/mapdemo/SamplesBaseActivity.java | 67 ++++++++++++-- .../kotlindemos/OnMapAndViewReadyListener.kt | 30 ++++--- .../kotlindemos/SamplesBaseActivity.kt | 88 +++++++++++++++---- .../kotlindemos/SaveStateDemoActivity.kt | 16 ++-- .../src/main/res/layout/save_state_demo.xml | 51 +++++++++++ .../kotlindemos/OnMapAndViewReadyListener.kt | 29 +++--- 6 files changed, 227 insertions(+), 54 deletions(-) create mode 100644 ApiDemos/project/kotlin-app/src/main/res/layout/save_state_demo.xml diff --git a/ApiDemos/project/java-app/src/main/java/com/example/mapdemo/SamplesBaseActivity.java b/ApiDemos/project/java-app/src/main/java/com/example/mapdemo/SamplesBaseActivity.java index 6a1518dcb..fe9969090 100644 --- a/ApiDemos/project/java-app/src/main/java/com/example/mapdemo/SamplesBaseActivity.java +++ b/ApiDemos/project/java-app/src/main/java/com/example/mapdemo/SamplesBaseActivity.java @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import android.os.Bundle; import android.view.View; +import android.view.ViewGroup; + import androidx.activity.EdgeToEdge; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -33,18 +35,67 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { EdgeToEdge.enable(this); } + @Override + public void setContentView(int layoutResID) { + super.setContentView(layoutResID); + setupEdgeToEdgeInsets(); + } + + @Override + public void setContentView(View view) { + super.setContentView(view); + setupEdgeToEdgeInsets(); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + super.setContentView(view, params); + setupEdgeToEdgeInsets(); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + super.addContentView(view, params); + setupEdgeToEdgeInsets(); + } + + private void setupEdgeToEdgeInsets() { + View root = findViewById(android.R.id.content); + if (root == null) return; + View topBar = root.findViewById(com.example.common_ui.R.id.top_bar); + if (topBar != null) { + android.util.TypedValue typedValue = new android.util.TypedValue(); + int baseHeight; + if (getTheme().resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) { + baseHeight = android.util.TypedValue.complexToDimensionPixelSize(typedValue.data, getResources().getDisplayMetrics()); + } else { + baseHeight = (int) (56 * getResources().getDisplayMetrics().density); + } + ViewCompat.setOnApplyWindowInsetsListener(topBar, (view, insets) -> { + Insets statusBar = insets.getInsets(WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.displayCutout()); + view.setPadding(statusBar.left, statusBar.top, statusBar.right, 0); + view.getLayoutParams().height = baseHeight + statusBar.top; + view.requestLayout(); + return insets; + }); + } + + View mapContainer = root.findViewById(com.example.common_ui.R.id.map_container); + View bottomTarget = mapContainer != null ? mapContainer : root; + ViewCompat.setOnApplyWindowInsetsListener(bottomTarget, (view, insets) -> { + Insets navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars() | WindowInsetsCompat.Type.displayCutout()); + int topInsets = (topBar == null) ? insets.getInsets(WindowInsetsCompat.Type.statusBars()).top : 0; + view.setPadding(navBars.left, topInsets, navBars.right, navBars.bottom); + return insets; + }); + } + /** * Applies insets to the container view to properly handle window insets. * * @param container the container view to apply insets to */ protected static void applyInsets(View container) { - ViewCompat.setOnApplyWindowInsetsListener(container, - (view, insets) -> { - Insets innerPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); - view.setPadding(innerPadding.left, innerPadding.top, innerPadding.right, innerPadding.bottom); - return insets; - } - ); + // Handled automatically in SamplesBaseActivity } } \ No newline at end of file diff --git a/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt b/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt index 58f018c46..ad6ec6b43 100644 --- a/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt +++ b/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ class OnMapAndViewReadyListener( ) : OnGlobalLayoutListener, OnMapReadyCallback { - private val mapView: View? = mapFragment.view + private var mapView: View? = null private var isViewReady = false private var isMapReady = false @@ -52,16 +52,6 @@ class OnMapAndViewReadyListener( } private fun registerListeners() { - // View layout. - mapView?.let { - if (it.width != 0 && it.height != 0) { - // View has already completed layout. - isViewReady = true - } else { - // Map has not undergone layout, register a View observer. - it.viewTreeObserver.addOnGlobalLayoutListener(this) - } - } // GoogleMap. Note if the GoogleMap is already ready it will still fire the callback later. mapFragment.getMapAsync(this) } @@ -70,6 +60,22 @@ class OnMapAndViewReadyListener( // NOTE: The GoogleMap API specifies the listener is removed just prior to invocation. map = googleMap isMapReady = true + + // View layout. + mapView = mapFragment.view + val view = mapView + if (view != null) { + if (view.width != 0 && view.height != 0) { + // View has already completed layout. + isViewReady = true + } else { + // Map has not undergone layout, register a View observer. + view.viewTreeObserver.addOnGlobalLayoutListener(this) + } + } else { + isViewReady = true + } + fireCallbackIfReady() } diff --git a/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SamplesBaseActivity.kt b/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SamplesBaseActivity.kt index a6982e167..e9d9d2ecf 100644 --- a/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SamplesBaseActivity.kt +++ b/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SamplesBaseActivity.kt @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,9 +15,9 @@ package com.example.kotlindemos import android.os.Bundle import android.view.View +import android.view.ViewGroup import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -27,27 +27,81 @@ open class SamplesBaseActivity : AppCompatActivity() { enableEdgeToEdge() } + override fun setContentView(layoutResID: Int) { + super.setContentView(layoutResID) + setupEdgeToEdgeInsets() + } + + override fun setContentView(view: View?) { + super.setContentView(view) + setupEdgeToEdgeInsets() + } + + override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) { + super.setContentView(view, params) + setupEdgeToEdgeInsets() + } + + override fun addContentView(view: View?, params: ViewGroup.LayoutParams?) { + super.addContentView(view, params) + setupEdgeToEdgeInsets() + } + + private fun setupEdgeToEdgeInsets() { + val root = findViewById(android.R.id.content) ?: return + val topBar = root.findViewById(com.example.common_ui.R.id.top_bar) + if (topBar != null) { + val typedValue = android.util.TypedValue() + val baseHeight = if (theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) { + android.util.TypedValue.complexToDimensionPixelSize(typedValue.data, resources.displayMetrics) + } else { + (56 * resources.displayMetrics.density).toInt() + } + ViewCompat.setOnApplyWindowInsetsListener(topBar) { view, insets -> + val statusBar = insets.getInsets( + WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout() + ) + view.setPadding( + statusBar.left, + statusBar.top, + statusBar.right, + 0 + ) + view.layoutParams.height = baseHeight + statusBar.top + view.requestLayout() + insets + } + } + + val mapContainer = root.findViewById(com.example.common_ui.R.id.map_container) + val bottomTarget = mapContainer ?: root + ViewCompat.setOnApplyWindowInsetsListener(bottomTarget) { view, insets -> + val navBars = insets.getInsets( + WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.displayCutout() + ) + val topInsets = if (topBar == null) { + insets.getInsets(WindowInsetsCompat.Type.statusBars()).top + } else { + 0 + } + view.setPadding( + navBars.left, + topInsets, + navBars.right, + navBars.bottom + ) + insets + } + } + companion object { /** * Applies insets to the container view to properly handle window insets. * * @param container the container view to apply insets to */ - fun applyInsets(container: View) { - ViewCompat.setOnApplyWindowInsetsListener( - container, - OnApplyWindowInsetsListener { view: View?, insets: WindowInsetsCompat? -> - val innerPadding = - insets!!.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) - view!!.setPadding( - innerPadding.left, - innerPadding.top, - innerPadding.right, - innerPadding.bottom - ) - insets - } - ) + fun applyInsets(container: View? = null) { + // Handled automatically in SamplesBaseActivity } } } \ No newline at end of file diff --git a/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SaveStateDemoActivity.kt b/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SaveStateDemoActivity.kt index 07cbd9110..a9b116003 100755 --- a/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SaveStateDemoActivity.kt +++ b/ApiDemos/project/kotlin-app/src/main/java/com/example/kotlindemos/SaveStateDemoActivity.kt @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.example.kotlindemos import android.os.Bundle import android.os.Parcelable +import androidx.core.os.BundleCompat import androidx.lifecycle.lifecycleScope import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener @@ -26,7 +27,8 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker import com.google.maps.android.ktx.addMarker import com.google.maps.android.ktx.awaitMap -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize +import kotlinx.coroutines.launch import java.util.Random /** @@ -73,13 +75,15 @@ class SaveStateDemoActivity : SamplesBaseActivity() { // the savedInsanceState Bundle. // - Custom Parcelable objects were wrapped in another Bundle. mMarkerPosition = - savedInstanceState?.getParcelable(MARKER_POSITION) ?: DEFAULT_MARKER_POSITION + savedInstanceState?.let { BundleCompat.getParcelable(it, MARKER_POSITION, LatLng::class.java) } + ?: DEFAULT_MARKER_POSITION mMarkerInfo = - savedInstanceState?.getBundle(OTHER_OPTIONS)?.getParcelable(MARKER_INFO) ?: MarkerInfo( - BitmapDescriptorFactory.HUE_RED) + savedInstanceState?.getBundle(OTHER_OPTIONS)?.let { + BundleCompat.getParcelable(it, MARKER_INFO, MarkerInfo::class.java) + } ?: MarkerInfo(BitmapDescriptorFactory.HUE_RED) mMoveCameraToMarker = savedInstanceState == null - lifecycleScope.launchWhenCreated { + lifecycleScope.launch { val map = awaitMap() map.addMarker { icon(BitmapDescriptorFactory.defaultMarker(mMarkerInfo.hue)) diff --git a/ApiDemos/project/kotlin-app/src/main/res/layout/save_state_demo.xml b/ApiDemos/project/kotlin-app/src/main/res/layout/save_state_demo.xml new file mode 100644 index 000000000..4291e256d --- /dev/null +++ b/ApiDemos/project/kotlin-app/src/main/res/layout/save_state_demo.xml @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/ApiDemos/project/kotlin-app/src/v3/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt b/ApiDemos/project/kotlin-app/src/v3/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt index decf452c9..d5cd25ece 100644 --- a/ApiDemos/project/kotlin-app/src/v3/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt +++ b/ApiDemos/project/kotlin-app/src/v3/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt @@ -6,7 +6,7 @@ * corresponding file under the `app/src/gms` directory. */ /* - * Copyright 2018 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class OnMapAndViewReadyListener( ) : OnGlobalLayoutListener, OnMapReadyCallback { - private val mapView: View? = mapFragment.view + private var mapView: View? = null private var isViewReady = false private var isMapReady = false @@ -59,15 +59,6 @@ class OnMapAndViewReadyListener( } private fun registerListeners() { - // View layout. - if (mapView?.width != 0 && mapView?.height != 0) { - // View has already completed layout. - isViewReady = true - } else { - // Map has not undergone layout, register a View observer. - mapView.viewTreeObserver.addOnGlobalLayoutListener(this) - } - // GoogleMap. Note if the GoogleMap is already ready it will still fire the callback later. mapFragment.getMapAsync(this) } @@ -76,6 +67,22 @@ class OnMapAndViewReadyListener( // NOTE: The GoogleMap API specifies the listener is removed just prior to invocation. map = googleMap ?: return isMapReady = true + + // View layout. + mapView = mapFragment.view + val view = mapView + if (view != null) { + if (view.width != 0 && view.height != 0) { + // View has already completed layout. + isViewReady = true + } else { + // Map has not undergone layout, register a View observer. + view.viewTreeObserver.addOnGlobalLayoutListener(this) + } + } else { + isViewReady = true + } + fireCallbackIfReady() }