I am tiring to remove the underline of TabRow, but did not succeed. Here is the code:
#ExperimentalPagerApi
#Composable
fun Tabs(pagerState: PagerState) {
val tabs = listOf(R.string.add, R.string.add)
val scope = rememberCoroutineScope()
val currentPage = pagerState.currentPage
TabRow(
modifier = Modifier
.padding(start = 36.dp, top = 16.dp, end = 36.dp)
.clip(shape = RoundedCornerShape(16.dp)),
selectedTabIndex = currentPage,
backgroundColor = Color.Transparent,
tabs = {
tabs.forEachIndexed { index, tab ->
Tab(
modifier = Modifier.clip(RoundedCornerShape(16.dp)),
text = {
Text(text = stringResource(id = tab))
},
selected = currentPage == index,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
}
)
}
}
)
}
I only want to have the selected color.
set divider param of TabRow as divider={}. Default one is
divider: #Composable () -> Unit = #Composable {
Divider()
}
Related
What I want
Top fixed item with status bar padding and adaptive radius
Bottom fixed item with navigation bar padding
Adaptive center item, have enough room => Not scrollable, if not => scrollable
Current status
For full demo click this link
Only problem here is when there is no extra space, and we are dragging the scrollable list.
I think it's a bug, because everything is fine except the scrollable column.
window.height - sheetContent.height >= statusBarHeight
Everything is fine.
window.height - sheetContent.height < statusBarHeight
Dragging top fixed item or bottom fixed item, scroll still acting well.
Dragging the scrollable list, sheet pops back to top when the sheetState.offset is approaching statusBarHeight
Test youself
You can test it with these 3 functions, for me, I'm using Pixel 2 Emulator, itemCount at 18,19 could tell the difference.
#Composable
fun CEModalBottomSheet(
sheetState: ModalBottomSheetState,
onCloseDialogClicked: () -> Unit,
title: String = "BottomSheet Title",
toolbarElevation: Dp = 0.dp,
sheetContent: #Composable ColumnScope.() -> Unit,
) {
val density = LocalDensity.current
val sheetOffset = sheetState.offset
val statusBar = WindowInsets.statusBars.asPaddingValues()
val shapeRadius by animateDpAsState(
if (sheetOffset.value < with(density) { statusBar.calculateTopPadding().toPx() }) {
0.dp
} else 12.dp
)
val dynamicStatusBarPadding by remember {
derivedStateOf {
val statusBarHeightPx2 = with(density) { statusBar.calculateTopPadding().toPx() }
val offsetValuePx = sheetOffset.value
if (offsetValuePx >= statusBarHeightPx2) {
0.dp
} else {
with(density) { (statusBarHeightPx2 - offsetValuePx).toDp() }
}
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(topStart = shapeRadius, topEnd = shapeRadius),
content = {},
sheetContent = {
Column(modifier = Modifier.fillMaxWidth()) {
TopTitleItemForDialog(
title = title,
elevation = toolbarElevation,
topPadding = dynamicStatusBarPadding,
onClick = onCloseDialogClicked
)
sheetContent()
}
})
}
#Composable
fun TopTitleItemForDialog(
title: String,
elevation: Dp = 0.dp,
topPadding: Dp = 0.dp,
onClick: () -> Unit
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.LightGray,
elevation = elevation
) {
Row(
modifier = Modifier.padding(top = topPadding),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.size(16.dp))
Text(
text = title,
maxLines = 1,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cancel),
tint = Color.Gray,
modifier = Modifier.size(24.dp)
)
}
}
}
}
class SheetPaddingTestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.statusBarColor = android.graphics.Color.TRANSPARENT
window.navigationBarColor = android.graphics.Color.TRANSPARENT
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
Box(
Modifier
.fillMaxSize()
.background(Color.Green), contentAlignment = Alignment.Center
) {
var itemCount by remember { mutableStateOf(20) }
val state = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) { false }
val scope = rememberCoroutineScope()
Text("显示弹窗", modifier = Modifier.clickable {
scope.launch { state.animateTo(ModalBottomSheetValue.Expanded) }
})
CEModalBottomSheet(sheetState = state,
onCloseDialogClicked = {
scope.launch {
state.hide()
}
}, sheetContent = {
Column(
Modifier
.verticalScroll(rememberScrollState())
.weight(1f, fill = false)
.padding(horizontal = 16.dp),
) {
repeat(itemCount) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(30.dp)
.background(Color.Blue.copy(alpha = 1f - it * 0.04f))
)
}
}
CompositionLocalProvider(
LocalContentColor.provides(Color.White)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary)
.padding(vertical = 12.dp)
.navigationBarsPadding(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(modifier = Modifier.weight(1f),
onClick = {
itemCount = max(itemCount - 1, 0)
}) {
Icon(Icons.Default.KeyboardArrowLeft, "")
}
Text(
modifier = Modifier.weight(1f), text = "$itemCount",
textAlign = TextAlign.Center
)
IconButton(modifier = Modifier.weight(1f),
onClick = {
itemCount++
}) {
Icon(Icons.Default.KeyboardArrowRight, "")
}
}
}
}
)
}
}
}
}
I have a shopping list item composable that is not taking up the entire width of the parent, as you can see below with the red border. I want it to be flush against the parent's edge. And why is there some space or padding just before the checkbox? What needs to be modified?
Composable
#Composable
fun ShoppingListScreenItem(
itemName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth().border(2.dp, Red),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
modifier = Modifier.padding(0.dp),
checked = checked,
onCheckedChange = onCheckedChange
)
Text(
modifier = Modifier.padding(start = 8.dp),
fontWeight = FontWeight.Bold,
text = itemName
)
}
}
Parent Composable
#Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel
) {
val scope = rememberCoroutineScope()
val name = stringResource(id = R.string.item_name)
val nameError = stringResource(id = R.string.item_IsNameError)
val category = stringResource(id = R.string.item_category)
val categoryError = stringResource(id = R.string.item_IsCategoryError)
val focusManager = LocalFocusManager.current
val allItems =
shoppingListScreenViewModel.shoppingListItemsState.value?.collectAsLazyPagingItems()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "AppBar") },
backgroundColor = Color.White,
navigationIcon = if (navController.previousBackStackEntry != null) {
{
IconButton(onClick = { navController.navigateUp() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
} else {
null
}
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
shoppingListScreenViewModel.setStateValue("ShowAddItemDialog", true)
},
backgroundColor = Color.Blue,
contentColor = Color.White
) {
Icon(Icons.Filled.Add, "")
}
},
// Defaults to false
isFloatingActionButtonDocked = false,
bottomBar = { BottomNavigationBar(navController = navController) }
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LazyColumn(
contentPadding = PaddingValues(
vertical = 8.dp,
horizontal = 8.dp
)
) {
//todo change it to non null
items(allItems!!) { item ->
ShoppingListScreenItem(
itemName = item?.name!!,
checked = item.isInCart
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item, isChecked)
}
}
}
}
if (shoppingListScreenViewModel.shoppingListScreenState.value.showAddItemDialog) {
OnTheFlyAddItemDialog(
shoppingListScreenViewModel = shoppingListScreenViewModel,
focusManager = focusManager,
navController = navController,
onDismiss = {
shoppingListScreenViewModel.setStateValue(name, "")
shoppingListScreenViewModel.setStateValue(nameError, false)
shoppingListScreenViewModel.setStateValue(category, "")
shoppingListScreenViewModel.setStateValue(categoryError, false)
shoppingListScreenViewModel.setStateValue("ShowAddItemDialog", false)
}
)
{
scope.launch {
shoppingListScreenViewModel.addShoppingListItemToDb()
shoppingListScreenViewModel.setStateValue("ShowAddItemDialog", false)
shoppingListScreenViewModel.setStateValue(name, "")
shoppingListScreenViewModel.setStateValue(nameError, false)
shoppingListScreenViewModel.setStateValue(category, "")
shoppingListScreenViewModel.setStateValue(categoryError, false)
// triggerCount++
}
}
}
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = {
navController.navigate(NavScreens.AddItemScreen.route) {
popUpTo(NavScreens.AddItemScreen.route) {
inclusive = true
}
}
}
) {
Text("Goto add item screen")
}
}
}
}
You set a contentPadding in your LazyColumn that is responsible for the spaces. Remove it, or set it to zero.
LazyColumn(
contentPadding = PaddingValues(
vertical = 8.dp,
horizontal = 0.dp
)
)
The padding around the Checkbox depends on the minimumTouchTargetSize defined when the onClick is not null with a hardcoded value of 48.dp.
You can override it using:
CompositionLocalProvider(
LocalMinimumTouchTargetEnforcement provides false,
) {
Checkbox(
modifier = Modifier.padding(0.dp), //4.dp
checked = false,
onCheckedChange = {}
)
}
but it is not the best choice in term of accessibility.
.
The space around the Row depends on the contentPadding defined in the LazyColumn.
I have a LazyVerticalGrid composable that shows my items. I want to show an error message with a button in the center of LazyVerticalGrid when an error has occured. Here is my error item and my LazyVerticalGrid:
ErrorItem
#Composable
fun ErrorItem(
message: String,
modifier: Modifier = Modifier,
onRetryClick: () -> Unit,
) {
Column(
modifier = modifier
.padding(dimensionResource(id = R.dimen.dimen_8)),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = message,
maxLines = 1,
style = MaterialTheme.typography.subtitle2,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.dimen_16))
)
Button(
onClick = onRetryClick,
shape = CircleShape,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError
)
) {
Text(text = stringResource(id = R.string.try_again))
}
}
}
#Composable
fun CompaniesScreen(
modifier: Modifier = Modifier
) {
val viewModel: CompaniesScreenViewModel = hiltViewModel()
val companies: LazyPagingItems<Company> = viewModel.companies.collectAsLazyPagingItems()
val isLoading = companies.loadState.refresh is LoadState.Loading
val isError = companies.loadState.refresh is LoadState.Error
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading)
val listState = rememberLazyGridState()
Scaffold(modifier = modifier, topBar = { LogoToolbar() }) { padding ->
SwipeRefresh(indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true,
backgroundColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary
)
}, state = swipeRefreshState, onRefresh = companies::refresh) {
LazyVerticalGrid(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = dimensionResource(id = R.dimen.dimen_20),
vertical = dimensionResource(
id = R.dimen.dimen_24
)
),
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.dimen_20)),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(
id = R.dimen.dimen_24
)
)
) {
if (!isLoading && !isError) {
items(companies.itemCount) { index ->
CompanyItem(companyName = companies[index]?.name!!)
}
}
companies.apply {
when {
loadState.refresh is LoadState.Loading -> {
items(16) {
CompanyItem(companyName = "", isLoading = true)
}
}
loadState.refresh is LoadState.Error -> {
val e = companies.loadState.refresh as LoadState.Error
item(span = {GridItemSpan(2)}) {
ErrorItem(
modifier = Modifier.fillMaxSize(), // fillParentMaxSize() is not working
message = e.error.localizedMessage
?: stringResource(id = R.string.something_went_wrong),
onRetryClick = companies::retry
)
}
}
}
}
}
}
}
}
I have tried to use fillParentMaxSizewith my ErrorItem but it is not working. Here is the result :
If you place your LazyVerticalGrid and ErrorItem inside a Box with contentAlignment = Alignment.Center ErrorItem will show center of your Box when isError is true
Box(
modifier= Modifier.fillMaxSize(),
contentAlignment = Alignment.Center){
LazyVerticalGrid(
// Rest of the properties
)
if (isError){
ErrorItem(""){
}
}
}
I want to customize the look of the tabs in jetpack compose, here is my tabs look like right now
But I want to look my tabs like this:
I am creating tabs like this way
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White
) {
filters.forEachIndexed { index, filter ->
Tab(
text = {
Text(
text = filter.name.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
)
},
selected = pagerState.currentPage == index,
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
)
}
}
How can I achieve that look, I have searched a lot but didn't find any clue, can anyone help?
#Composable
fun CustomTabs() {
var selectedIndex by remember { mutableStateOf(0) }
val list = listOf("Active", "Completed")
TabRow(selectedTabIndex = selectedIndex,
backgroundColor = Color(0xff1E76DA),
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
.clip(RoundedCornerShape(50))
.padding(1.dp),
indicator = { tabPositions: List<TabPosition> ->
Box {}
}
) {
list.forEachIndexed { index, text ->
val selected = selectedIndex == index
Tab(
modifier = if (selected) Modifier
.clip(RoundedCornerShape(50))
.background(
Color.White
)
else Modifier
.clip(RoundedCornerShape(50))
.background(
Color(
0xff1E76DA
)
),
selected = selected,
onClick = { selectedIndex = index },
text = { Text(text = text, color = Color(0xff6FAAEE)) }
)
}
}
}
Result is as in gif.
In addition to Thracian's answer:
If you need to keep the state of tabs after the screen orientation change, use rememberSaveable instead of remember for selectedIndex.
Animated Tabs
Do you want to create something like this?
I tried to use the Material Design 3 library but it makes everything much more difficult, so I created the TabRow from scratch.
You can use this code to save you some time:
#Composable
fun AnimatedTab(
items: List<String>,
modifier: Modifier,
indicatorPadding: Dp = 4.dp,
selectedItemIndex: Int = 0,
onSelectedTab: (index: Int) -> Unit
) {
var tabWidth by remember { mutableStateOf(0.dp) }
val indicatorOffset: Dp by animateDpAsState(
if (selectedItemIndex == 0) {
tabWidth * (selectedItemIndex / items.size.toFloat())
} else {
tabWidth * (selectedItemIndex / items.size.toFloat()) - indicatorPadding
}
)
Box(
modifier = modifier
.onGloballyPositioned { coordinates ->
tabWidth = coordinates.size.width.toDp
}
.background(color = gray100, shape = Shapes.card4)
) {
MyTabIndicator(
modifier = Modifier
.padding(indicatorPadding)
.fillMaxHeight()
.width(tabWidth / items.size - indicatorPadding),
indicatorOffset = indicatorOffset
)
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
items.forEachIndexed { index, title ->
MyTabItem(
modifier = Modifier
.fillMaxHeight()
.width(tabWidth / items.size),
onClick = {
onSelectedTab(index)
},
title = title
)
}
}
}
}
#Composable
private fun MyTabIndicator(
modifier: Modifier,
indicatorOffset: Dp,
) {
Box(
modifier = modifier
.offset(x = indicatorOffset)
.clip(Shapes.card4)
.background(white100)
)
}
#Composable
private fun MyTabItem(
modifier: Modifier,
onClick: () -> Unit,
title: String
) {
Box(
modifier = modifier
.clip(Shapes.card4)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { onClick() },
contentAlignment = Alignment.Center
) {
Text(text = title)
}
}
Usage:
var selectedTab by remember { mutableStateOf(0) }
AnimatedTab(
modifier = Modifier
.height(40.dp)
.fillMaxSize(.9f),
selectedItemIndex = selectedTab,
onSelectedTab = { selectedTab = it },
items = listOf("first", "second")
)
I am trying to show a section within LazyColumn which has a list of Rows that are shown horizontally using LazyRow. I would like to have a button which displays show/hide so that I can show a minimal list in this section instead of full list. I would like to animate the expand/collapse part and currently expanding on button click is working as expected but when collapsing, the LazyCloumn scrolls up which seems to push this section out of screen (as shown in the video below). Is there any way we can collapse so that the button at least gets snapped to the top and the remaining section is removed? This way, user can still expand the list if required rather than scrolling up to find the button.
I have tried the following but none of them seem to work:
Using AnimatedVisibility
Using animate*AsState low level API's
Also tried to just remove the contents from list allowing LazyColumn to re-order based on the list content
val RandomColor
get() = Color(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
typealias ClickHandler = (Boolean) -> Unit
#Composable
fun DemoLayout(demoDataList: List<DemoData>, isExpanded: Boolean, clickHandler: ClickHandler) {
LazyColumn {
demoDataList.forEachIndexed { index, it ->
when (it) {
is DemoData.Header -> item(key = "cell_$index") { HeaderComposable(header = it) }
is DemoData.BigCard -> item(key = "hero_$index") { BigCardComposable(bigCard = it) }
is DemoData.Card -> item(key = "banner_$index") { CardComposable(card = it) }
is DemoData.ExpandableSection -> {
items(count = 2, key = { indexInner: Int -> "categories_first_half_$index$indexInner" }) { index ->
Section(
sectionInfo = it.sectionInfo[index]
)
}
//Comment below and try another approach
item(key = "first_approach_$index") {
FirstApproach(
expandableSection = DemoData.ExpandableSection(
it.sectionInfo.subList(
3,
5
)
)
)
}
//Second approach
/*if (isExpanded)
items(count = 3, key = { indexInner -> "categories_second_half_$index$indexInner" }) { index ->
Section(
sectionInfo = it.sectionInfo[index + 2]
)
}
item(key = "button_$index") {
ShowHideButton(isExpanded, clickHandler)
}*/
}
}
}
}
}
#Composable
fun FirstApproach(expandableSection: DemoData.ExpandableSection) {
var expanded by remember { mutableStateOf(false) }
val density = LocalDensity.current
Column {
AnimatedVisibility(
visible = expanded,
enter = slideInVertically() +
expandVertically(
// Expand from the top.
expandFrom = Alignment.Top,
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing)
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically(
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing)
) + shrinkVertically(
shrinkTowards = Alignment.Bottom,
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing)
) + fadeOut(
animationSpec = tween(durationMillis = 350, easing = FastOutLinearInEasing),
targetAlpha = 0f
)
) {
Column {
for (i in 0 until expandableSection.sectionInfo.size) {
HeaderComposable(header = expandableSection.sectionInfo[i].header)
InfoCardsComposable(expandableSection.sectionInfo[i].infoCard)
DetailsCardComposable(expandableSection.sectionInfo[i].detailCard)
}
}
}
Button(
modifier = Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth(),
onClick = {
expanded = !expanded
}) {
Text(text = if (expanded) "Hide" else "Show")
}
}
}
#Composable
fun HeaderComposable(header: DemoData.Header) {
Row(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth()
.height(64.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = header.title, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun CardComposable(card: DemoData.Card) {
Card(
modifier = Modifier
.padding(top = 16.dp)
.size(164.dp),
backgroundColor = RandomColor
) {
Text(text = card.cardText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun BigCardComposable(bigCard: DemoData.BigCard) {
Card(
modifier = Modifier
.padding(top = 16.dp)
.size(172.dp),
backgroundColor = RandomColor
) {
Text(text = bigCard.bigCardText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun Section(sectionInfo: SectionInfo) {
Column(
modifier = Modifier.animateContentSize()
) {
HeaderComposable(header = sectionInfo.header)
InfoCardsComposable(sectionInfo.infoCard)
DetailsCardComposable(sectionInfo.detailCard)
}
}
#Composable
private fun ShowHideButton(isExpanded: Boolean, clickHandler: ClickHandler) {
Button(
modifier = Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
.fillMaxWidth(),
onClick = {
clickHandler.invoke(
!isExpanded
)
}) {
Text(text = if (isExpanded) "Hide" else "Show")
}
}
#Composable
fun DetailsCardComposable(detailCardsList: List<DetailCard>) {
LazyRow(
modifier = Modifier.padding(top = 16.dp)
) {
items(detailCardsList) {
DetailCardComposable(detailCard = it)
}
}
}
#Composable
fun InfoCardsComposable(infoCardsList: List<InfoCard>) {
LazyRow(
modifier = Modifier.padding(top = 16.dp)
) {
items(infoCardsList) {
InfoCardComposable(infoCard = it)
}
}
}
#Composable
fun InfoCardComposable(infoCard: InfoCard) {
Card(
modifier = Modifier
.size(136.dp),
backgroundColor = RandomColor
) {
Text(text = infoCard.infoText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
#Composable
fun DetailCardComposable(detailCard: DetailCard) {
Card(
modifier = Modifier
.size(156.dp),
backgroundColor = RandomColor
) {
Text(text = detailCard.detailText, modifier = Modifier.padding(horizontal = 16.dp))
}
}
Complete code to try out is available here: https://github.com/DirajHS/ComposeAnimation/tree/master
I would like to know if this is the expected behavior or am I doing something wrong?
Any suggestions on snapping the button to the top during collapse would be much appreciated.