| /* |
| * Copyright 2023 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 androidx.compose.animation.demos.lookahead |
| |
| import androidx.compose.animation.core.Animatable |
| import androidx.compose.animation.core.TweenSpec |
| import androidx.compose.foundation.background |
| import androidx.compose.foundation.border |
| import androidx.compose.foundation.clickable |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.Column |
| import androidx.compose.foundation.layout.fillMaxHeight |
| 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.wrapContentSize |
| import androidx.compose.foundation.lazy.LazyColumn |
| import androidx.compose.foundation.shape.CircleShape |
| import androidx.compose.foundation.shape.CutCornerShape |
| import androidx.compose.foundation.shape.RoundedCornerShape |
| import androidx.compose.material.BackdropScaffold |
| import androidx.compose.material.BackdropValue |
| import androidx.compose.material.BottomAppBar |
| import androidx.compose.material.ExperimentalMaterialApi |
| import androidx.compose.material.ExtendedFloatingActionButton |
| import androidx.compose.material.FabPosition |
| import androidx.compose.material.Icon |
| import androidx.compose.material.IconButton |
| import androidx.compose.material.ListItem |
| import androidx.compose.material.MaterialTheme |
| import androidx.compose.material.Scaffold |
| import androidx.compose.material.ScrollableTabRow |
| import androidx.compose.material.Snackbar |
| import androidx.compose.material.SnackbarHost |
| import androidx.compose.material.Tab |
| import androidx.compose.material.Text |
| import androidx.compose.material.TopAppBar |
| import androidx.compose.material.icons.Icons |
| import androidx.compose.material.icons.filled.Close |
| import androidx.compose.material.icons.filled.Favorite |
| import androidx.compose.material.icons.filled.Menu |
| import androidx.compose.material.rememberBackdropScaffoldState |
| import androidx.compose.material.rememberScaffoldState |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.LaunchedEffect |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.produceState |
| import androidx.compose.runtime.remember |
| import androidx.compose.runtime.rememberCoroutineScope |
| import androidx.compose.runtime.setValue |
| import androidx.compose.ui.ExperimentalComposeUiApi |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.layout.LookaheadScope |
| import androidx.compose.ui.tooling.preview.Preview |
| import androidx.compose.ui.unit.dp |
| import kotlin.math.abs |
| import kotlin.math.roundToInt |
| import kotlinx.coroutines.delay |
| import kotlinx.coroutines.launch |
| |
| private val colors = listOf( |
| Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84) |
| ) |
| |
| @OptIn(ExperimentalComposeUiApi::class) |
| @Preview |
| @Composable |
| fun LookaheadWithScaffold() { |
| val hasPadding by produceState(initialValue = true) { |
| while (true) { |
| delay(3000) |
| value = !value |
| } |
| } |
| LookaheadScope { |
| Box( |
| Modifier |
| .fillMaxHeight() |
| .background(Color.Gray) |
| .animateBounds( |
| if (hasPadding) Modifier.padding(bottom = 300.dp) else Modifier |
| ) |
| ) { |
| var state by remember { mutableStateOf(0) } |
| val titles = listOf( |
| "SimpleScaffold", "W/Cutout", "SimpleSnackbar", "CustomSnackbar", "Backdrop" |
| ) |
| Column { |
| ScrollableTabRow( |
| selectedTabIndex = state, |
| ) { |
| titles.forEachIndexed { index, title -> |
| Tab( |
| selected = state == index, |
| onClick = { state = index }, |
| text = { Text(title) } |
| ) |
| } |
| } |
| when (state) { |
| 0 -> SimpleScaffoldWithTopBar() |
| 1 -> ScaffoldWithBottomBarAndCutout() |
| 2 -> ScaffoldWithSimpleSnackbar() |
| 3 -> ScaffoldWithCustomSnackbar() |
| 4 -> BackdropScaffoldSample() |
| } |
| } |
| } |
| } |
| } |
| |
| @OptIn(ExperimentalComposeUiApi::class) |
| @Composable |
| fun SimpleScaffoldWithTopBar() { |
| val scaffoldState = rememberScaffoldState() |
| val scope = rememberCoroutineScope() |
| Scaffold( |
| scaffoldState = scaffoldState, |
| drawerContent = { Text("Drawer content") }, |
| topBar = { |
| TopAppBar( |
| title = { Text("Simple Scaffold Screen") }, |
| navigationIcon = { |
| IconButton( |
| onClick = { |
| scope.launch { scaffoldState.drawerState.open() } |
| } |
| ) { |
| Icon(Icons.Filled.Menu, contentDescription = "Localized description") |
| } |
| } |
| ) |
| }, |
| floatingActionButtonPosition = FabPosition.End, |
| floatingActionButton = { |
| ExtendedFloatingActionButton( |
| text = { Text("Inc") }, |
| onClick = { /* fab click handler */ } |
| ) |
| }, |
| content = { innerPadding -> |
| Box( |
| Modifier |
| .fillMaxSize() |
| .background(Color.White) |
| ) |
| LazyColumn(contentPadding = innerPadding) { |
| items(count = 20) { |
| Box( |
| Modifier |
| .fillMaxWidth() |
| .height(150.dp) |
| .background(colors[it % colors.size]) |
| ) |
| } |
| } |
| } |
| ) |
| } |
| |
| @Composable |
| fun ScaffoldWithBottomBarAndCutout() { |
| val scaffoldState = rememberScaffoldState() |
| |
| // Consider negative values to mean 'cut corner' and positive values to mean 'round corner' |
| val sharpEdgePercent = -50f |
| val roundEdgePercent = 45f |
| // Start with sharp edges |
| val animatedProgress = remember { Animatable(sharpEdgePercent) } |
| // Create a coroutineScope for the animation |
| val coroutineScope = rememberCoroutineScope() |
| // animation value to animate shape |
| val progress = animatedProgress.value.roundToInt() |
| |
| // When progress is 0, there is no modification to the edges so we are just drawing a rectangle. |
| // This allows for a smooth transition between cut corners and round corners. |
| val fabShape = if (progress < 0) { |
| CutCornerShape(abs(progress)) |
| } else if (progress == roundEdgePercent.toInt()) { |
| CircleShape |
| } else { |
| RoundedCornerShape(progress) |
| } |
| // lambda to call to trigger shape animation |
| val changeShape: () -> Unit = { |
| val target = animatedProgress.targetValue |
| val nextTarget = if (target == roundEdgePercent) sharpEdgePercent else roundEdgePercent |
| coroutineScope.launch { |
| animatedProgress.animateTo( |
| targetValue = nextTarget, |
| animationSpec = TweenSpec(durationMillis = 600) |
| ) |
| } |
| } |
| |
| Scaffold( |
| scaffoldState = scaffoldState, |
| drawerContent = { Text("Drawer content") }, |
| topBar = { TopAppBar(title = { Text("Scaffold with bottom cutout") }) }, |
| bottomBar = { |
| BottomAppBar(cutoutShape = fabShape) { |
| IconButton( |
| onClick = { |
| coroutineScope.launch { scaffoldState.drawerState.open() } |
| } |
| ) { |
| Icon(Icons.Filled.Menu, contentDescription = "Localized description") |
| } |
| } |
| }, |
| floatingActionButton = { |
| ExtendedFloatingActionButton( |
| text = { Text("Change shape") }, |
| onClick = changeShape, |
| shape = fabShape |
| ) |
| }, |
| floatingActionButtonPosition = FabPosition.Center, |
| isFloatingActionButtonDocked = true, |
| content = { innerPadding -> |
| LazyColumn(contentPadding = innerPadding) { |
| items(count = 100) { |
| Box( |
| Modifier |
| .fillMaxWidth() |
| .height(50.dp) |
| .background(colors[it % colors.size]) |
| ) |
| } |
| } |
| } |
| ) |
| } |
| |
| @Composable |
| fun ScaffoldWithSimpleSnackbar() { |
| val scaffoldState = rememberScaffoldState() |
| val scope = rememberCoroutineScope() |
| Scaffold( |
| scaffoldState = scaffoldState, |
| floatingActionButton = { |
| var clickCount by remember { mutableStateOf(0) } |
| ExtendedFloatingActionButton( |
| text = { Text("Show snackbar") }, |
| onClick = { |
| // show snackbar as a suspend function |
| scope.launch { |
| scaffoldState.snackbarHostState.showSnackbar("Snackbar # ${++clickCount}") |
| } |
| } |
| ) |
| }, |
| content = { innerPadding -> |
| Text( |
| text = "Body content", |
| modifier = Modifier |
| .padding(innerPadding) |
| .fillMaxSize() |
| .wrapContentSize() |
| ) |
| } |
| ) |
| } |
| |
| @Composable |
| fun ScaffoldWithCustomSnackbar() { |
| val scaffoldState = rememberScaffoldState() |
| val scope = rememberCoroutineScope() |
| Scaffold( |
| scaffoldState = scaffoldState, |
| snackbarHost = { |
| // reuse default SnackbarHost to have default animation and timing handling |
| SnackbarHost(it) { data -> |
| // custom snackbar with the custom border |
| Snackbar( |
| modifier = Modifier.border(2.dp, MaterialTheme.colors.secondary), |
| snackbarData = data |
| ) |
| } |
| }, |
| floatingActionButton = { |
| var clickCount by remember { mutableStateOf(0) } |
| ExtendedFloatingActionButton( |
| text = { Text("Show snackbar") }, |
| onClick = { |
| scope.launch { |
| scaffoldState.snackbarHostState.showSnackbar("Snackbar # ${++clickCount}") |
| } |
| } |
| ) |
| }, |
| content = { innerPadding -> |
| Text( |
| text = "Custom Snackbar Demo", |
| modifier = Modifier |
| .padding(innerPadding) |
| .fillMaxSize() |
| .wrapContentSize() |
| ) |
| } |
| ) |
| } |
| |
| @Preview |
| @Composable |
| @OptIn(ExperimentalMaterialApi::class) |
| fun BackdropScaffoldSample() { |
| val scope = rememberCoroutineScope() |
| val selection = remember { mutableStateOf(1) } |
| val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed) |
| LaunchedEffect(scaffoldState) { |
| scaffoldState.reveal() |
| } |
| BackdropScaffold( |
| scaffoldState = scaffoldState, |
| appBar = { |
| TopAppBar( |
| title = { Text("Backdrop scaffold") }, |
| navigationIcon = { |
| if (scaffoldState.isConcealed) { |
| IconButton(onClick = { scope.launch { scaffoldState.reveal() } }) { |
| Icon(Icons.Default.Menu, contentDescription = "Localized description") |
| } |
| } else { |
| IconButton(onClick = { scope.launch { scaffoldState.conceal() } }) { |
| Icon(Icons.Default.Close, contentDescription = "Localized description") |
| } |
| } |
| }, |
| actions = { |
| var clickCount by remember { mutableStateOf(0) } |
| IconButton( |
| onClick = { |
| // show snackbar as a suspend function |
| scope.launch { |
| scaffoldState.snackbarHostState |
| .showSnackbar("Snackbar #${++clickCount}") |
| } |
| } |
| ) { |
| Icon(Icons.Default.Favorite, contentDescription = "Localized description") |
| } |
| }, |
| elevation = 0.dp, |
| backgroundColor = Color.Transparent |
| ) |
| }, |
| backLayerContent = { |
| LazyColumn { |
| items(if (selection.value >= 3) 3 else 5) { |
| ListItem( |
| Modifier.clickable { |
| selection.value = it |
| scope.launch { scaffoldState.conceal() } |
| }, |
| text = { Text("Select $it") } |
| ) |
| } |
| } |
| }, |
| frontLayerContent = { |
| Text("Selection: ${selection.value}") |
| LazyColumn { |
| items(50) { |
| ListItem( |
| text = { Text("Item $it") }, |
| icon = { |
| Icon( |
| Icons.Default.Favorite, |
| contentDescription = "Localized description" |
| ) |
| } |
| ) |
| } |
| } |
| } |
| ) |
| } |