diff options
| author | 2024-09-04 12:33:30 -0700 | |
|---|---|---|
| committer | 2024-09-16 14:43:14 -0700 | |
| commit | ab133055f90d61bd77cbe0c005ff89d35e1d5d34 (patch) | |
| tree | e8b82a36f60f5dbcb702901a64883f1c511a0e97 | |
| parent | c99762b26c019ea640d1fcedbe377b5df41478c7 (diff) | |
[flexiglass] Implement stack scroll on remote input activation in Shade
When remote input is started, we pipe the target bottom bound of the remote input row after its expansion animation to flexiglass, where we diff that value with the target top bound of the IME after its expansion animation. We then artificially add the IME height to the stack content height, and scroll it by the diff so that the remote input row isn't overlapped by the IME.
Bug: 359876385
Test: manually verified stack scrolls above IME when remote input starts
Test: manually verified stack scrolls above IME when a second remote input is started while another is already active
Test: manually verified stack scrolls above IME even when there is no space below the remote input row, and then scrolls back down when IME is dismissed
Test: manually verified remote input in HUN is unaffected
Flag: com.android.systemui.scene_container
Change-Id: Ibe85ae9f6e10502fcf986b3a0056f57716a2cbb5
18 files changed, 241 insertions, 77 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt index 296fc27ac0ff..dcf32b2bcda4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/ScreenDecorProvider.kt @@ -16,15 +16,10 @@ package com.android.systemui.common.ui.compose.windowinsets -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.systemBars import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -36,9 +31,6 @@ val LocalDisplayCutout = staticCompositionLocalOf { DisplayCutout() } /** The corner radius in px of the current display. */ val LocalScreenCornerRadius = staticCompositionLocalOf { 0.dp } -/** The screen height in px without accounting for any screen insets (cutouts, status/nav bars) */ -val LocalRawScreenHeight = staticCompositionLocalOf { 0f } - @Composable fun ScreenDecorProvider( displayCutout: StateFlow<DisplayCutout>, @@ -48,22 +40,9 @@ fun ScreenDecorProvider( val cutout by displayCutout.collectAsStateWithLifecycle() val screenCornerRadiusDp = with(LocalDensity.current) { screenCornerRadius.toDp() } - val density = LocalDensity.current - val navBarHeight = - with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() } - val statusBarHeight = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() - val displayCutoutHeight = WindowInsets.displayCutout.asPaddingValues().calculateTopPadding() - val screenHeight = - with(density) { - (LocalConfiguration.current.screenHeightDp.dp + - maxOf(statusBarHeight, displayCutoutHeight)) - .toPx() - } + navBarHeight - CompositionLocalProvider( LocalScreenCornerRadius provides screenCornerRadiusDp, LocalDisplayCutout provides cutout, - LocalRawScreenHeight provides screenHeight, ) { content() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt index 897a8613263f..a2ae8bbf66e4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt @@ -24,9 +24,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import com.android.compose.nestedscroll.PriorityNestedScrollConnection -import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import kotlin.math.max import kotlin.math.roundToInt import kotlin.math.tanh @@ -36,9 +38,10 @@ import kotlinx.coroutines.launch @Composable fun Modifier.stackVerticalOverscroll( coroutineScope: CoroutineScope, - canScrollForward: () -> Boolean + canScrollForward: () -> Boolean, ): Modifier { - val screenHeight = LocalRawScreenHeight.current + val screenHeight = + with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() } val overscrollOffset = remember { Animatable(0f) } val stackNestedScrollConnection = remember { NotificationStackNestedScrollConnection( @@ -60,10 +63,10 @@ fun Modifier.stackVerticalOverscroll( overscrollOffset.animateTo( targetValue = 0f, initialVelocity = velocityAvailable, - animationSpec = tween() + animationSpec = tween(), ) } - } + }, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 91ecfc18a76e..1b99a9644575 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -19,6 +19,7 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background @@ -29,6 +30,8 @@ import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.absoluteOffset @@ -36,9 +39,11 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imeAnimationTarget import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme @@ -68,6 +73,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp @@ -81,7 +87,6 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.thenIf -import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession @@ -96,6 +101,7 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch object Notifications { @@ -171,7 +177,7 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( setCurrent = { scrollOffset = it }, min = minScrollOffset, max = maxScrollOffset, - delta + delta, ) } @@ -209,8 +215,8 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( calculateHeadsUpPlaceholderYOffset( scrollOffset.roundToInt(), minScrollOffset.roundToInt(), - stackScrollView.topHeadsUpHeight - ) + stackScrollView.topHeadsUpHeight, + ), ) } .thenIf(isHeadsUp) { @@ -218,11 +224,8 @@ fun SceneScope.SnoozeableHeadsUpNotificationSpace( bottomBehavior = NestedScrollBehavior.EdgeAlways ) .nestedScroll(nestedScrollConnection) - .scrollable( - orientation = Orientation.Vertical, - state = scrollableState, - ) - } + .scrollable(orientation = Orientation.Vertical, state = scrollableState) + }, ) } @@ -259,6 +262,7 @@ fun SceneScope.ConstrainedNotificationStack( * Adds the space where notification stack should appear in the scene, with a scrim and nested * scrolling. */ +@OptIn(ExperimentalLayoutApi::class) @Composable fun SceneScope.NotificationScrollingStack( shadeSession: SaveableSession, @@ -291,7 +295,7 @@ fun SceneScope.NotificationScrollingStack( val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() val bottomPadding = if (shouldReserveSpaceForNavBar) navBarHeight else 0.dp - val screenHeight = LocalRawScreenHeight.current + val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } /** * The height in px of the contents of notification stack. Depending on the number of @@ -325,6 +329,14 @@ fun SceneScope.NotificationScrollingStack( screenHeight - maxScrimTop() - with(density) { navBarHeight.toPx() } } + val isRemoteInputActive by viewModel.isRemoteInputActive.collectAsStateWithLifecycle(false) + + // The bottom Y bound of the currently focused remote input notification. + val remoteInputRowBottom by viewModel.remoteInputRowBottomBound.collectAsStateWithLifecycle(0f) + + // The top y bound of the IME. + val imeTop = remember { mutableFloatStateOf(0f) } + // we are not scrolled to the top unless the scrim is at its maximum offset. LaunchedEffect(viewModel, scrimOffset) { snapshotFlow { scrimOffset.value >= 0f } @@ -342,15 +354,34 @@ fun SceneScope.NotificationScrollingStack( LaunchedEffect(syntheticScroll, scrimOffset, scrollState) { snapshotFlow { syntheticScroll.value } .collect { delta -> - val minOffset = minScrimOffset() - if (scrimOffset.value > minOffset) { - val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f) - scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset)) - if (remainingDelta > 0f) { - scrollState.scrollBy(remainingDelta) - } - } else { - scrollState.scrollTo(delta.roundToInt()) + scrollNotificationStack( + scope = coroutineScope, + delta = delta, + animate = false, + scrimOffset = scrimOffset, + minScrimOffset = minScrimOffset, + scrollState = scrollState, + ) + } + } + + // if remote input state changes, compare the row and IME's overlap and offset the scrim and + // placeholder accordingly. + LaunchedEffect(isRemoteInputActive, remoteInputRowBottom, imeTop) { + imeTop.floatValue = 0f + snapshotFlow { imeTop.floatValue } + .collect { imeTopValue -> + // only scroll the stack if ime value has been populated (ime placeholder has been + // composed at least once), and our remote input row overlaps with the ime bounds. + if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) { + scrollNotificationStack( + scope = coroutineScope, + delta = remoteInputRowBottom - imeTopValue, + animate = true, + scrimOffset = scrimOffset, + minScrimOffset = minScrimOffset, + scrollState = scrollState, + ) } } } @@ -394,12 +425,12 @@ fun SceneScope.NotificationScrollingStack( scrimOffset.value < 0 && layoutState.isTransitioning( from = Scenes.Shade, - to = Scenes.QuickSettings + to = Scenes.QuickSettings, ) ) { IntOffset( x = 0, - y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt() + y = (scrimOffset.value * (1 - shadeToQsFraction)).roundToInt(), ) } else { IntOffset(x = 0, y = scrimOffset.value.roundToInt()) @@ -458,13 +489,11 @@ fun SceneScope.NotificationScrollingStack( .thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() } .debugBackground(viewModel, DEBUG_BOX_COLOR) ) { - NotificationPlaceholder( - stackScrollView = stackScrollView, - viewModel = viewModel, + Column( modifier = Modifier.verticalNestedScrollToScene( topBehavior = NestedScrollBehavior.EdgeWithPreview, - isExternalOverscrollGesture = { isCurrentGestureOverscroll.value } + isExternalOverscrollGesture = { isCurrentGestureOverscroll.value }, ) .thenIf(shadeMode == ShadeMode.Single) { Modifier.nestedScroll(scrimNestedScrollConnection) @@ -473,18 +502,31 @@ fun SceneScope.NotificationScrollingStack( .verticalScroll(scrollState) .padding(top = topPadding) .fillMaxWidth() - .notificationStackHeight( - view = stackScrollView, - totalVerticalPadding = topPadding + bottomPadding, - ) - .onSizeChanged { size -> stackHeight.intValue = size.height }, - ) + ) { + NotificationPlaceholder( + stackScrollView = stackScrollView, + viewModel = viewModel, + modifier = + Modifier.notificationStackHeight( + view = stackScrollView, + totalVerticalPadding = topPadding + bottomPadding, + ) + .onSizeChanged { size -> stackHeight.intValue = size.height }, + ) + Spacer( + modifier = + Modifier.windowInsetsBottomHeight(WindowInsets.imeAnimationTarget) + .onGloballyPositioned { coordinates: LayoutCoordinates -> + imeTop.floatValue = screenHeight - coordinates.size.height + } + ) + } } if (shouldIncludeHeadsUpSpace) { HeadsUpNotificationSpace( stackScrollView = stackScrollView, viewModel = viewModel, - modifier = Modifier.padding(top = topPadding) + modifier = Modifier.padding(top = topPadding), ) } } @@ -572,6 +614,42 @@ private fun SceneScope.NotificationPlaceholder( ) } +private suspend fun scrollNotificationStack( + scope: CoroutineScope, + delta: Float, + animate: Boolean, + scrimOffset: Animatable<Float, AnimationVector1D>, + minScrimOffset: () -> Float, + scrollState: ScrollState, +) { + val minOffset = minScrimOffset() + if (scrimOffset.value > minOffset) { + val remainingDelta = + (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f).roundToInt() + if (remainingDelta > 0) { + if (animate) { + // launch a new coroutine for the remainder animation so that it doesn't suspend the + // scrim animation, allowing both to play simultaneously. + scope.launch { scrollState.animateScrollTo(remainingDelta) } + } else { + scrollState.scrollTo(remainingDelta) + } + } + val newScrimOffset = (scrimOffset.value - delta).coerceAtLeast(minOffset) + if (animate) { + scrimOffset.animateTo(newScrimOffset) + } else { + scrimOffset.snapTo(newScrimOffset) + } + } else { + if (animate) { + scrollState.animateScrollBy(delta) + } else { + scrollState.scrollBy(delta) + } + } +} + private fun calculateCornerRadius( scrimCornerRadius: Dp, screenCornerRadius: Dp, @@ -618,7 +696,7 @@ private fun consumeDeltaWithinRange( setCurrent: (Float) -> Unit, min: Float, max: Float, - delta: Float + delta: Float, ): Float { return if (delta < 0 && current > min) { val remainder = (current + delta - min).coerceAtMost(0f) @@ -631,10 +709,7 @@ private fun consumeDeltaWithinRange( } else 0f } -private inline fun debugLog( - viewModel: NotificationsPlaceholderViewModel, - msg: () -> Any, -) { +private inline fun debugLog(viewModel: NotificationsPlaceholderViewModel, msg: () -> Any) { if (viewModel.isDebugLoggingEnabled) { Log.d(TAG, msg().toString()) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index fa92bef34f38..0c1c16522567 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource @@ -79,7 +80,6 @@ import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout -import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable @@ -229,17 +229,16 @@ private fun SceneScope.QuickSettingsScene( } .thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() } ) { + val density = LocalDensity.current val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsStateWithLifecycle() val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle() val customizingAnimationDuration by viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsStateWithLifecycle() - val screenHeight = LocalRawScreenHeight.current + val screenHeight = with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } BackHandler(enabled = isCustomizing) { viewModel.qsSceneAdapter.requestCloseCustomizer() } - val collapsedHeaderHeight = - with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } val lifecycleOwner = LocalLifecycleOwner.current val footerActionsViewModel = remember(lifecycleOwner, viewModel) { @@ -268,7 +267,6 @@ private fun SceneScope.QuickSettingsScene( val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - val density = LocalDensity.current val bottomPadding by animateDpAsState( targetValue = if (isCustomizing) 0.dp else navBarBottomHeight, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt index c0302bc348b6..9af4b8c18c86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt @@ -25,6 +25,7 @@ import dagger.Module import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow /** * Repository used for tracking the state of notification remote input (e.g. when the user presses @@ -33,14 +34,21 @@ import kotlinx.coroutines.flow.Flow interface RemoteInputRepository { /** Whether remote input is currently active for any notification. */ val isRemoteInputActive: Flow<Boolean> + + /** + * The bottom bound of the currently focused remote input notification row, or null if there + * isn't one. + */ + val remoteInputRowBottomBound: Flow<Float?> + + fun setRemoteInputRowBottomBound(bottom: Float?) } @SysUISingleton class RemoteInputRepositoryImpl @Inject -constructor( - private val notificationRemoteInputManager: NotificationRemoteInputManager, -) : RemoteInputRepository { +constructor(private val notificationRemoteInputManager: NotificationRemoteInputManager) : + RemoteInputRepository { override val isRemoteInputActive: Flow<Boolean> = conflatedCallbackFlow { trySend(false) // initial value is false val callback = @@ -52,6 +60,12 @@ constructor( notificationRemoteInputManager.addControllerCallback(callback) awaitClose { notificationRemoteInputManager.removeControllerCallback(callback) } } + + override val remoteInputRowBottomBound = MutableStateFlow<Float?>(null) + + override fun setRemoteInputRowBottomBound(bottom: Float?) { + remoteInputRowBottomBound.value = bottom + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt index 68f727b046c0..b83b0cc8d2c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt @@ -20,13 +20,24 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.data.repository.RemoteInputRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull /** * Interactor used for business logic pertaining to the notification remote input (e.g. when the * user presses "reply" on a notification and the keyboard opens). */ @SysUISingleton -class RemoteInputInteractor @Inject constructor(remoteInputRepository: RemoteInputRepository) { +class RemoteInputInteractor +@Inject +constructor(private val remoteInputRepository: RemoteInputRepository) { /** Is remote input currently active for a notification? */ val isRemoteInputActive: Flow<Boolean> = remoteInputRepository.isRemoteInputActive + + /** The bottom bound of the currently focused remote input notification row. */ + val remoteInputRowBottomBound: Flow<Float> = + remoteInputRepository.remoteInputRowBottomBound.mapNotNull { it } + + fun setRemoteInputRowBottomBound(bottom: Float?) { + remoteInputRepository.setRemoteInputRowBottomBound(bottom) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index cb3e26b9f8ea..5003a6af5c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -21,6 +21,7 @@ import static android.service.notification.NotificationListenerService.REASON_CA import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; +import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE; import static com.android.systemui.util.ColorUtilKt.hexColorString; import android.animation.Animator; @@ -83,6 +84,7 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; @@ -118,6 +120,7 @@ import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; +import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.statusbar.policy.SmartReplyConstants; import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; import com.android.systemui.util.Compile; @@ -830,6 +833,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.setRemoteInputController(r); } + /** + * Return the cumulative y-value that the actions container expands via its scale animator when + * remote input is activated. + */ + public float getRemoteInputActionsContainerExpandedOffset() { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f; + RemoteInputView expandedRemoteInput = mPrivateLayout.getExpandedRemoteInput(); + if (expandedRemoteInput == null) return 0f; + View actionsContainerLayout = expandedRemoteInput.getActionsContainerLayout(); + if (actionsContainerLayout == null) return 0f; + + return actionsContainerLayout.getHeight() * (1 - FOCUS_ANIMATION_MIN_SCALE) * 0.5f; + } + public void addChildNotification(ExpandableNotificationRow row) { addChildNotification(row, -1); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7543f3b48e48..e7c67f93eb78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -99,6 +99,7 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FakeShadowView; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -120,7 +121,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrim import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape; import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.ScrollAdapter; @@ -740,6 +740,15 @@ public class NotificationStackScrollLayout updateFooter(); } + void sendRemoteInputRowBottomBound(Float bottom) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + if (bottom != null) { + bottom += getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_content_margin); + } + mScrollViewFields.sendRemoteInputRowBottomBound(bottom); + } + /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */ public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) { FooterViewRefactor.assertInLegacyMode(); @@ -1274,6 +1283,11 @@ public class NotificationStackScrollLayout } @Override + public void setRemoteInputRowBottomBoundConsumer(@Nullable Consumer<Float> consumer) { + mScrollViewFields.setRemoteInputRowBottomBoundConsumer(consumer); + } + + @Override public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) { mScrollViewFields.setHeadsUpHeightConsumer(consumer); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e5f63c1cb480..dad6894a43ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -98,6 +98,9 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; +import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; +import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; @@ -129,9 +132,6 @@ import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.notification.HeadsUpNotificationViewControllerEmptyImpl; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; -import com.android.systemui.statusbar.notification.HeadsUpTouchHelper.HeadsUpNotificationViewController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -1605,6 +1605,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { return new RemoteInputController.Delegate() { public void setRemoteInputActive(NotificationEntry entry, boolean remoteInputActive) { + if (SceneContainerFlag.isEnabled()) { + sendRemoteInputRowBottomBound(entry, remoteInputActive); + } mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive); entry.notifyHeightChanged(true /* needsAnimation */); if (!FooterViewRefactor.isEnabled()) { @@ -1620,6 +1623,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.requestDisallowLongPress(); mView.requestDisallowDismiss(); } + + private void sendRemoteInputRowBottomBound(NotificationEntry entry, + boolean remoteInputActive) { + ExpandableNotificationRow row = entry.getRow(); + float top = row.getTranslationY(); + int height = row.getActualHeight(); + float bottom = top + height + row.getRemoteInputActionsContainerExpandedOffset(); + mView.sendRemoteInputRowBottomBound(remoteInputActive ? bottom : null); + } }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt index aa3953987c10..c08ed6120832 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt @@ -57,6 +57,13 @@ class ScrollViewFields { * guts off of this gesture, we can notify the placeholder through here. */ var currentGestureInGutsConsumer: Consumer<Boolean>? = null + + /** + * When a notification begins remote input, its bottom Y bound is sent to the placeholder + * through here in order to adjust to accommodate the IME. + */ + var remoteInputRowBottomBoundConsumer: Consumer<Float?>? = null + /** * Any time the heads up height is recalculated, it should be updated here to be used by the * placeholder @@ -75,6 +82,10 @@ class ScrollViewFields { fun sendCurrentGestureInGuts(isCurrentGestureInGuts: Boolean) = currentGestureInGutsConsumer?.accept(isCurrentGestureInGuts) + /** send [bottomY] to the [remoteInputRowBottomBoundConsumer], if present. */ + fun sendRemoteInputRowBottomBound(bottomY: Float?) = + remoteInputRowBottomBoundConsumer?.accept(bottomY) + /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */ fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index 235b4da3f029..41c02934efa6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -74,6 +74,9 @@ interface NotificationScrollView { /** Set a consumer for current gesture in guts events */ fun setCurrentGestureInGutsConsumer(consumer: Consumer<Boolean>?) + /** Set a consumer for current remote input notification row bottom bound events */ + fun setRemoteInputRowBottomBoundConsumer(consumer: Consumer<Float?>?) + /** Set a consumer for heads up height changed events */ fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 6d5553fec6b4..2e37dead8787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -108,10 +108,14 @@ constructor( view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) view.setCurrentGestureInGutsConsumer(viewModel.currentGestureInGutsConsumer) + view.setRemoteInputRowBottomBoundConsumer( + viewModel.remoteInputRowBottomBoundConsumer + ) DisposableHandle { view.setSyntheticScrollConsumer(null) view.setCurrentGestureOverscrollConsumer(null) view.setCurrentGestureInGutsConsumer(null) + view.setRemoteInputRowBottomBoundConsumer(null) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 8d7007b2fba4..5b2e02d446cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -31,6 +31,7 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape @@ -56,6 +57,7 @@ constructor( dumpManager: DumpManager, stackAppearanceInteractor: NotificationStackAppearanceInteractor, shadeInteractor: ShadeInteractor, + private val remoteInputInteractor: RemoteInputInteractor, private val sceneInteractor: SceneInteractor, // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released - // while the flag is off, creating this object too early results in a crash @@ -240,6 +242,10 @@ constructor( val currentGestureInGutsConsumer: (Boolean) -> Unit = stackAppearanceInteractor::setCurrentGestureInGuts + /** Receives the bottom bound of the currently focused remote input notification row. */ + val remoteInputRowBottomBoundConsumer: (Float?) -> Unit = + remoteInputInteractor::setRemoteInputRowBottomBound + /** Whether the notification stack is scrollable or not. */ val isScrollable: Flow<Boolean> = combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 69c1bf3b61b7..c8e83581e831 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -24,6 +24,7 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds @@ -49,6 +50,7 @@ constructor( private val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, + remoteInputInteractor: RemoteInputInteractor, featureFlags: FeatureFlagsClassic, dumpManager: DumpManager, ) : @@ -132,6 +134,12 @@ constructor( val isCurrentGestureOverscroll: Flow<Boolean> = interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll") + /** Whether remote input is currently active for any notification. */ + val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive + + /** The bottom bound of the currently focused remote input notification row. */ + val remoteInputRowBottomBound = remoteInputInteractor.remoteInputRowBottomBound + /** Sets whether the notification stack is scrolled to the top. */ fun setScrolledToTop(scrolledToTop: Boolean) { interactor.setScrolledToTop(scrolledToTop) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 31776cf5ad1b..16d5f8d30593 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -106,7 +106,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50; private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; - private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; + public static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt index c416ea1c1b39..91602c23ac17 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt @@ -16,8 +16,13 @@ package com.android.systemui.statusbar.data.repository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf class FakeRemoteInputRepository : RemoteInputRepository { override val isRemoteInputActive = MutableStateFlow(false) + override val remoteInputRowBottomBound: Flow<Float?> = flowOf(null) + + override fun setRemoteInputRowBottomBound(bottom: Float?) {} } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt index 6370a5d9c80b..7244d465ed7e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.notificationScrollViewModel by Fixture { @@ -29,6 +30,7 @@ val Kosmos.notificationScrollViewModel by Fixture { dumpManager = dumpManager, stackAppearanceInteractor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, + remoteInputInteractor = remoteInputInteractor, sceneInteractor = sceneInteractor, keyguardInteractor = { keyguardInteractor }, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index 8bfc390ecfa3..e5cf0a90ebbd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor @@ -31,6 +32,7 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, headsUpNotificationInteractor = headsUpNotificationInteractor, + remoteInputInteractor = remoteInputInteractor, featureFlags = featureFlagsClassic, dumpManager = dumpManager, ) |