How to implement BottomSheet in Material 3 Jetpack Compose Android - android

I know how to implement BottomSheet in Material 2 Jetpack Compose using BottomSheetScaffold.
But there is no BottomSheetScaffold in Material 3. Also, there is nothing in official samples about BottomSheet.

So I was able to make it work!
It seems that, as of today, BottomSheetScaffold is not available yet on Material3, this is discussed in this issue I found digging around: https://issuetracker.google.com/issues/229839039
I quote the important part from the reply of a google dev:
we aren't in a super easy spot with Swipeable. It currently has a number of critical bugs that need to be addressed first (we are working on this), which is why we are limiting the surface we are exposing for Swipeable in M3 for the time. Our plan for the coming months is to focus on this specific area and improve developer experience.
Material 3 for Jetpack Compose is still in alpha - this means we
consider components production-ready, but the API shape is flexible
while in alpha. This gives us space to iterate while getting
real-world feedback from developers, which ultimately helps improve
your experience. Copy-pasting source code for components that are not
(fully) implemented or exposed in an alpha version can be a good thing
to do in the meantime! Owning the source code while the API shape is
still flexible gives you a number of benefits like ease of updating
dependencies, even if the APIs change, and allows you to evolve your
components in your own pace.
So I just followed the advice and I copy pasted BottomSheetScaffold into my project. Of course it did not work straight away because of a few missing classes and some minor API changes. At the end I was able to make it work by pulling and hacking the following classes and adding them to my project:
BottomSheetScaffold.kt
Drawer.kt
Strings.kt
Swipeable.kt
I have created a gist with the source code if you want to try:
https://gist.github.com/Marlinski/0b043968c2f574d70ee6060aeda54882
You will have to change the import to make it work on your project as well as add the "-Xjvm-default=all" option by adding the following into your gradle file in the android{} section:
android{
...
kotlinOptions {
freeCompilerArgs += ["-Xjvm-default=all"]
// "-Xjvm-default=all" option added because of this error:
// ... Inheritance from an interface with '#JvmDefault' members is only allowed with -Xjvm-default option
// triggered by porting BottomSheetScaffold for Material3 on Swipeable.kt:844
}
}
It works very well for me, will keep this solution until it is officially supported in material3.
Hope it helps!

I got pretty similar results using a fullscreen dialog with AnimatedVisibility, here is the code if interested:
// Visibility state for the dialog which will trigger it only once when called
val transitionState = remember {
MutableTransitionState(false).apply {
targetState = true
}
}
Dialog(
onDismissRequest = {} // You can set a visibility state variable to false in here which will close the dialog when clicked outside its bounds, no reason to when full screen though,
properties = DialogProperties(
// This property makes the dialog full width of the screen
usePlatformDefaultWidth = false
)
) {
// Visibility animation, more information in android docs if needed
AnimatedVisibility(
visibleState = transitionState,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = ...
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = ...
)
)
) {
Box(
modifier = Modifier.fillMaxSize().background(color = ...)
) {
// Your layout
// This can be any user interraction that closes the dialog
Button(
transitionState.apply { targetState = false }
) ...
}
}
All of this is in a composable function that gets called when a UI action to open said dialog is performed, it's not ideal but it works.
Hope I was able to help!

There is already a great answer by Marlinski, but i would like to add that there is also a ModalBottomSheetLayout which also does not have any implementation for Material 3.
I created a gist for people who need it in use right now:
https://gist.github.com/Pasha831/bdedcfee01acdc96cf3ae643da64f88a

We finally have ModalBottomSheet in Material3.
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
val bottomSheetState = rememberSheetState(skipHalfExpanded = true)
// Sheet content
if (openBottomSheet) {
ModalBottomSheet(
onDismissRequest = { openBottomSheet = false },
sheetState = bottomSheetState,
) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
// Note: If you provide logic outside of onDismissRequest to remove the sheet,
// you must additionally handle intended state cleanup, if any.
onClick = {
scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
if (!bottomSheetState.isVisible) {
openBottomSheet = false
}
}
}
) {
Text("Hide Bottom Sheet")
}
}
}
}
For more link here: https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ModalBottomSheet(kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.material3.SheetState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Color,kotlin.Function0,kotlin.Function1)

Related

Animating between three component states in Jetpack Compose

I am familiar with the Transition API in Jetpack Compose that lets me easily animate between two states inside a Compose component.
But what would be the best way to animate between three different states?
Consider as an example a loading indicator for a component. The states could be NotLoading, Loading and HasLoaded. My thinking here would be to have a loading indicator for a component and transition between those states:
Transition for showing the loading indicator: NotLoading -> Loading
Transition for showing the data: Loading -> HasLoaded
I guess what kind of transition doesn't really matter but I was thinking first fade in loading indicator then fade out loading indicator and fade in content. But this is just an example; in reality I need to specify the transition parameters.
What would be the best way to achieve this with Jetpack Compose? Not sure if my state thinking here is the best approach for this either.
You can use the Transition API with more than 2 states - and define the individual properties of each component using animate*AsState APIs.
There is another option if you have completely different Composables, you can use the AnimatedContent APIs.
For example, the below sample uses an enum UiState, and a button to change between the states. The content is then wrapped inside the AnimatedContent() composable. By default, the initial content fades out and then the target content fades in (this behavior is called fade through).
#Composable
fun AnimatedContentStateExample() {
Column {
var state by remember { mutableStateOf(UiState.Loading) }
Button(onClick = {
state = when (state) {
UiState.Loading -> UiState.Loaded
UiState.Loaded -> UiState.Empty
UiState.Empty -> UiState.Loading
}
}) {
Text("Switch States")
}
AnimatedContent(
targetState = state
) { targetState ->
// Make sure to use `targetState`, not `state`.
when (targetState) {
UiState.Loading -> {
CircularProgressIndicator()
}
UiState.Loaded -> {
Box(
Modifier
.background(androidGreen)
.size(100.dp))
Text("Loaded")
}
UiState.Empty -> {
Box(
Modifier
.background(androidBlue)
.size(200.dp))
Text("Empty")
}
}
}
}
}
You can customize this animation behavior by specifying a ContentTransform object to the transitionSpec parameter. You can create ContentTransform by combining an EnterTransition with an ExitTransition using the with infix function. You can apply SizeTransform to the ContentTransform by attaching it with the using infix function.
More information about AnimatedContent can be found here: https://developer.android.com/jetpack/compose/animation#animatedcontent.
And samples of its usage here: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/animation/animation/samples/src/main/java/androidx/compose/animation/samples/AnimatedContentSamples.kt

Android Jetpack Compose Multiple Layer Navigation

I have multiple destination,and some of the destinations also have subsidiary destinations represented by a Navigation component to interact with.
NavigationBar {
page1 {
NavigationBar {
innerPager1
innerPager2
.....
}
}
page2 {....}
}
I want only the part excluding the navigationBar (innerPager1 & 2) switching not the entire page (page1) , is this possible with navController? if possible how to achieve?
I'm not sure if i fully understood your question but i would start from looking for the documents about Scaffold component. As you will see, you can integrate, among others, a BottomBar component that is the one you're looking for. To switch only the innerPage content you need to change the innerPadding content. This is a general schema:
#Composable
fun MyLayout() {
Scaffold(
bottomBar = { MyBottomBar() }
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
There are many possible approaches to change the BodyContent. I'm going to cite a couple of them:
using State to switch the #Composable method called:
var selectedTab by rememberSaveable { mutableStateOf(BottomNavItemTab.TAB_1) }
...
...
innerPadding ->
Box(modifier = Modifier.padding(0.dp, 0.dp, 0.dp, innerPadding.calculateBottomPadding())) {
when (selectedTab) {
BottomNavItemTab.TAB_1 -> Tab1_Screen()
BottomNavItemTab.TAB_2 -> Tab2_Screen()
BottomNavItemTab.TAB_3 -> Tab3_Screen()
}
}
using Navigation component. In this case i suggest you to read the official docs because they provide well detailed example.
Please keep in mind that you are not forced to use only one of these solutions but you can integrate both of them. I'm saying this because it's not trivial to achieve navigation for tabs with and without BottomBar together.

Unable to Focus Anything other than TextField

I have already viewed other posts on the site. They suggest using the focusRequestor modifier and I've tried that.
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
Text(
modifier = Modifier
.focusable()
.focusTarget() //Tried Adding/Removing
.onFocusChanged {
if (it.hasFocus || it.isFocused) {
Log.i("Test", "Text Focused")
}
} // Tried Adding Visual Elements to delete it as well.
.focusRequester(focusRequester),
text = "MARSK"
)
LaunchedEffect(scope) {
focusRequester.requestFocus()
}
I'm taking Text in this example, but actually I need to implement this on Canvas. This approach is not working on Box too. I'm afraid the same is the case for other containers as well.
For background, this is being built for my TV, so I wish to make this element clickable by the OK button on the remote's D-Pad. My approach is to make it focused first, then assuming that once it is focused, it will automatically detect the press of the OK button as the 'click'. If there is something wrong with my approach, feedback for improvements is also welcome.
PS: The solution by Abhimanyu works like a charm on Mobile Devices. However, since I mentioned a TV above, a TV is the device in consideration. On the TV, I have to press a button (Any button on the DPAD works, even the OK button, strangely) to get it focused up. Any idea how to get round this issue?
Thanks
Issue
The onFocusChanged should be added BEFORE the focusTarget or focusable that is being observed.
Source
Changes
Remove focusTarget and move onFocusChanged before focusable.
Also, note that focusRequester must be before focusable.
This would hopefully work for all comopsables. I have tested using Text and the example from Docs was using Box.
Extra details.
Prefer focusable over focusTarget.
From focusTarget docs,
Note: This is a low level modifier. Before using this consider using Modifier.focusable(). It uses a focusTarget in its implementation. Modifier.focusable() adds semantics that are needed for accessibility.
Order of Modifiers matter
Layouts in Jetpack Compose Codelab
Check Order Matters section
order matters when chaining modifiers as they're applied to the composable they modify from earlier to later,
Sample code
#Composable
fun FocusableText() {
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
var color by remember { mutableStateOf(White) }
LaunchedEffect(scope) {
focusRequester.requestFocus()
}
Text(
modifier = Modifier
.background(color)
.onFocusChanged {
color = if (it.hasFocus || it.isFocused) {
LightGray
} else {
White
}
}
.focusRequester(focusRequester)
.focusable(),
text = "Test Text"
)
}
The Text background is LightGray which indicates the text is focused.

Jetpack Compose LazyList not showing items correctly

world!
I've been playing with Jetpack Compose for a while, and I've had really good results.
However, I found this problem: I have a list of items, and I use a LazyColumn to show them to the user. But when I scroll up and down, some items are not displayed correctly and they show up like there is no information in them.
I don't know if I'm missing something or this behavior will be corrected in the future.
How it looks
My code:
LazyColumn(
modifier = modifier
.fillMaxHeight(),
) {
items(expenses, Expense::id) { expense ->
ExpenseItem(expense = expense, areExpensesSelectable,
onExpenseLongClick = { onExpenseLongClick?.invoke() },
onExpenseSelected = { onExpenseSelected?.invoke(it) },
onExpenseDeselected = { onExpenseDeselected?.invoke(it) })
}
}
Self response: this was a bug present in Jetpack Compose until fixed in version beta08.
I hear this bug is fixed in the latest version of Compose.

Jetpack Compose expanding BottomSheet of BottomSheetScaffold only works after recomposition

I am trying to have a way to show a BottomSheet from everywhere within my app, for that I use the BottomSheetScaffold and a LiveData object that holds the current composable function which is observed as state:
val sheetContent by MusicHub.state.bottomSheet.getContent().observeAsState()
BottomSheetScaffold(
modifier = Modifier
.fillMaxSize(),
scaffoldState = bottomSheetScaffoldState,
sheetPeekHeight = 0.dp,
sheetContent = sheetContent!!
)
I use the BottomSheet as a context menu in my app. For example when i longClick on a playlist in my app it sets the content of the BottomSheet and shows it like this:
PlaylistItem(
// ...
onLongClick = {
// Set the LiveData composable
MusicHub.state.bottomSheet.setContent {
PlaylistContextMenuTest(playlist!!, viewModel)
}
// Expand BottomSheet
scope.launch {
MusicHub.state.bottomSheet.expand()
}
}
)
In general this works but the first time the BottomSheet gets expanded it shows for a split second before it disappears at the bottom again. Here is a small GIF:
My guess is that somehow the size of the BottomSheet is not recalculated yet and hence it only works in the next recomposition. Coming from web dev i would say its a typical case of requestAnimationFrame but i don't quite know how to solve this issue in compose.
Edit:
PlaylistContextMenuTest code:
#Composable
fun PlaylistContextMenuTest(playlist: Playlist, viewModel: LibraryViewModel = activityViewModel()){
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
// TODO: Replace that with a percentage of the screen height
.heightIn(max = 384.dp)
.verticalScroll(scrollState),
content = {
ContextMenu {
repeat(4){
addOption(R.drawable.ic_delete_black_24dp, "Delete"){
Timber.d("Delete Playlist")
viewModel.deletePlaylist(playlist)
}
}
}
}
)
}
Full ContextMenu source: (https://pastebin.com/sg4ed96L)
You seem to be right. The first time it seems to be re-calculating the BottomSheet's height.
As this does not change on the second try, it will then be displayed.
Does this answer help you?
It seems to work out quite okay for BottomSheetScaffold, but not for ModalBottomSheetLayout (the thing I was looking for as you can see in the comments of the linked answer).
Edit: Accompanist's Navigation Material is also worth a look.
This issue has been resolved with a new release of jetpack compose. I am not sure if it was beta 8 or 9 but everything works as intended now.

Categories

Resources