aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-05-31 21:58:19 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2023-05-31 21:58:19 +0000
commit085ee5253d8371b267a2c42c2b91d92ad5f57fff (patch)
tree7eb69c03a0132f678589e86b804d7a9e4b478677
parent27c69a0a8f6f433a0e969ff934e622adcfd5a947 (diff)
parented62a1363cd750498d1126498ad847c7024c523c (diff)
downloadsupport-snap-temp-L01400000961003868.tar.gz
Merge "[M3][ModalBottomSheet] Update internal popup logic to use custom abstract view" into snap-temp-L01400000961003868snap-temp-L01400000961003868
-rw-r--r--compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt10
-rw-r--r--compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt273
-rw-r--r--compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt3
-rw-r--r--compose/material3/material3/src/androidMain/res/values/strings.xml4
-rw-r--r--compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt1
-rw-r--r--compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Strings.desktop.kt1
6 files changed, 235 insertions, 57 deletions
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
index 72643ece42b..cbbc054736f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalBottomSheetTest.kt
@@ -213,8 +213,11 @@ class ModalBottomSheetTest {
@Test
fun modalBottomSheet_defaultStateForSmallContentIsFullExpanded() {
lateinit var sheetState: SheetState
+ var height by mutableStateOf(0.dp)
rule.setContent {
+ val config = LocalContext.current.resources.configuration
+ height = config.screenHeightDp.dp
sheetState = rememberModalBottomSheetState()
ModalBottomSheet(onDismissRequest = {}, sheetState = sheetState, dragHandle = null) {
Box(
@@ -226,7 +229,6 @@ class ModalBottomSheetTest {
}
}
- val height = rule.onNode(isPopup()).getUnclippedBoundsInRoot().height
assertThat(sheetState.currentValue).isEqualTo(SheetValue.Expanded)
rule.onNodeWithTag(sheetTag).assertTopPositionInRootIsEqualTo(height - sheetHeight)
}
@@ -356,15 +358,15 @@ class ModalBottomSheetTest {
)
}
}
- assertThat(state.requireOffset()).isWithin(0.5f).of(expectedExpandedAnchor)
+ assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
size = 100.dp
rule.waitForIdle()
- assertThat(state.requireOffset()).isWithin(0.5f).of(expectedExpandedAnchor)
+ assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
size = 30.dp
rule.waitForIdle()
- assertThat(state.requireOffset()).isWithin(0.5f).of(expectedExpandedAnchor)
+ assertThat(state.requireOffset()).isWithin(1f).of(expectedExpandedAnchor)
}
@Test
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
index 33e9f0cdec1..9ba0d942241 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ModalBottomSheet.android.kt
@@ -16,6 +16,13 @@
package androidx.compose.material3
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.WindowManager
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
@@ -23,21 +30,27 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.SheetValue.Hidden
+import androidx.compose.material3.SheetValue.PartiallyExpanded
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionContext
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -45,17 +58,30 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.ViewRootForInspector
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.popup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.window.Popup
-import androidx.compose.ui.window.PopupProperties
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import java.util.UUID
import kotlin.math.max
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -102,6 +128,9 @@ fun ModalBottomSheet(
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
content: @Composable ColumnScope.() -> Unit,
) {
+ val configuration = LocalConfiguration.current
+ val density = LocalDensity.current
+ val fullHeight = with(density) { configuration.screenHeightDp.dp.toPx() }
val scope = rememberCoroutineScope()
val animateToDismiss: () -> Unit = {
if (sheetState.swipeableState.confirmValueChange(Hidden)) {
@@ -128,7 +157,6 @@ fun ModalBottomSheet(
snapTo = { target -> scope.launch { sheetState.snapTo(target) } }
)
}
- val systemBarHeight = WindowInsets.systemBarsForVisualComponents.getBottom(LocalDensity.current)
ModalBottomSheetPopup(
onDismissRequest = {
@@ -139,18 +167,19 @@ fun ModalBottomSheet(
}
}
) {
- BoxWithConstraints(Modifier.fillMaxSize()) {
- val fullHeight = constraints.maxHeight
+ Box {
Scrim(
color = scrimColor,
onDismissRequest = animateToDismiss,
visible = sheetState.targetValue != Hidden
)
+ val bottomSheetPaneTitle = getString(string = Strings.BottomSheetPaneTitle)
Surface(
modifier = modifier
.widthIn(max = BottomSheetMaxWidth)
.fillMaxWidth()
.align(Alignment.TopCenter)
+ .semantics { paneTitle = bottomSheetPaneTitle }
.offset {
IntOffset(
0,
@@ -171,8 +200,7 @@ fun ModalBottomSheet(
.modalBottomSheetSwipeable(
sheetState = sheetState,
anchorChangeHandler = anchorChangeHandler,
- screenHeight = fullHeight.toFloat(),
- bottomPadding = systemBarHeight.toFloat(),
+ screenHeight = fullHeight,
onDragStopped = {
settleToDismiss(it)
},
@@ -188,35 +216,38 @@ fun ModalBottomSheet(
getString(Strings.BottomSheetPartialExpandDescription)
val dismissActionLabel = getString(Strings.BottomSheetDismissDescription)
val expandActionLabel = getString(Strings.BottomSheetExpandDescription)
- Box(Modifier
- .align(Alignment.CenterHorizontally)
- .semantics(mergeDescendants = true) {
- // Provides semantics to interact with the bottomsheet based on its
- // current value.
- with(sheetState) {
- dismiss(dismissActionLabel) {
- animateToDismiss()
- true
- }
- if (currentValue == SheetValue.PartiallyExpanded) {
- expand(expandActionLabel) {
- if (swipeableState.confirmValueChange(Expanded)) {
- scope.launch { sheetState.expand() }
- }
+ Box(
+ Modifier
+ .align(Alignment.CenterHorizontally)
+ .semantics(mergeDescendants = true) {
+ // Provides semantics to interact with the bottomsheet based on its
+ // current value.
+ with(sheetState) {
+ dismiss(dismissActionLabel) {
+ animateToDismiss()
true
}
- } else if (hasPartiallyExpandedState) {
- collapse(collapseActionLabel) {
- val confirmPartial = swipeableState
- .confirmValueChange(SheetValue.PartiallyExpanded)
- if (confirmPartial) {
- scope.launch { partialExpand() }
+ if (currentValue == PartiallyExpanded) {
+ expand(expandActionLabel) {
+ if (swipeableState.confirmValueChange(Expanded)) {
+ scope.launch { sheetState.expand() }
+ }
+ true
+ }
+ } else if (hasPartiallyExpandedState) {
+ collapse(collapseActionLabel) {
+ if (
+ swipeableState.confirmValueChange(
+ PartiallyExpanded
+ )
+ ) {
+ scope.launch { partialExpand() }
+ }
+ true
}
- true
}
}
}
- }
) {
dragHandle()
}
@@ -265,7 +296,8 @@ private fun Scrim(
detectTapGestures {
onDismissRequest()
}
- }.clearAndSetSemantics {}
+ }
+ .clearAndSetSemantics {}
} else {
Modifier
}
@@ -284,23 +316,22 @@ private fun Modifier.modalBottomSheetSwipeable(
sheetState: SheetState,
anchorChangeHandler: AnchorChangeHandler<SheetValue>,
screenHeight: Float,
- bottomPadding: Float,
onDragStopped: CoroutineScope.(velocity: Float) -> Unit,
) = draggable(
- state = sheetState.swipeableState.swipeDraggableState,
- orientation = Orientation.Vertical,
- enabled = sheetState.isVisible,
- startDragImmediately = sheetState.swipeableState.isAnimationRunning,
- onDragStopped = onDragStopped
-)
+ state = sheetState.swipeableState.swipeDraggableState,
+ orientation = Orientation.Vertical,
+ enabled = sheetState.isVisible,
+ startDragImmediately = sheetState.swipeableState.isAnimationRunning,
+ onDragStopped = onDragStopped
+ )
.swipeAnchors(
state = sheetState.swipeableState,
anchorChangeHandler = anchorChangeHandler,
- possibleValues = setOf(Hidden, SheetValue.PartiallyExpanded, Expanded),
+ possibleValues = setOf(Hidden, PartiallyExpanded, Expanded),
) { value, sheetSize ->
when (value) {
- Hidden -> screenHeight + bottomPadding
- SheetValue.PartiallyExpanded -> when {
+ Hidden -> screenHeight
+ PartiallyExpanded -> when {
sheetSize.height < screenHeight / 2 -> null
sheetState.skipPartiallyExpanded -> null
else -> screenHeight / 2f
@@ -320,9 +351,9 @@ private fun ModalBottomSheetAnchorChangeHandler(
val previousTargetOffset = previousAnchors[previousTarget]
val newTarget = when (previousTarget) {
Hidden -> Hidden
- SheetValue.PartiallyExpanded, Expanded -> {
- val hasPartiallyExpandedState = newAnchors.containsKey(SheetValue.PartiallyExpanded)
- val newTarget = if (hasPartiallyExpandedState) SheetValue.PartiallyExpanded
+ PartiallyExpanded, Expanded -> {
+ val hasPartiallyExpandedState = newAnchors.containsKey(PartiallyExpanded)
+ val newTarget = if (hasPartiallyExpandedState) PartiallyExpanded
else if (newAnchors.containsKey(Expanded)) Expanded else Hidden
newTarget
}
@@ -343,12 +374,150 @@ private fun ModalBottomSheetAnchorChangeHandler(
* Popup specific for modal bottom sheet.
*/
@Composable
-@ExperimentalMaterial3Api
internal fun ModalBottomSheetPopup(
onDismissRequest: () -> Unit,
content: @Composable () -> Unit
-) = Popup(
- onDismissRequest = onDismissRequest,
- properties = PopupProperties(focusable = true),
- content = content
-) \ No newline at end of file
+) {
+ val view = LocalView.current
+ val id = rememberSaveable { UUID.randomUUID() }
+ val parentComposition = rememberCompositionContext()
+ val currentContent by rememberUpdatedState(content)
+ val modalBottomSheetWindow = remember {
+ ModalBottomSheetWindow(
+ onDismissRequest = onDismissRequest,
+ composeView = view,
+ saveId = id
+ ).apply {
+ setCustomContent(
+ parent = parentComposition,
+ content = {
+ Box(Modifier.semantics { this.popup() }) {
+ currentContent()
+ }
+ }
+ )
+ }
+ }
+
+ DisposableEffect(modalBottomSheetWindow) {
+ modalBottomSheetWindow.show()
+ onDispose {
+ modalBottomSheetWindow.disposeComposition()
+ modalBottomSheetWindow.dismiss()
+ }
+ }
+}
+
+/** Custom compose view for [ModalBottomSheet] */
+private class ModalBottomSheetWindow(
+ private var onDismissRequest: () -> Unit,
+ private val composeView: View,
+ saveId: UUID,
+) :
+ AbstractComposeView(composeView.context),
+ ViewTreeObserver.OnGlobalLayoutListener,
+ ViewRootForInspector {
+ init {
+ id = android.R.id.content
+ // Set up view owners
+ setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
+ setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
+ setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
+ setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Popup:$saveId")
+ // Enable children to draw their shadow by not clipping them
+ clipChildren = false
+ }
+
+ private val windowManager =
+ composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ private val displayWidth: Int
+ get() {
+ val density = context.resources.displayMetrics.density
+ return (context.resources.configuration.screenWidthDp * density).roundToInt()
+ }
+
+ private val displayHeight: Int
+ get() {
+ val density = context.resources.displayMetrics.density
+ return (context.resources.configuration.screenHeightDp * density).roundToInt()
+ }
+
+ private val params: WindowManager.LayoutParams =
+ WindowManager.LayoutParams().apply {
+ // Position bottom sheet from the bottom of the screen
+ gravity = Gravity.BOTTOM or Gravity.START
+ // Application panel window
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+ // Fill up the entire app view
+ width = displayWidth
+ height = displayHeight
+
+ // Format of screen pixels
+ format = PixelFormat.TRANSLUCENT
+ // Title used as fallback for a11y services
+ // TODO: Provide bottom sheet window resource
+ title = composeView.context.resources.getString(
+ androidx.compose.ui.R.string.default_popup_window_title
+ )
+ // Get the Window token from the parent view
+ token = composeView.applicationWindowToken
+ }
+
+ private var content: @Composable () -> Unit by mutableStateOf({})
+
+ override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
+ private set
+
+ @Composable
+ override fun Content() {
+ content()
+ }
+
+ fun setCustomContent(
+ parent: CompositionContext? = null,
+ content: @Composable () -> Unit
+ ) {
+ parent?.let { setParentCompositionContext(it) }
+ this.content = content
+ shouldCreateCompositionOnAttachedToWindow = true
+ }
+
+ fun show() {
+ windowManager.addView(this, params)
+ }
+
+ fun dismiss() {
+ setViewTreeLifecycleOwner(null)
+ setViewTreeSavedStateRegistryOwner(null)
+ composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ windowManager.removeViewImmediate(this)
+ }
+
+ /**
+ * Taken from PopupWindow. Calls [onDismissRequest] when back button is pressed.
+ */
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ if (event.keyCode == KeyEvent.KEYCODE_BACK) {
+ if (keyDispatcherState == null) {
+ return super.dispatchKeyEvent(event)
+ }
+ if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
+ val state = keyDispatcherState
+ state?.startTracking(event, this)
+ return true
+ } else if (event.action == KeyEvent.ACTION_UP) {
+ val state = keyDispatcherState
+ if (state != null && state.isTracking(event) && !event.isCanceled) {
+ onDismissRequest()
+ return true
+ }
+ }
+ }
+ return super.dispatchKeyEvent(event)
+ }
+
+ override fun onGlobalLayout() {
+ // No-op
+ }
+}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
index 07394fccbb0..27b2cc7e2d9 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/Strings.android.kt
@@ -153,6 +153,9 @@ internal actual fun getString(string: Strings): String {
Strings.DateRangeInputInvalidRangeInput -> resources.getString(
androidx.compose.material3.R.string.date_range_input_invalid_range_input
)
+ Strings.BottomSheetPaneTitle -> resources.getString(
+ androidx.compose.material3.R.string.bottom_sheet_pane_title
+ )
Strings.BottomSheetDragHandleDescription -> resources.getString(
androidx.compose.material3.R.string.bottom_sheet_drag_handle_description
)
diff --git a/compose/material3/material3/src/androidMain/res/values/strings.xml b/compose/material3/material3/src/androidMain/res/values/strings.xml
index dbd7ceb95af..2250a9cc5df 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -71,7 +71,9 @@
<!--
Describes an invalid date range input when a user enters a start or end date [CHAR_LIMIT=NONE]
-->
- <string name="date_range_input_invalid_range_input">Invalid date range input</string>
+ <string name="m3c_date_range_input_invalid_range_input">Invalid date range input</string>
+ <!-- Spoken description of a bottom sheet -->
+ <string name="m3c_bottom_sheet_pane_title">Bottom Sheet</string>
<!-- Names the drag handle visual for bottom sheet. -->
<string name="bottom_sheet_drag_handle_description">Drag handle</string>
<!-- Describes the collapse action for bottom sheet. -->
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
index dd8f671de58..285f8b4ad59 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Strings.kt
@@ -73,6 +73,7 @@ internal value class Strings private constructor(
val DateRangePickerDayInRange = Strings()
val DateRangeInputTitle = Strings()
val DateRangeInputInvalidRangeInput = Strings()
+ val BottomSheetPaneTitle = Strings()
val BottomSheetDragHandleDescription = Strings()
val BottomSheetPartialExpandDescription = Strings()
val BottomSheetDismissDescription = Strings()
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Strings.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Strings.desktop.kt
index 2de7b3174c7..71be0df073e 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Strings.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/Strings.desktop.kt
@@ -69,6 +69,7 @@ internal actual fun getString(string: Strings): String {
Strings.DateRangePickerDayInRange -> "In range"
Strings.DateRangeInputTitle -> "Enter dates"
Strings.DateRangeInputInvalidRangeInput -> "Invalid date range input"
+ Strings.BottomSheetPaneTitle -> "Bottom Sheet"
Strings.BottomSheetDragHandleDescription -> "Drag Handle"
Strings.BottomSheetPartialExpandDescription -> "Collapse bottom sheet"
Strings.BottomSheetDismissDescription -> "Dismiss bottom sheet"