diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e88cfeb9..b2e76a7040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Breaking: New: - `UIConfiguration.viewInsets` tracks the safe area of the specific `RedwoodView` being targeted. This is currently implemented for views on Android and UIViews on iOS. +- `RedwoodLayout.additionalInsets` configures additional insets for application controls like tab bars and floating action buttons. - `ConsumeInsets {}` composable consumes insets. Most applications should call this in their root composable function. Changed: diff --git a/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt b/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt index e0c8cd32e9..35c7acba17 100644 --- a/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt +++ b/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt @@ -121,6 +121,42 @@ class TreehouseLayoutTest { } } + @Test fun viewInsetsSumsSystemBarsAndAdditionalInsets() = runTest { + val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher) + layout.uiConfiguration.test { + assertThat(awaitItem().viewInsets).isEqualTo(Margin.Zero) + + layout.additionalInsets = Insets.of(5, 6, 7, 8) + assertThat(awaitItem().viewInsets).isEqualTo( + with(Density(activity.resources)) { + Margin( + start = 5.toDp(), + top = 6.toDp(), + end = 7.toDp(), + bottom = 8.toDp(), + ) + }, + ) + + ViewCompat.dispatchApplyWindowInsets( + layout, + WindowInsetsCompat.Builder() + .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.of(10, 20, 30, 40)) + .build(), + ) + assertThat(awaitItem().viewInsets).isEqualTo( + with(Density(activity.resources)) { + Margin( + start = 15.toDp(), + top = 26.toDp(), + end = 37.toDp(), + bottom = 48.toDp(), + ) + }, + ) + } + } + @Test fun uiConfigurationEmitsLayoutDirectionChanges() = runTest { val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher) layout.uiConfiguration.test { diff --git a/redwood-widget/api/android/redwood-widget.api b/redwood-widget/api/android/redwood-widget.api index 94cd94c664..6f66f3f433 100644 --- a/redwood-widget/api/android/redwood-widget.api +++ b/redwood-widget/api/android/redwood-widget.api @@ -50,6 +50,7 @@ public final class app/cash/redwood/widget/MutableListChildren : app/cash/redwoo public class app/cash/redwood/widget/RedwoodLayout : android/view/ViewGroup, app/cash/redwood/widget/RedwoodView { public fun (Landroid/content/Context;Landroidx/activity/OnBackPressedDispatcher;)V + public final fun getAdditionalInsets ()Landroidx/core/graphics/Insets; public final fun getChildren ()Lapp/cash/redwood/widget/Widget$Children; public fun getOnBackPressedDispatcher ()Lapp/cash/redwood/ui/OnBackPressedDispatcher; public fun getSavedStateRegistry ()Lapp/cash/redwood/widget/SavedStateRegistry; @@ -59,6 +60,7 @@ public class app/cash/redwood/widget/RedwoodLayout : android/view/ViewGroup, app protected fun onConfigurationChanged (Landroid/content/res/Configuration;)V protected fun onLayout (ZIIII)V protected fun onMeasure (II)V + public final fun setAdditionalInsets (Landroidx/core/graphics/Insets;)V } public abstract interface class app/cash/redwood/widget/RedwoodView { diff --git a/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt b/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt index a40157dd06..65966e6de7 100644 --- a/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt +++ b/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt @@ -22,6 +22,7 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback as AndroidOnBackPressedCallback import androidx.activity.OnBackPressedDispatcher as AndroidOnBackPressedDispatcher +import androidx.core.graphics.Insets import androidx.core.view.children as viewGroupChildren import androidx.savedstate.findViewTreeSavedStateRegistryOwner import app.cash.redwood.ui.Cancellable @@ -46,15 +47,27 @@ public open class RedwoodLayout( final override val value: View get() = this - init { - // The view needs to have an ID to participate in instance state saving. - id = R.id.redwood_layout - } + private var windowInsets: Insets = Insets.NONE + set(value) { + if (field == value) return + field = value + mutableUiConfiguration.value = computeUiConfiguration() + } + + /** + * Additional insets that are summed with this view's window insets to produce + * [UiConfiguration.viewInsets]. Use this when an application-layer control like a floating action + * button or toolbar requires content to be inset. + */ + public var additionalInsets: Insets = Insets.NONE + set(value) { + if (field == value) return + field = value + mutableUiConfiguration.value = computeUiConfiguration() + } private val mutableUiConfiguration = MutableStateFlow( - computeUiConfiguration( - viewInsets = Margin.Zero, - ), + computeUiConfiguration(), ) override val onBackPressedDispatcher: RedwoodOnBackPressedDispatcher = @@ -85,10 +98,11 @@ public open class RedwoodLayout( get() = mutableUiConfiguration init { + // The view needs to have an ID to participate in instance state saving. + id = R.id.redwood_layout + setOnWindowInsetsChangeListener { insets -> - mutableUiConfiguration.value = computeUiConfiguration( - viewInsets = insets.safeDrawing.toMargin(Density(resources)), - ) + windowInsets = insets.safeDrawing } } @@ -118,13 +132,19 @@ public open class RedwoodLayout( private fun computeUiConfiguration( config: Configuration = context.resources.configuration, - viewInsets: Margin = uiConfiguration.value.viewInsets, ): UiConfiguration { val viewportSize: Size val density: Double + val viewInsets: Margin with(Density(resources)) { density = rawDensity viewportSize = Size(width.toDp(), height.toDp()) + viewInsets = Margin( + start = (windowInsets.left + additionalInsets.left).toDp(), + end = (windowInsets.right + additionalInsets.right).toDp(), + top = (windowInsets.top + additionalInsets.top).toDp(), + bottom = (windowInsets.bottom + additionalInsets.bottom).toDp(), + ) } return UiConfiguration( darkMode = (config.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES,