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

Commit 030d8882 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add stub app selection carousel" into main

parents 9bbaf778 428d0a42
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -348,6 +348,9 @@
    <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
    <string name="screenrecord_stop_dialog_button">Stop recording</string>

    <!-- Header for the carousel to choose an app for screen recording. [CHAR LIMIT=50] -->
    <string name="screen_record_capture_target_choose_app">Choose an app to record</string>

    <!-- Button text in the region select box for triggering a screenshot of the selected area. [CHAR LIMIT=35] -->
    <string name="screen_capture_region_selection_button">Capture selected area</string>
    <!-- Button text in the toolbar for choosing the screenshot type. [CHAR LIMIT=35] -->
+124 −1
Original line number Diff line number Diff line
@@ -14,9 +14,132 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalMaterial3Api::class)

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

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.carousel.CarouselDefaults
import androidx.compose.material3.carousel.CarouselItemScope
import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
import androidx.compose.material3.carousel.rememberCarouselState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformIconButton
import com.android.systemui.res.R
import com.android.systemui.screencapture.record.smallscreen.ui.viewmodel.RecordDetailsAppSelectorViewModel
import com.android.systemui.screencapture.record.smallscreen.ui.viewmodel.RecordDetailsAppViewModel

@Composable
fun RecordDetailsAppSelector(
    viewModel: RecordDetailsAppSelectorViewModel,
    onBackPressed: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(4.dp),
        modifier = modifier.padding(bottom = 32.dp),
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth().height(64.dp).padding(horizontal = 8.dp),
        ) {
            PlatformIconButton(
                onClick = onBackPressed,
                iconResource = R.drawable.ic_arrow_back,
                contentDescription = stringResource(R.string.accessibility_back),
                colors =
                    IconButtonDefaults.iconButtonColors(
                        contentColor = MaterialTheme.colorScheme.onSurface
                    ),
                modifier = Modifier.size(48.dp),
            )
            Text(
                text = stringResource(R.string.screen_record_capture_target_choose_app),
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.onSurface,
            )
        }

        val carouselState = rememberCarouselState { viewModel.apps.size }
        HorizontalUncontainedCarousel(
            state = carouselState,
            itemWidth = 168.dp,
            itemSpacing = 24.dp,
            contentPadding = PaddingValues(horizontal = 32.dp),
            flingBehavior = CarouselDefaults.singleAdvanceFlingBehavior(carouselState),
            modifier = Modifier,
        ) { index ->
            val appViewModel = viewModel.apps[index]
            AppPreview(viewModel = appViewModel, modifier = Modifier)
        }
    }
}

@Composable
private fun CarouselItemScope.AppPreview(
    viewModel: RecordDetailsAppViewModel,
    modifier: Modifier = Modifier,
    cornersRadius: Dp = 16.dp,
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier =
            modifier
                .maskClip(RoundedCornerShape(cornersRadius))
                .clickable(onClick = viewModel.onSelect),
    ) {
        Icon(
            bitmap = viewModel.icon.asImageBitmap(),
            contentDescription = null,
            modifier = Modifier.size(18.dp),
        )

@Composable fun RecordDetailsAppSelector(modifier: Modifier = Modifier) {}
        AnimatedContent(
            targetState = viewModel.thumbnail,
            contentAlignment = Alignment.Center,
            transitionSpec = { fadeIn() togetherWith fadeOut() },
            modifier =
                Modifier.clip(RoundedCornerShape(cornersRadius)).aspectRatio(9 / 16f).fillMaxSize(),
        ) { thumbnail ->
            if (thumbnail == null) {
                Spacer(
                    modifier =
                        Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerHigh)
                )
            } else {
                Image(bitmap = thumbnail.asImageBitmap(), contentDescription = null)
            }
        }
    }
}
+5 −1
Original line number Diff line number Diff line
@@ -72,7 +72,11 @@ constructor(private val viewModelFactory: SmallScreenCaptureRecordViewModel.Fact
                            /* show nothing */
                        }
                        RecordDetailsPopupType.Settings -> RecordDetailsSettings()
                        RecordDetailsPopupType.AppSelector -> RecordDetailsAppSelector()
                        RecordDetailsPopupType.AppSelector ->
                            RecordDetailsAppSelector(
                                viewModel = viewModel.recordDetailsAppSelectorViewModel,
                                onBackPressed = { viewModel.showSettings() },
                            )
                        RecordDetailsPopupType.MarkupColorSelector ->
                            RecordDetailsMarkupColorSelector()
                    }
+59 −0
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.smallscreen.ui.viewmodel

import android.content.ComponentName
import android.graphics.Bitmap
import androidx.collection.LruCache
import com.android.systemui.lifecycle.HydratedActivatable
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

private data class AppModel(val app: ComponentName, val icon: Bitmap)

class RecordDetailsAppSelectorViewModel @AssistedInject constructor() : HydratedActivatable() {

    private val thumbnailsCache: LruCache<ComponentName, Bitmap> = LruCache(3)
    val apps: List<RecordDetailsAppViewModel> =
        emptyList<AppModel>().map { appModel ->
            RecordDetailsAppViewModel(
                icon = appModel.icon,
                onSelect = { onAppSelected(appModel.app) },
                loadThumbnail = {
                    thumbnailsCache[appModel.app]
                        ?: loadThumbnail(appModel.app).also { loadedThumbnail ->
                            thumbnailsCache.put(appModel.app, loadedThumbnail)
                        }
                },
            )
        }

    private fun onAppSelected(app: ComponentName) {}

    override suspend fun onActivated() {
        super.onActivated()
    }

    private suspend fun loadThumbnail(app: ComponentName): Bitmap {
        TODO("Not implemented yet")
    }

    @AssistedFactory
    interface Factory {
        fun create(): RecordDetailsAppSelectorViewModel
    }
}
+37 −0
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.smallscreen.ui.viewmodel

import android.graphics.Bitmap
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.android.systemui.lifecycle.HydratedActivatable

class RecordDetailsAppViewModel(
    val icon: Bitmap,
    val onSelect: () -> Unit,
    private val loadThumbnail: suspend () -> Bitmap,
) : HydratedActivatable() {

    var thumbnail: Bitmap? by mutableStateOf(null)
        private set

    override suspend fun onActivated() {
        thumbnail = loadThumbnail()
    }
}
Loading