I need to make tabs with Jetpack Compose, looking like horizontal buttons. Tabs should be left aligned, and not centered. Just like in the image.
Also selected tab shouldn't show underline.
Jetpack compose has Scaffold for such case, something like this should work for you
enum class Tab {
Day,
Week,
Month,
}
#Composable
fun TestView(
) {
var selectedTab by remember { mutableStateOf(Tab.Day) }
Scaffold(topBar = {
Row(Modifier.padding(5.dp)) {
Tab.values().forEach { tab ->
BottomBarButton(
tab.name,
selected = selectedTab == tab,
onSelect = {
selectedTab = tab
},
)
}
}
}) {
when (selectedTab) {
Tab.Day -> Text("$selectedTab content")
Tab.Week -> Text("$selectedTab content")
Tab.Month -> Text("$selectedTab content")
}
}
}
#Composable
fun BottomBarButton(
text: String,
selected: Boolean,
onSelect: () -> Unit
) {
Text(
text,
modifier = Modifier
.background(
if (selected)
Color.Green
else
Color.Transparent
)
.clickable(onClick = onSelect)
.padding(10.dp)
)
}
If you need bottom bar, just replace topBar = { with bottomBar = {
See more about Scaffold
How about this one?
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = colorResource(id = R.color.white),
divider = { TabRowDefaults.Divider(color = colorResource(id = R.color.transparent)) },
edgePadding = 0.dp
) {
//draw your tab
}
Try this out
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
#Composable
fun RecentTabs(tabs: List<CustomTabs>, onSelectedTab: (CustomTabs) -> Unit) {
var selectedTab by remember { mutableStateOf("") }
#Composable
fun RecentTabItem(text: String, selectedColor: Color = Color.Green, onSelect: () -> Unit) {
val selected = text == selectedTab
Text(
text,
modifier = Modifier
.clip(CircleShape)
.background(
if (selected)
selectedColor
else
Color.Transparent
)
.clickable(
onClick = {
selectedTab = text
onSelect.invoke()
}
)
.padding(vertical = 8.dp, horizontal = 18.dp)
)
}
Row(
Modifier
.scrollable(rememberScrollState(), orientation = Orientation.Horizontal)
.padding(horizontal = 5.dp, vertical = 8.dp)) {
tabs.forEach {
RecentTabItem(text = it.name, selectedColor = it.color.toColor(Color.Magenta)) { onSelectedTab.invoke(it) }
Spacer(modifier = Modifier.width(5.dp))
}
}
}
Usage:
RecentTabs(tabs = listOf(
CustomeTabs(1,"Tab1", color = Color.Blue.toString()),
CustomeTabs(2,"Tab2", color = Color.Gray.toString()),
CustomeTabs(3,"Tab3", color = Color.Red.toString())
), onSelectedTab = {
Log.d(TAG, "RecentScreen() called ${it.toString()}")
})
Related
I want to add Divider after title. I tried to add Divider(), but it goes to above the text.
I am using Material 3 using implementation "androidx.compose.material3:material3:1.0.1"
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.letsgetchecked.app.common.DialogOptionsData
#Composable
fun <T> DialogOptionsView(
optionData: DialogOptionsData<T>,
) {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
title = {
Text(text = optionData.header)
Divider()
},
text = {
LazyColumn {
items(optionData.items) {
Text(text = "$it")
}
}
},
)
}
#Preview(showBackground = true)
#Composable
fun PreviewDialogOptions() {
val items = listOf(1, 2)
val dataItems = DialogOptionsData(header = "Header", items = items)
DialogOptionsView(dataItems)
}
Expected Output
Actual Output
It happens because the title attribute internally uses a Box as parent container.
Add a Column to achieve the expected result:
AlertDialog(
onDismissRequest = {},
confirmButton = {},
title = {
Column() {
Text(text = "header")
Divider()
}
},
If you use Dialog() composable instead of AlertDialog() you can get full width divider. Try this code
#Composable
fun <T> DialogOptionsView(optionData: DialogOptionsData<T>) {
Dialog(onDismissRequest = {}) {
Surface(shape = RoundedCornerShape(10.dp)) {
Column {
Text(
text = optionData.header,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp, 14.dp)
)
Divider()
LazyColumn(contentPadding = PaddingValues(16.dp, 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(optionData.items) {
Text(text = "$it")
}
}
}
}
}
}
and the result is
My current android applications main screen contains a androidx.compose.material.BackdropScaffold. The backdrop looks great and functions exactly as it says on the tin.
however I have an issue with the frontLayerContent which contains a list of items.
I allow the user to interact with the frontLayerContent list while the backdrop is revealed, the issue is with the backdrop in the revealed state the user cannot scroll down to see the last item in the frontLayerContent list.
The solution to this issue is to use backdropState.offset in the modifier of the frontLayerContent which I obtain as follows:-
val backdropState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
var offset by (backdropState.offset as MutableState)
and set as follows:-
onBackdropReveal = {
if (!backdropState.isAnimationRunning) {
scope.launch {
if (backdropState.isConcealed) {
offset = backdropState.offset.value
backdropState.reveal()
} else {
offset = backdropState.offset.value
backdropState.conceal()
}
}
}
}
and make the offset value available to other composables with:-
frontLayerContent = {
CompositionLocalProvider(LocalBackdropStateOffset provides offset) {
frontLayerContent()
}
}
Then in my frontLayerContent I retrieve the offset value as use as follows:-
val context = LocalContext.current
val offset = LocalBackdropStateOffset.current
Scaffold(
topBar = { Spacer(modifier = Modifier.height(0.dp)) },
modifier = Modifier.fillMaxSize(),
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.offset { IntOffset(x = 0, y = -offset.toInt()) },
verticalArrangement = Arrangement.Top
) {
LazyVerticalGrid(
modifier = Modifier.padding(10.dp),
columns = GridCells.Adaptive(125.dp),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(state.size) { index ->
this solution is close to what is required, however it looks as though I have made a mistake somewhere as my frontLayerContent list is always vertically offset even when the backdrop is concealed. In fact revealing or concealing the backdrop does not change the amount of offset of my frontLayerContent list.
how do I fix this issue?
how can I correctly set the frontLayerContent list vertical offset depending on whether the backdrop is concealed or revealed?
UPDATE
I only need to "fix" when the backdrop is revealed. therefore i need a conditional configuration of my frontLayerContent Modifier.offset(y = -offset)
how are you supposed to use the backdropState.offset value when correcting the frontLayerContent offset?
as the compose coordinate systems origin is the top left hand corner (x = 0, y = 0) and y dimension increases down the screen and x dimension increases Left to Right. when the backdrop is revealed the offset is a value (on my pixel 5) of approx 600.dp and concealed value of approx 300.dp. why is the concealed offset not 0.dp? is this taking into account the screens TopAppBar?
UPDATE (2)
Heres a GIF I made earlier
UPDATE (3)
This basic sample shows the problem i am having.
where have i made my mistake that stops me being able to scroll down
to see the complete last item when the backdrop is revealed?
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.rememberBackdropScaffoldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.elsevier.daisy.ui.theme.MyTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
val scope = rememberCoroutineScope()
MyTheme {
BackdropScaffold(
scaffoldState = scaffoldState,
frontLayerScrimColor = Color.Unspecified,
frontLayerElevation = 5.dp,
gesturesEnabled = false,
appBar = {
TopAppBar(
title = { Text("Backdrop") },
navigationIcon = {
if (scaffoldState.isConcealed) {
IconButton(
onClick = {
scope.launch { scaffoldState.reveal() }
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu"
)
}
} else {
IconButton(
onClick = {
scope.launch { scaffoldState.conceal() }
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
)
},
backLayerContent = {
Column {
Text(
text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
Text(
text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
}
},
frontLayerContent = {
LazyColumn(modifier = Modifier.fillMaxSize()) {
// Add 5 items
items(55) { index ->
Text(
text = "Item: ${index + 1}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(5.dp)
)
}
}
},
peekHeight = 60.dp,
) {
}
}
}
}
}
I would suggest adding a padding to the LazyColumn/LazyVerticalGrid itself rather than playing with the offset.
Given your example, I would simply add a conditional padding to the LazyColumn:
val padding = if (
scaffoldState.isRevealed &&
!scaffoldState.isAnimationRunning
) {
64.dp
} else {
0.dp
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(bottom = padding)
) {
// Your items here
}
I've used 64.dp here but use whatever you feel is necessary according to your design.
If instead of using that magic number you want to calculate the height of the backLayerContent you can by using the onGloballyPositioned modifier and save the height in a state.
Add this before your BackdropScaffold
var backLayerContentHeight by remember { mutableStateOf(0.dp) }
Get the backLayerContent height:
backLayerContent = {
val localDensity = LocalDensity.current
Column(
modifier = Modifier.onGloballyPositioned {
val heightInPx = it.size.height
val heightInDp = with(localDensity) { heightInPx.toDp() }
backLayerContentHeight = heightInDp
}
) {
...
}
},
Then you can use this value for the padding:
val padding = if (
scaffoldState.isRevealed &&
!scaffoldState.isAnimationRunning
) {
backLayerContentHeight
} else {
0.dp
}
Alternative to BackdropScaffold
You may want to create a custom implementation to get a similar behavior without workarounds.
This custom component should help:
#Composable
fun CustomBackdropScaffold(
modifier: Modifier = Modifier,
appBar: #Composable () -> Unit,
backLayerContent: #Composable () -> Unit,
frontLayerContent: #Composable () -> Unit,
isRevealed: Boolean,
) {
Surface(modifier = modifier) {
Column {
appBar()
AnimatedVisibility(
visible = isRevealed,
enter = expandVertically(
expandFrom = Alignment.Top,
),
exit = shrinkVertically(
shrinkTowards = Alignment.Top,
),
) {
backLayerContent()
}
frontLayerContent()
}
}
}
Here as an example of how to use it:
var isRevealed by remember { mutableStateOf(false) }
CustomBackdropScaffold(
appBar = {
TopAppBar(
title = { Text("Backdrop") },
navigationIcon = {
if (!isRevealed) {
IconButton(
onClick = {
isRevealed = true
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu"
)
}
} else {
IconButton(
onClick = {
isRevealed = false
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
)
},
backLayerContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
) {
Text(
text = "Menu Item 1",
modifier = Modifier.padding(8.dp),
color = Color.White,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
Text(
text = "Menu Item 1",
modifier = Modifier.padding(8.dp),
color = Color.White,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
}
},
frontLayerContent = {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
// Add 5 items
items(55, key = { it }) { index ->
Text(
text = "Item: ${index + 1}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(5.dp)
)
}
}
},
isRevealed = isRevealed
)
I want to change solid or gradient color to jetpack compose snack bar. Please guide me how to
change color
Here is my snack bar using material3 compose, I am looking solution to change the background color
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import compose.material.theme.ui.theme.Material3ComposeTheme
import compose.material.theme.ui.theme.Purple40
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Material3ComposeTheme {
val context = LocalContext.current
val snackState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
topBar = {
},
content = {
fun launchSnackbar(message: String, actionLabel : String?=null, duration: SnackbarDuration = SnackbarDuration.Short){
scope.launch {
snackState.showSnackbar(message = message,actionLabel=actionLabel, duration=duration)
}
}
Column(
modifier = Modifier
.padding(it)
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(47.dp))
Text("Snackbar", Modifier.padding(bottom = 10.dp), style = MaterialTheme.typography.labelLarge)
Button(onClick = {
// * Snackbar
launchSnackbar(message = "Hi i am snackbar message", actionLabel = "Hide", duration = SnackbarDuration.Long)
}) { Text("Snackbar",style = MaterialTheme.typography.labelLarge) }
ListDividerPadding()
Text("Toast", Modifier.padding(bottom = 10.dp), style = MaterialTheme.typography.labelLarge)
Button(onClick = {
Toast.makeText(
context,
"Hi i am toast message",
Toast.LENGTH_LONG
).show()
}) { Text("Toast",style = MaterialTheme.typography.labelLarge) }
}
}
)
Box(modifier = Modifier.fillMaxSize(), Alignment.BottomCenter){
SnackbarHost(hostState = snackState)
}
}
}
}
}
You can add SnackBar composable to SnackbarHost and change colors as
SnackbarHost(hostState = snackState) {
Snackbar(
snackbarData = it,
containerColor = Color.Green,
contentColor = Color.Red
)
}
Edit
There is no overload function that takes Brush instead of Color but you can add another Composable as with gradient color or more customization via content: #Composable () -> Unit
#Composable
fun Snackbar(
modifier: Modifier = Modifier,
action: #Composable (() -> Unit)? = null,
dismissAction: #Composable (() -> Unit)? = null,
actionOnNewLine: Boolean = false,
shape: Shape = SnackbarTokens.ContainerShape.toShape(),
containerColor: Color = SnackbarTokens.ContainerColor.toColor(),
contentColor: Color = SnackbarTokens.SupportingTextColor.toColor(),
actionContentColor: Color = SnackbarTokens.ActionLabelTextColor.toColor(),
dismissActionContentColor: Color = SnackbarTokens.IconColor.toColor(),
content: #Composable () -> Unit
)
Can be used as
Snackbar {
Row(
modifier = Modifier.background(
brush = Brush.horizontalGradient(
listOf(
Color.Red,
Color.Green,
Color.Blue
)
)
)
) {
Text("Hello World")
}
}
I have implemented 2 bottom sheets in the ModalBottomSheetLayout, both bottom sheets has a list of item checkable with checkbox.
The state of the screen is managed by the viewModel and when the selection changes is invoked a function that copies the state with the new value of the selected text.
When the bottom sheet opens the selection is correct but when I click to change the selection, the bottomsheet is not recomposed and the selection does not change, but in the main screen the state change is correctly read and the value is updated.
Here my code:
MainScreen:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
#ExperimentalMaterialApi
#Composable
fun MainScreen(
viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
val screenState = viewModel.screenState
val scope = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
)
var bottomSheetContent: (#Composable () -> Unit)? by remember {
mutableStateOf(null)
}
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetContent = {
Box(
modifier = Modifier.defaultMinSize(minHeight = 1.dp)
) {
bottomSheetContent?.let { it() }
}
}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(text = "First BottomSheet", style = MaterialTheme.typography.h6)
Text(
text = "Selected: ${screenState.selectedTextFromFirstBottomSheet}",
Modifier.padding(16.dp)
)
Button(onClick = {
bottomSheetContent = {
FirstBottomSheet(
selectedText = screenState.selectedTextFromFirstBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnFirstBottomSheetSelectedTextChanged(text)
)
},
textList = screenState.firstBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier.padding(16.dp)) {
Text(text = " Open First BottomSheet")
}
Text(text = "Second BottomSheet", style = MaterialTheme.typography.h6)
Text(
text = "Selected: ${screenState.selectedTextFromSecondBottomSheet}",
Modifier.padding(16.dp)
)
Button(
onClick = {
bottomSheetContent = {
SecondBottomSheet(
selectedText = screenState.selectedTextFromSecondBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnSecondBottomSheetSelectedTextChanged(text)
)
},
textList = screenState.secondBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier
.padding(16.dp)
) {
Text(text = " Open Second BottomSheet")
}
}
}
}
ViewModel:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel(){
var screenState by mutableStateOf(MainScreenState())
fun onEvent(event: MainScreenEvent){
when(event){
is MainScreenEvent.OnFirstBottomSheetSelectedTextChanged -> {
screenState = screenState.copy(
selectedTextFromFirstBottomSheet = event.text
)
}
is MainScreenEvent.OnSecondBottomSheetSelectedTextChanged -> {
screenState = screenState.copy(
selectedTextFromSecondBottomSheet = event.text
)
}
}
}
}
ScreenState
data class MainScreenState(
val selectedTextFromFirstBottomSheet: String = "First Text b1",
val selectedTextFromSecondBottomSheet: String = "Third Text b2",
val firstBottomSheetTextList: List<String> = listOf(
"First Text b1",
"Second Text b1",
"Third Text b1",
"Fourth Text b1",
"Five Text b1"
),
val secondBottomSheetTextList: List<String> = listOf(
"First Text b2",
"Second Text b2",
"Third Text b2",
"Fourth Text b2",
"Five Text b2"
)
)
Screen Event
sealed class MainScreenEvent(){
data class OnFirstBottomSheetSelectedTextChanged(val text: String): MainScreenEvent()
data class OnSecondBottomSheetSelectedTextChanged(val text: String): MainScreenEvent()
}
First Bottom Sheet
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
#Composable
fun FirstBottomSheet(
selectedText: String,
textList: List<String>,
onSelected: (text: String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
textList.forEach { text ->
Row(modifier = Modifier
.fillMaxWidth()
.toggleable(
value = selectedText == text,
role = Role.Checkbox,
onValueChange = { isSelected ->
if (isSelected) {
onSelected(text)
}
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = text, modifier = Modifier.weight(1f))
Checkbox(checked = selectedText == text, onCheckedChange = null)
}
}
}
}
Second Bottom Sheet
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
#Composable
fun SecondBottomSheet(
selectedText: String,
textList: List<String>,
onSelected: (text: String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
textList.forEach { text ->
Row(modifier = Modifier
.fillMaxWidth()
.toggleable(
value = selectedText == text,
role = Role.Checkbox,
onValueChange = { isSelected ->
if (isSelected) {
onSelected(text)
}
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically) {
Text(text = text, modifier = Modifier.weight(1f))
Checkbox(checked = selectedText == text, onCheckedChange = null)
}
}
}
}
Thanks for your help!
I copied and pasted your code. The only changes I made were:
Remove this line from MainScreen
val screenState = viewModel.screenState
Access the state directly.
Text(
text = "Selected: ${viewModel.screenState.selectedTextFromFirstBottomSheet}",
Modifier.padding(16.dp)
)
Button(onClick = {
bottomSheetContent = {
FirstBottomSheet(
selectedText = viewModel.screenState.selectedTextFromFirstBottomSheet,
onSelected = { text ->
viewModel.onEvent(
MainScreenEvent.OnFirstBottomSheetSelectedTextChanged(text)
)
},
textList = viewModel.screenState.firstBottomSheetTextList
)
}
scope.launch {
bottomSheetState.show()
}
}, modifier = Modifier.padding(16.dp)) {
Text(text = " Open First BottomSheet")
}
Boom! It worked :)
My understanding is: you're creating a variable containing the value of a state, but you're not listening to the state changes, so the Compose doesn't know the state has changed, therefore the recomposition doesn't happen. The by keyword in your state declaration is a property delegate which set/get the current value of state, but not register the composable to react to these changes.
There are another solutions you can use to observe the state without repeat viewModel.screenState:
Using derivedStateOf:
val screenState by remember {
derivedStateOf {
viewModel.screenState
}
}
Changing the screenState declaration.
// Using "=" instead of "by"
var screenState = mutableStateOf(MainScreenState())
and then use screenState.value to set/get the state value.
And in the screen, use like below:
val screenState = viewModel.screenState
I had the exact same use case in my app but everything was working fine until I updated some of the compose libraries.
I want to reduce padding of a single tab. Following image shows what I want:
What I am getting:
I am currently using the "accompanist-pager" and "accompanist-pager-indicators" with version 0.16.0.
Code:
#Composable
fun Tabs(tabNames: List<String>, pagerState: PagerState, scrollToPage: (Int) -> Unit) {
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = Color.White,
contentColor = Color.Black,
divider = {
TabRowDefaults.Divider(
thickness = 4.dp
)
},
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.customTabIndicatorOffset(tabPositions[pagerState.currentPage]),
height = 4.dp,
color = EmeraldTheme.colors.primary
)
}
) {
tabNames.forEachIndexed { index, name ->
Tab(
text = {
Text(
text = name,
maxLines = 1,
style = globalSearchDefaultTextStyle,
fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal,
color = if (pagerState.currentPage == index) EmeraldColor.Black100 else colorResource(globalSearchR.color.darkGrey20),
)
},
selected = pagerState.currentPage == index,
onClick = {
scrollToPage(index)
}
)
}
Row { Spacer(Modifier.weight(1f, true)) }
}
}
With the current version of TabRow (or ScrollableTabRow) you will not be able to do it. You will need to create your own TabRow composable.
Also, you should probably use a ScrollableTabRow instead of TabRow because TabRow evenly distributes the entire available width for its Tabs. So the content padding for that doesn't matter that much.
You can pretty much copy-paste the entire code for ScrollableTabRow, but modify the bit that sets up the tabConstraints.
It should no longer use the minTabWidth:
val minTabWidth = ScrollableTabRowMinimumTabWidth.roundToPx()
Custom tab is the way to go.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.Tab
import androidx.compose.material.Text
Tab(selected, onClick) {
Column(
Modifier.padding(10.dp).height(50.dp).fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween
) {
Box(
Modifier.size(10.dp)
.align(Alignment.CenterHorizontally)
.background(color = if (selected) Color.Red else Color.White)
)
Text(
text = title,
style = MaterialTheme.typography.body1,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#Tab(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)
You can use java reflection to change the value of ScrollableTabRowMinimumTabWidth.
And you can upvote here -> https://issuetracker.google.com/issues/226665301
try {
Class
.forName("androidx.compose.material3.TabRowKt")
.getDeclaredField("ScrollableTabRowMinimumTabWidth").apply {
isAccessible = true
}.set(this, 0f)
} catch (e: Exception) {
e.printStackTrace()
}