Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4c022dbd authored by Anton Potapov's avatar Anton Potapov
Browse files

Add target choosing spinner

Flag: com.android.systemui.new_screen_record_toolbar
Bug: 428171948
Test: manual on foldable: check that the new spinner is working
Change-Id: Idb7a790ccfce28c8ca45974b7525aa219a52147c
parent 5bff7692
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -348,6 +348,12 @@
    <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
    <string name="screenrecord_stop_dialog_button">Stop recording</string>

    <!-- Screen recording should record entire screen [CHAR LIMIT=35] -->
    <string name="screen_record_entire_screen">Entire screen</string>
    <!-- Screen recording should record a single app [CHAR LIMIT=35] -->
    <string name="screen_record_single_app">Single app</string>
    <!-- Screen recording should record a single app but there are no apps open [CHAR LIMIT=35] -->
    <string name="screen_record_single_app_no_recents">Single app (Open an app to choose)</string>
    <!-- Screen recording should record device audio setting [CHAR LIMIT=35] -->
    <string name="screen_record_record_device_audio_label">Record device audio</string>
    <!-- Screen recording should record microphone setting [CHAR LIMIT=35] -->
+0 −24
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.screencapture.record.shared.ui.viewmodel

/** Models a generic ViewModel that supports selecting a single item from a list. */
data class SingleSelectViewModel<T>(
    val items: List<T>,
    val selectedItemIndex: Int,
    val onItemSelect: (Int) -> Unit,
)
+177 −0
Original line number Diff line number Diff line
@@ -16,28 +16,19 @@

package com.android.systemui.screencapture.record.smallscreen.ui.compose

import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -47,112 +38,140 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.systemui.res.R
import com.android.systemui.screencapture.record.shared.ui.viewmodel.SingleSelectViewModel
import com.android.systemui.screencapture.record.smallscreen.ui.viewmodel.CaptureTargetSpinnerItemViewModel
import com.android.systemui.screencapture.common.ui.compose.LoadingIcon
import com.android.systemui.screencapture.common.ui.compose.loadIcon
import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel

@Composable
fun CaptureTargetSpinner(
    viewModel: SingleSelectViewModel<CaptureTargetSpinnerItemViewModel>,
fun <T> CaptureTargetSelector(
    items: List<T>?,
    selectedItemIndex: Int,
    onItemSelected: (Int) -> Unit,
    viewModel: DrawableLoaderViewModel,
    modifier: Modifier = Modifier,
    itemToString: @Composable (T) -> String = { it.toString() },
    isItemEnabled: @Composable (T) -> Boolean = { true },
) {
    val itemHeight: Dp = 56.dp
    val cornerRadius: Dp = itemHeight / 2
    val itemHeight = 56.dp
    val width = 272.dp
    val shape = RoundedCornerShape(itemHeight / 2)
    var expanded by remember { mutableStateOf(false) }
    Box(
        modifier = modifier.animateContentSize().height(IntrinsicSize.Min).width(IntrinsicSize.Min)

    Box(modifier = modifier.width(width)) {
        TextButton(
            onClick = { expanded = true },
            shape = shape,
            border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
            colors =
                ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.onSurface),
            contentPadding = PaddingValues(16.dp),
        ) {
        AnimatedContent(
            targetState = expanded,
            transitionSpec = { fadeIn() togetherWith fadeOut() },
        ) { currentExpanded ->
            if (currentExpanded) {
                Surface(
                    content = {},
                    shape = RoundedCornerShape(cornerRadius),
                    color = MaterialTheme.colorScheme.surfaceBright,
                    modifier = Modifier.fillMaxSize(),
            if (!items.isNullOrEmpty()) {
                Text(
                    text = itemToString(items[selectedItemIndex]),
                    style = MaterialTheme.typography.labelLarge,
                    maxLines = 1,
                    modifier = Modifier.weight(1f).basicMarquee(),
                )
            } else {
                Surface(
                    content = {},
                    color = Color.Transparent,
                    shape = RoundedCornerShape(cornerRadius),
                    border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
                    modifier = Modifier.fillMaxWidth().height(itemHeight),
                LoadingIcon(
                    loadIcon(
                            viewModel = viewModel,
                            resId = R.drawable.ic_arrow_down_24dp,
                            contentDescription = null,
                        )
                        .value
                )
            }
        }
        Column(modifier = Modifier.clip(RoundedCornerShape(cornerRadius))) {
            viewModel.items.fastForEachIndexed { index, itemViewModel ->
                val isCurrentOption = index == viewModel.selectedItemIndex
                AnimatedVisibility(visible = expanded || isCurrentOption) {
                    ToolbarItem(
                        label = itemViewModel.label,
                        icon =
                            if (expanded) {
                                if (isCurrentOption) {
                                    R.drawable.ic_check_expressive
                                } else {
                                    null
                                }
                            } else {
                                R.drawable.ic_expressive_spinner_arrow
                            },
                        onClick = {
                            if (expanded) {
        DropdownMenu(
            expanded = expanded,
            shape = shape,
            // -1.dp guarantees overlap with the TextButton border. Otherwise the dialog doesn't
            // fully cover its top
            offset = DpOffset(0.dp, -itemHeight - 1.dp),
            containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
            onDismissRequest = { expanded = false },
            // DropdownMenu adds unavoidable vertical padding to the content. This offsets it
            modifier = Modifier.width(width).padding(vertical = { -8.dp.roundToPx() }),
        ) {
            items ?: return@DropdownMenu
            items.fastForEachIndexed { index, item ->
                Item(
                    label = itemToString(item),
                    selected = index == selectedItemIndex,
                    onSelected = {
                        expanded = false
                                viewModel.onItemSelect(index)
                            } else {
                                expanded = true
                            }
                        onItemSelected(index)
                    },
                        active = !expanded && isCurrentOption,
                        modifier = Modifier.fillMaxWidth().height(itemHeight),
                    enabled = isItemEnabled(item),
                    shape = shape,
                    viewModel = viewModel,
                    modifier = Modifier.height(itemHeight),
                )
            }
        }
    }
}
}

@Composable
private fun ToolbarItem(
private fun Item(
    label: String,
    onClick: () -> Unit,
    active: Boolean,
    selected: Boolean,
    enabled: Boolean,
    onSelected: () -> Unit,
    viewModel: DrawableLoaderViewModel,
    shape: Shape,
    modifier: Modifier = Modifier,
    @DrawableRes icon: Int? = null,
) {
    TextButton(
        onClick = onClick,
        modifier = modifier,
        colors =
            if (active) {
                ButtonDefaults.textButtonColors(
                    containerColor = MaterialTheme.colorScheme.secondaryContainer,
                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
                )
    val selectedBackgroundColor = MaterialTheme.colorScheme.secondaryContainer
    val contentColor =
        if (selected) {
            MaterialTheme.colorScheme.onSurface
        } else {
                ButtonDefaults.textButtonColors(
                    containerColor = Color.Transparent,
                    contentColor = MaterialTheme.colorScheme.onSurface,
            MaterialTheme.colorScheme.onSecondaryContainer
        }
    DropdownMenuItem(
        text = {
            Text(
                text = label,
                style = MaterialTheme.typography.labelLarge,
                maxLines = 1,
                modifier = Modifier.basicMarquee(),
            )
        },
        contentPadding = PaddingValues(horizontal = 16.dp),
    ) {
        Text(text = label, modifier = Modifier.weight(1f))
        icon?.let {
            Icon(
                painter = painterResource(it),
        onClick = onSelected,
        enabled = enabled,
        trailingIcon =
            if (selected) {
                {
                    LoadingIcon(
                        loadIcon(
                                viewModel = viewModel,
                                resId = R.drawable.ic_check_expressive,
                                contentDescription = null,
                modifier = Modifier.size(24.dp),
                            )
                            .value
                    )
                }
    }
            } else {
                null
            },
        colors =
            MenuDefaults.itemColors(
                textColor = contentColor,
                trailingIconColor = contentColor,
                leadingIconColor = contentColor,
            ),
        modifier =
            modifier.clip(shape).thenIf(selected) {
                Modifier.background(color = selectedBackgroundColor, shape = shape)
            },
    )
}
+24 −10
Original line number Diff line number Diff line
@@ -39,11 +39,13 @@ import com.android.systemui.res.R
import com.android.systemui.screencapture.common.ui.compose.LoadingIcon
import com.android.systemui.screencapture.common.ui.compose.loadIcon
import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel
import com.android.systemui.screencapture.record.smallscreen.ui.viewmodel.RecordDetailsTargetViewModel
import com.android.systemui.screencapture.record.ui.viewmodel.ScreenCaptureRecordParametersViewModel

@Composable
fun RecordDetailsSettings(
    viewModel: ScreenCaptureRecordParametersViewModel,
    parametersViewModel: ScreenCaptureRecordParametersViewModel,
    targetViewModel: RecordDetailsTargetViewModel,
    drawableLoaderViewModel: DrawableLoaderViewModel,
    modifier: Modifier = Modifier,
) {
@@ -52,7 +54,19 @@ fun RecordDetailsSettings(
        color = MaterialTheme.colorScheme.surface,
        shape = RoundedCornerShape(28.dp),
    ) {
        Column(modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth()) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.padding(vertical = 12.dp).fillMaxWidth(),
        ) {
            CaptureTargetSelector(
                items = targetViewModel.items,
                selectedItemIndex = targetViewModel.selectedIndex,
                onItemSelected = { targetViewModel.select(it) },
                itemToString = { stringResource(it.labelRes) },
                isItemEnabled = { it.isSelectable },
                viewModel = drawableLoaderViewModel,
                modifier = Modifier.padding(vertical = 12.dp),
            )
            RichSwitch(
                icon =
                    loadIcon(
@@ -61,8 +75,8 @@ fun RecordDetailsSettings(
                        contentDescription = null,
                    ),
                label = stringResource(R.string.screen_record_record_device_audio_label),
                checked = viewModel.shouldRecordDevice,
                onCheckedChange = { viewModel.shouldRecordDevice = it },
                checked = parametersViewModel.shouldRecordDevice,
                onCheckedChange = { parametersViewModel.shouldRecordDevice = it },
                modifier = Modifier,
            )
            RichSwitch(
@@ -73,8 +87,8 @@ fun RecordDetailsSettings(
                        contentDescription = null,
                    ),
                label = stringResource(R.string.screen_record_record_microphone_label),
                checked = viewModel.shouldRecordMicrophone,
                onCheckedChange = { viewModel.shouldRecordMicrophone = it },
                checked = parametersViewModel.shouldRecordMicrophone,
                onCheckedChange = { parametersViewModel.shouldRecordMicrophone = it },
                modifier = Modifier,
            )
            RichSwitch(
@@ -85,8 +99,8 @@ fun RecordDetailsSettings(
                        contentDescription = null,
                    ),
                label = stringResource(R.string.screen_record_should_show_camera_label),
                checked = viewModel.shouldShowFrontCamera == true,
                onCheckedChange = { viewModel.setShouldShowFrontCamera(it) },
                checked = parametersViewModel.shouldShowFrontCamera == true,
                onCheckedChange = { parametersViewModel.setShouldShowFrontCamera(it) },
                modifier = Modifier,
            )
            RichSwitch(
@@ -97,8 +111,8 @@ fun RecordDetailsSettings(
                        contentDescription = null,
                    ),
                label = stringResource(R.string.screen_record_should_show_touches_label),
                checked = viewModel.shouldShowTaps == true,
                onCheckedChange = { viewModel.setShouldShowTaps(it) },
                checked = parametersViewModel.shouldShowTaps == true,
                onCheckedChange = { parametersViewModel.setShouldShowTaps(it) },
                modifier = Modifier,
            )
        }
+2 −1
Original line number Diff line number Diff line
@@ -143,7 +143,8 @@ constructor(private val viewModelFactory: SmallScreenCaptureRecordViewModel.Fact
                        }
                        RecordDetailsPopupType.Settings ->
                            RecordDetailsSettings(
                                viewModel = viewModel.recordDetailsParametersViewModel,
                                parametersViewModel = viewModel.recordDetailsParametersViewModel,
                                targetViewModel = viewModel.recordDetailsTargetViewModel,
                                drawableLoaderViewModel = viewModel,
                                modifier = contentModifier,
                            )
Loading