composeNav
一、Comopose navigation 有三样东西:
1、NavHost -> 船
2、NavController -> 舵手
3、route -> 航线
https://developer.android.google.cn/courses/pathways/android-basics-compose-unit-4-pathway-2?hl=zh-cn
/*
* Copyright (C) 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
*
* https://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.example.cupcake
import android.content.Context
import android.content.Intent
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.example.cupcake.ui.OrderViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import com.example.cupcake.data.DataSource
import com.example.cupcake.ui.OrderSummaryScreen
import com.example.cupcake.ui.SelectOptionScreen
import com.example.cupcake.ui.StartOrderScreen
/**
* Composable that displays the topBar and displays back button if back navigation is possible.
*/
@Composable
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
TopAppBar(
//zsg 给每一个导航页设置名称
title = { Text(stringResource(currentScreen.title)) },
colors = TopAppBarDefaults.mediumTopAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
modifier = modifier,
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
}
)
}
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel(),
//zsg 这个NavController 是直接拿框架中的就可以了
//zsg nav 2 浆
navController: NavHostController = rememberNavController()
) {
/**
* //zsg 给每一个导航页设置名称
* 仅当返回堆栈上有可组合项时才应显示向上按钮。如果应用在返回堆栈上没有任何屏幕(显示 StartOrderScreen),
* 则不应显示向上按钮。如需检查这一点,您需要建立对返回堆栈的引用。
*/
//zsg 从navController 获取当前是哪一个页面
val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
Scaffold(
topBar = {
CupcakeAppBar(
currentScreen = currentScreen,
//zsg 判断是否还有上一页
canNavigateBack = navController.previousBackStackEntry != null,
//zsg 返回上一页
navigateUp = { navController.navigateUp() }
)
}
) { innerPadding ->
//zsg 这个uiState是viewModel 在CupcakeScreen.Start.name 设置后,在CupcakeScreen.Flavor.name 用
val uiState by viewModel.uiState.collectAsState()
//zsg nav 1 船
NavHost(navController = navController,
//zsg 设置第一页是CupcakeScreen.Start.name
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)){
//zsg 一个composable 就是一个页面
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {
viewModel.setQuantity(it)
//zsg nav 3 导航
navController.navigate(CupcakeScreen.Flavor.name)
},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
onCancelButtonClicked = { cancelOrderAndNavigateToStart(viewModel, navController) },
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
onCancelButtonClicked = { cancelOrderAndNavigateToStart(viewModel, navController) },
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Summary.name) {
val context = LocalContext.current
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = { cancelOrderAndNavigateToStart(viewModel, navController) },
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
},
modifier = Modifier.fillMaxHeight()
)
}
}
}
}
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
//zsg inclusive = true 移除所有,false 显示第一个起始页
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
private fun shareOrder(context: Context, subject: String, summary: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
}
//zsg kotlin 枚举与java类似 每一个成员 如Start 就是一个CupcakeScreen
enum class CupcakeScreen(@StringRes val title: Int) {
Start(title = R.string.app_name),
Flavor(title = R.string.choose_flavor),
Pickup(title = R.string.choose_pickup_date),
Summary(title = R.string.order_summary)
}
/*
* Copyright (C) 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
*
* https://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.example.cupcake.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.example.cupcake.R
import com.example.cupcake.ui.components.FormattedPriceLabel
import com.example.cupcake.ui.theme.CupcakeTheme
/**
* Composable that displays the list of items as [RadioButton] options,
* [onSelectionChanged] lambda that notifies the parent composable when a new value is selected,
* [onCancelButtonClicked] lambda that cancels the order when user clicks cancel and
* [onNextButtonClicked] lambda that triggers the navigation to next screen
*/
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
onNextButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
) {
var selectedValue by rememberSaveable { mutableStateOf("") }
Column(
modifier = modifier,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))) {
options.forEach { item ->
Row(
//zsg modifier 设置点击监听
modifier = Modifier.selectable(
selected = selectedValue == item,
onClick = {
selectedValue = item
onSelectionChanged(item)
}
),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedValue == item,
onClick = {
selectedValue = item
onSelectionChanged(item)
}
)
Text(item)
}
}
Divider(
thickness = dimensionResource(R.dimen.thickness_divider),
modifier = Modifier.padding(bottom = dimensionResource(R.dimen.padding_medium))
)
FormattedPriceLabel(
subtotal = subtotal,
modifier = Modifier
.align(Alignment.End)
.padding(
top = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_medium)
)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_medium)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.Bottom
) {
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
Button(
modifier = Modifier.weight(1f),
// the button is enabled when the user makes a selection
enabled = selectedValue.isNotEmpty(),
onClick = onNextButtonClicked
) {
Text(stringResource(R.string.next))
}
}
}
}
@Preview
@Composable
fun SelectOptionPreview() {
CupcakeTheme {
SelectOptionScreen(
subtotal = "299.99",
options = listOf("Option 1", "Option 2", "Option 3", "Option 4"),
modifier = Modifier.fillMaxHeight()
)
}
}
/*
* Copyright (C) 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
*
* https://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.example.cupcake.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.cupcake.R
import com.example.cupcake.data.DataSource
import com.example.cupcake.ui.theme.CupcakeTheme
/**
* Composable that allows the user to select the desired cupcake quantity and expects
* [onNextButtonClicked] lambda that expects the selected quantity and triggers the navigation to
* next screen
*/
@Composable
fun StartOrderScreen(
quantityOptions: List<Pair<Int, Int>>,
onNextButtonClicked: (Int) -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_small))
) {
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_medium)))
Image(
painter = painterResource(R.drawable.cupcake),
contentDescription = null,
modifier = Modifier.width(300.dp)
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_medium)))
Text(
text = stringResource(R.string.order_cupcakes),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(dimensionResource(R.dimen.padding_small)))
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
dimensionResource(id = R.dimen.padding_medium)
)
) {
//zsg 这里用forEach创建了三个按钮
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
//zsg 这里是第一页设置点击的地方 都是导航到CupcakeScreen.Flavor.name
onClick = {onNextButtonClicked(item.second)}
)
}
}
}
}
/**
* Customizable button composable that displays the [labelResourceId]
* and triggers [onClick] lambda when this composable is clicked
*/
@Composable
fun SelectQuantityButton(
@StringRes labelResourceId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier.widthIn(min = 250.dp)
) {
Text(stringResource(labelResourceId))
}
}
@Preview
@Composable
fun StartOrderPreview() {
CupcakeTheme {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}