Loading packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/PreCaptureUI.kt +27 −1 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex Loading @@ -37,6 +40,7 @@ import com.android.systemui.screencapture.common.ui.compose.loadIcon import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureRegion import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureRegion import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureType import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureType import com.android.systemui.screencapture.record.largescreen.ui.viewmodel.PreCaptureViewModel import com.android.systemui.screencapture.record.largescreen.ui.viewmodel.PreCaptureViewModel import kotlin.math.roundToInt /** Main component for the pre-capture UI. */ /** Main component for the pre-capture UI. */ @Composable @Composable Loading @@ -53,6 +57,19 @@ fun PreCaptureUI(viewModel: PreCaptureViewModel) { viewModel = viewModel, viewModel = viewModel, expanded = true, expanded = true, onCloseClick = { viewModel.closeUi() }, onCloseClick = { viewModel.closeUi() }, modifier = Modifier.onGloballyPositioned { val boundsInWindow = it.boundsInWindow() viewModel.updateToolbarBounds( Rect( boundsInWindow.left.roundToInt(), boundsInWindow.top.roundToInt(), boundsInWindow.right.roundToInt(), boundsInWindow.bottom.roundToInt(), ) ) } .graphicsLayer { alpha = viewModel.toolbarOpacity }, ) ) } } Loading Loading @@ -115,8 +132,17 @@ fun PreCaptureUI(viewModel: PreCaptureViewModel) { } } ), ), buttonIcon = icon, buttonIcon = icon, onRegionSelected = { rect: Rect -> viewModel.updateRegionBox(rect) }, onRegionSelected = { regionBoxRect -> viewModel.updateRegionBoxBounds(regionBoxRect) viewModel.updateToolbarOpacityForRegionBox( isInteracting = false, regionBoxRect = regionBoxRect, ) }, onCaptureClick = viewModel::beginCapture, onCaptureClick = viewModel::beginCapture, onInteractionStateChanged = { isInteracting -> viewModel.updateToolbarOpacityForRegionBox(isInteracting) }, ) ) } } Loading packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/RegionBox.kt +6 −0 Original line number Original line Diff line number Diff line Loading @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.remember Loading Loading @@ -355,6 +356,8 @@ class RegionBoxState(private val minSizePx: Float, private val touchAreaPx: Floa * user finishes a drag gesture. This rectangle is used for taking a screenshot. The rectangle is * user finishes a drag gesture. This rectangle is used for taking a screenshot. The rectangle is * of type [android.graphics.Rect] because the screenshot API requires int values. * of type [android.graphics.Rect] because the screenshot API requires int values. * @param onCaptureClick A callback function that is invoked when the capture button is clicked. * @param onCaptureClick A callback function that is invoked when the capture button is clicked. * @param onInteractionStateChanged A callback function that is invoked when the user starts or * stops interacting with the region box. * @param modifier The modifier to be applied to the composable. * @param modifier The modifier to be applied to the composable. */ */ @Composable @Composable Loading @@ -363,6 +366,7 @@ fun RegionBox( buttonIcon: Icon?, buttonIcon: Icon?, onRegionSelected: (rect: IntRect) -> Unit, onRegionSelected: (rect: IntRect) -> Unit, onCaptureClick: () -> Unit, onCaptureClick: () -> Unit, onInteractionStateChanged: (isInteracting: Boolean) -> Unit, modifier: Modifier = Modifier, modifier: Modifier = Modifier, ) { ) { val density = LocalDensity.current val density = LocalDensity.current Loading @@ -379,6 +383,8 @@ fun RegionBox( val scrimColor = ScreenCaptureColors.scrimColor val scrimColor = ScreenCaptureColors.scrimColor val pointerIcon = rememberPointerIcon(state) val pointerIcon = rememberPointerIcon(state) LaunchedEffect(state.dragMode) { onInteractionStateChanged(state.dragMode != DragMode.NONE) } Box( Box( modifier = modifier = modifier modifier Loading packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/viewmodel/PreCaptureViewModel.kt +37 −1 Original line number Original line Diff line number Diff line Loading @@ -71,6 +71,8 @@ constructor( ?: ScreenCaptureRegion.FULLSCREEN ?: ScreenCaptureRegion.FULLSCREEN ) ) private val regionBoxSource = MutableStateFlow<Rect?>(null) private val regionBoxSource = MutableStateFlow<Rect?>(null) private val toolbarBoundsSource = MutableStateFlow(Rect()) private val toolbarOpacitySource = MutableStateFlow(1f) val icons: ScreenCaptureIcons? by iconProvider.icons.hydratedStateOf() val icons: ScreenCaptureIcons? by iconProvider.icons.hydratedStateOf() Loading @@ -83,6 +85,7 @@ constructor( val captureRegion: ScreenCaptureRegion by captureRegionSource.hydratedStateOf() val captureRegion: ScreenCaptureRegion by captureRegionSource.hydratedStateOf() val regionBox: Rect? by regionBoxSource.hydratedStateOf() val regionBox: Rect? by regionBoxSource.hydratedStateOf() val toolbarOpacity: Float by toolbarOpacitySource.hydratedStateOf() val screenRecordingSupported = featuresInteractor.screenRecordingSupported val screenRecordingSupported = featuresInteractor.screenRecordingSupported Loading Loading @@ -118,10 +121,43 @@ constructor( captureRegionSource.value = selectedRegion captureRegionSource.value = selectedRegion } } fun updateRegionBox(bounds: Rect) { fun updateRegionBoxBounds(bounds: Rect) { regionBoxSource.value = bounds regionBoxSource.value = bounds } } /** * Updates the toolbar opacity based on whether the region box selection intersects with the * toolbar, and whether the region box is resized or moved. * * @param isInteracting whether the region box is currently resized or moved. * @param regionBoxRect the current bounds of the region box selection. If not provided, will * use the last selected region as the input to calculate the desired opacity. */ fun updateToolbarOpacityForRegionBox(isInteracting: Boolean, regionBoxRect: Rect? = null) { if (isInteracting) { toolbarOpacitySource.value = 0f return } // When interaction stops, or a region is selected, calculate the final opacity. val finalRegion = regionBoxRect ?: regionBoxSource.value if (finalRegion == null) { toolbarOpacitySource.value = 1f return } val toolbarRect = toolbarBoundsSource.value if (!toolbarRect.isEmpty && Rect.intersects(finalRegion, toolbarRect)) { toolbarOpacitySource.value = 0.15f } else { toolbarOpacitySource.value = 1f } } fun updateToolbarBounds(bounds: Rect) { toolbarBoundsSource.value = bounds } /** Initiates capture of the screen depending on the currently chosen capture type. */ /** Initiates capture of the screen depending on the currently chosen capture type. */ fun beginCapture() { fun beginCapture() { when (captureTypeSource.value) { when (captureTypeSource.value) { Loading packages/SystemUI/tests/src/com/android/systemui/screencapture/record/largescreen/ui/viewmodel/PreCaptureViewModelTest.kt +47 −3 Original line number Original line Diff line number Diff line Loading @@ -213,7 +213,7 @@ class PreCaptureViewModelTest : SysuiTestCase() { } } @Test @Test fun updateRegionBox_updatesState() = fun updateRegionBoxBounds_updatesState() = kosmos.runTest { kosmos.runTest { setupViewModel() setupViewModel() Loading @@ -221,7 +221,7 @@ class PreCaptureViewModelTest : SysuiTestCase() { assertThat(viewModel.regionBox).isNull() assertThat(viewModel.regionBox).isNull() val regionBox = Rect(0, 0, 100, 100) val regionBox = Rect(0, 0, 100, 100) viewModel.updateRegionBox(regionBox) viewModel.updateRegionBoxBounds(regionBox) assertThat(viewModel.regionBox).isEqualTo(regionBox) assertThat(viewModel.regionBox).isEqualTo(regionBox) } } Loading Loading @@ -255,7 +255,7 @@ class PreCaptureViewModelTest : SysuiTestCase() { viewModel.updateCaptureRegion(ScreenCaptureRegion.PARTIAL) viewModel.updateCaptureRegion(ScreenCaptureRegion.PARTIAL) val regionBox = Rect(0, 0, 100, 100) val regionBox = Rect(0, 0, 100, 100) viewModel.updateRegionBox(regionBox) viewModel.updateRegionBoxBounds(regionBox) whenever(kosmos.mockImageCapture.captureDisplay(any(), eq(regionBox))) whenever(kosmos.mockImageCapture.captureDisplay(any(), eq(regionBox))) .thenReturn(mockBitmap) .thenReturn(mockBitmap) Loading Loading @@ -403,4 +403,48 @@ class PreCaptureViewModelTest : SysuiTestCase() { assertThat(uiState).isEqualTo(ScreenCaptureUiState.Invisible) assertThat(uiState).isEqualTo(ScreenCaptureUiState.Invisible) } } @Test fun updateToolbarOpacityForRegionBox_isInteracting_opacityIsZero() = kosmos.runTest { setupViewModel() viewModel.updateToolbarOpacityForRegionBox(isInteracting = true) assertThat(viewModel.toolbarOpacity).isEqualTo(0f) } @Test fun updateToolbarOpacityForRegionBox_notInteracting_noOverlap_opacityIsOne() = kosmos.runTest { setupViewModel() viewModel.updateToolbarBounds(Rect(0, 0, 100, 100)) viewModel.updateRegionBoxBounds(Rect(200, 200, 300, 300)) viewModel.updateToolbarOpacityForRegionBox(isInteracting = false) assertThat(viewModel.toolbarOpacity).isEqualTo(1f) } @Test fun updateToolbarOpacityForRegionBox_notInteracting_overlap_opacityIsPoint15() = kosmos.runTest { setupViewModel() viewModel.updateToolbarBounds(Rect(0, 0, 100, 100)) viewModel.updateRegionBoxBounds(Rect(50, 50, 150, 150)) viewModel.updateToolbarOpacityForRegionBox(isInteracting = false) assertThat(viewModel.toolbarOpacity).isEqualTo(0.15f) } @Test fun updateToolbarOpacityForRegionBox_notInteracting_noRegion_opacityIsOne() = kosmos.runTest { setupViewModel() viewModel.updateToolbarBounds(Rect(0, 0, 100, 100)) viewModel.updateToolbarOpacityForRegionBox(isInteracting = false) assertThat(viewModel.toolbarOpacity).isEqualTo(1f) } } } Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/PreCaptureUI.kt +27 −1 Original line number Original line Diff line number Diff line Loading @@ -27,6 +27,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex Loading @@ -37,6 +40,7 @@ import com.android.systemui.screencapture.common.ui.compose.loadIcon import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureRegion import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureRegion import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureType import com.android.systemui.screencapture.record.largescreen.shared.model.ScreenCaptureType import com.android.systemui.screencapture.record.largescreen.ui.viewmodel.PreCaptureViewModel import com.android.systemui.screencapture.record.largescreen.ui.viewmodel.PreCaptureViewModel import kotlin.math.roundToInt /** Main component for the pre-capture UI. */ /** Main component for the pre-capture UI. */ @Composable @Composable Loading @@ -53,6 +57,19 @@ fun PreCaptureUI(viewModel: PreCaptureViewModel) { viewModel = viewModel, viewModel = viewModel, expanded = true, expanded = true, onCloseClick = { viewModel.closeUi() }, onCloseClick = { viewModel.closeUi() }, modifier = Modifier.onGloballyPositioned { val boundsInWindow = it.boundsInWindow() viewModel.updateToolbarBounds( Rect( boundsInWindow.left.roundToInt(), boundsInWindow.top.roundToInt(), boundsInWindow.right.roundToInt(), boundsInWindow.bottom.roundToInt(), ) ) } .graphicsLayer { alpha = viewModel.toolbarOpacity }, ) ) } } Loading Loading @@ -115,8 +132,17 @@ fun PreCaptureUI(viewModel: PreCaptureViewModel) { } } ), ), buttonIcon = icon, buttonIcon = icon, onRegionSelected = { rect: Rect -> viewModel.updateRegionBox(rect) }, onRegionSelected = { regionBoxRect -> viewModel.updateRegionBoxBounds(regionBoxRect) viewModel.updateToolbarOpacityForRegionBox( isInteracting = false, regionBoxRect = regionBoxRect, ) }, onCaptureClick = viewModel::beginCapture, onCaptureClick = viewModel::beginCapture, onInteractionStateChanged = { isInteracting -> viewModel.updateToolbarOpacityForRegionBox(isInteracting) }, ) ) } } Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/compose/RegionBox.kt +6 −0 Original line number Original line Diff line number Diff line Loading @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.remember Loading Loading @@ -355,6 +356,8 @@ class RegionBoxState(private val minSizePx: Float, private val touchAreaPx: Floa * user finishes a drag gesture. This rectangle is used for taking a screenshot. The rectangle is * user finishes a drag gesture. This rectangle is used for taking a screenshot. The rectangle is * of type [android.graphics.Rect] because the screenshot API requires int values. * of type [android.graphics.Rect] because the screenshot API requires int values. * @param onCaptureClick A callback function that is invoked when the capture button is clicked. * @param onCaptureClick A callback function that is invoked when the capture button is clicked. * @param onInteractionStateChanged A callback function that is invoked when the user starts or * stops interacting with the region box. * @param modifier The modifier to be applied to the composable. * @param modifier The modifier to be applied to the composable. */ */ @Composable @Composable Loading @@ -363,6 +366,7 @@ fun RegionBox( buttonIcon: Icon?, buttonIcon: Icon?, onRegionSelected: (rect: IntRect) -> Unit, onRegionSelected: (rect: IntRect) -> Unit, onCaptureClick: () -> Unit, onCaptureClick: () -> Unit, onInteractionStateChanged: (isInteracting: Boolean) -> Unit, modifier: Modifier = Modifier, modifier: Modifier = Modifier, ) { ) { val density = LocalDensity.current val density = LocalDensity.current Loading @@ -379,6 +383,8 @@ fun RegionBox( val scrimColor = ScreenCaptureColors.scrimColor val scrimColor = ScreenCaptureColors.scrimColor val pointerIcon = rememberPointerIcon(state) val pointerIcon = rememberPointerIcon(state) LaunchedEffect(state.dragMode) { onInteractionStateChanged(state.dragMode != DragMode.NONE) } Box( Box( modifier = modifier = modifier modifier Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/largescreen/ui/viewmodel/PreCaptureViewModel.kt +37 −1 Original line number Original line Diff line number Diff line Loading @@ -71,6 +71,8 @@ constructor( ?: ScreenCaptureRegion.FULLSCREEN ?: ScreenCaptureRegion.FULLSCREEN ) ) private val regionBoxSource = MutableStateFlow<Rect?>(null) private val regionBoxSource = MutableStateFlow<Rect?>(null) private val toolbarBoundsSource = MutableStateFlow(Rect()) private val toolbarOpacitySource = MutableStateFlow(1f) val icons: ScreenCaptureIcons? by iconProvider.icons.hydratedStateOf() val icons: ScreenCaptureIcons? by iconProvider.icons.hydratedStateOf() Loading @@ -83,6 +85,7 @@ constructor( val captureRegion: ScreenCaptureRegion by captureRegionSource.hydratedStateOf() val captureRegion: ScreenCaptureRegion by captureRegionSource.hydratedStateOf() val regionBox: Rect? by regionBoxSource.hydratedStateOf() val regionBox: Rect? by regionBoxSource.hydratedStateOf() val toolbarOpacity: Float by toolbarOpacitySource.hydratedStateOf() val screenRecordingSupported = featuresInteractor.screenRecordingSupported val screenRecordingSupported = featuresInteractor.screenRecordingSupported Loading Loading @@ -118,10 +121,43 @@ constructor( captureRegionSource.value = selectedRegion captureRegionSource.value = selectedRegion } } fun updateRegionBox(bounds: Rect) { fun updateRegionBoxBounds(bounds: Rect) { regionBoxSource.value = bounds regionBoxSource.value = bounds } } /** * Updates the toolbar opacity based on whether the region box selection intersects with the * toolbar, and whether the region box is resized or moved. * * @param isInteracting whether the region box is currently resized or moved. * @param regionBoxRect the current bounds of the region box selection. If not provided, will * use the last selected region as the input to calculate the desired opacity. */ fun updateToolbarOpacityForRegionBox(isInteracting: Boolean, regionBoxRect: Rect? = null) { if (isInteracting) { toolbarOpacitySource.value = 0f return } // When interaction stops, or a region is selected, calculate the final opacity. val finalRegion = regionBoxRect ?: regionBoxSource.value if (finalRegion == null) { toolbarOpacitySource.value = 1f return } val toolbarRect = toolbarBoundsSource.value if (!toolbarRect.isEmpty && Rect.intersects(finalRegion, toolbarRect)) { toolbarOpacitySource.value = 0.15f } else { toolbarOpacitySource.value = 1f } } fun updateToolbarBounds(bounds: Rect) { toolbarBoundsSource.value = bounds } /** Initiates capture of the screen depending on the currently chosen capture type. */ /** Initiates capture of the screen depending on the currently chosen capture type. */ fun beginCapture() { fun beginCapture() { when (captureTypeSource.value) { when (captureTypeSource.value) { Loading
packages/SystemUI/tests/src/com/android/systemui/screencapture/record/largescreen/ui/viewmodel/PreCaptureViewModelTest.kt +47 −3 Original line number Original line Diff line number Diff line Loading @@ -213,7 +213,7 @@ class PreCaptureViewModelTest : SysuiTestCase() { } } @Test @Test fun updateRegionBox_updatesState() = fun updateRegionBoxBounds_updatesState() = kosmos.runTest { kosmos.runTest { setupViewModel() setupViewModel() Loading @@ -221,7 +221,7 @@ class PreCaptureViewModelTest : SysuiTestCase() { assertThat(viewModel.regionBox).isNull() assertThat(viewModel.regionBox).isNull() val regionBox = Rect(0, 0, 100, 100) val regionBox = Rect(0, 0, 100, 100) viewModel.updateRegionBox(regionBox) viewModel.updateRegionBoxBounds(regionBox) assertThat(viewModel.regionBox).isEqualTo(regionBox) assertThat(viewModel.regionBox).isEqualTo(regionBox) } } Loading Loading @@ -255,7 +255,7 @@ class PreCaptureViewModelTest : SysuiTestCase() { viewModel.updateCaptureRegion(ScreenCaptureRegion.PARTIAL) viewModel.updateCaptureRegion(ScreenCaptureRegion.PARTIAL) val regionBox = Rect(0, 0, 100, 100) val regionBox = Rect(0, 0, 100, 100) viewModel.updateRegionBox(regionBox) viewModel.updateRegionBoxBounds(regionBox) whenever(kosmos.mockImageCapture.captureDisplay(any(), eq(regionBox))) whenever(kosmos.mockImageCapture.captureDisplay(any(), eq(regionBox))) .thenReturn(mockBitmap) .thenReturn(mockBitmap) Loading Loading @@ -403,4 +403,48 @@ class PreCaptureViewModelTest : SysuiTestCase() { assertThat(uiState).isEqualTo(ScreenCaptureUiState.Invisible) assertThat(uiState).isEqualTo(ScreenCaptureUiState.Invisible) } } @Test fun updateToolbarOpacityForRegionBox_isInteracting_opacityIsZero() = kosmos.runTest { setupViewModel() viewModel.updateToolbarOpacityForRegionBox(isInteracting = true) assertThat(viewModel.toolbarOpacity).isEqualTo(0f) } @Test fun updateToolbarOpacityForRegionBox_notInteracting_noOverlap_opacityIsOne() = kosmos.runTest { setupViewModel() viewModel.updateToolbarBounds(Rect(0, 0, 100, 100)) viewModel.updateRegionBoxBounds(Rect(200, 200, 300, 300)) viewModel.updateToolbarOpacityForRegionBox(isInteracting = false) assertThat(viewModel.toolbarOpacity).isEqualTo(1f) } @Test fun updateToolbarOpacityForRegionBox_notInteracting_overlap_opacityIsPoint15() = kosmos.runTest { setupViewModel() viewModel.updateToolbarBounds(Rect(0, 0, 100, 100)) viewModel.updateRegionBoxBounds(Rect(50, 50, 150, 150)) viewModel.updateToolbarOpacityForRegionBox(isInteracting = false) assertThat(viewModel.toolbarOpacity).isEqualTo(0.15f) } @Test fun updateToolbarOpacityForRegionBox_notInteracting_noRegion_opacityIsOne() = kosmos.runTest { setupViewModel() viewModel.updateToolbarBounds(Rect(0, 0, 100, 100)) viewModel.updateToolbarOpacityForRegionBox(isInteracting = false) assertThat(viewModel.toolbarOpacity).isEqualTo(1f) } } }