Wrap content of `ScrollableTabRow` composable so it goes past the screen width - android-jetpack-compose

I want to make a ScrollableTabRow that goes past the width of the screen to make it clearer that there are more tabs to scroll through. It should look something like this:
When scrolled all the way to the left:
When scrolled somewhere in the middle:
When scrolled all the way to the right:
However, I cannot achieve it using the Material component ScrollableTabRow because the ScrollableTabRow is filling the remaining width of the screen, instead of fully wrapping its content.
When scrolled all the way to the left:
When scrolled all the way to the right:
Here is my code using the ScrollableTabRow composable:
Note: 1.unit is equal to 4.dp
Row {
HorizontalSpacer(width = 4.unit)
ScrollableTabRow(
modifier = Modifier.clip(CircleShape).wrapContentSize(),
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
SpecsheetTabIndicator(tabPositions = tabPositions, pagerState = pagerState)
},
edgePadding = 0.dp,
divider = {},
containerColor = MaterialTheme.colorScheme.primaryContainer,
) {
for ((index, tab) in tabs.withIndex()) {
val textColor by animateColorAsState(
targetValue = when (pagerState.currentPage) {
index -> MaterialTheme.colorScheme.onPrimary
else -> MaterialTheme.colorScheme.onPrimaryContainer
},
)
Tab(
modifier = Modifier
.zIndex(6f)
.clip(CircleShape),
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(
text = tab.text(),
color = textColor,
)
},
)
}
}
HorizontalSpacer(width = 4.unit)
}
Also, as you can see in my code above, the Row is not really scrolling because I am not sure how to implement nested scrolling. Adding a horizontalScroll modifier to the Row makes the app crash because of the unhandled nested scrolling.
I have achieved the behavior I wanted as demonstrated in the first three images using regular rows:
Row(
modifier = Modifier.horizontalScroll(scrollState),
) {
HorizontalSpacer(width = 4.unit)
Row(
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
) {
for ((index, tab) in tabs.withIndex()) {
val containerColor by animateColorAsState(
targetValue = when (pagerState.currentPage) {
index -> MaterialTheme.colorScheme.primary
else -> Color.Transparent
},
)
val textColor by animateColorAsState(
targetValue = when (pagerState.currentPage) {
index -> MaterialTheme.colorScheme.onPrimary
else -> MaterialTheme.colorScheme.onPrimaryContainer
},
)
Surface(
modifier = Modifier.clip(CircleShape),
color = containerColor,
) {
Tab(
modifier = Modifier
.zIndex(6f)
.clip(CircleShape)
.defaultMinSize(minWidth = 24.unit),
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(
text = tab.text(),
color = textColor,
)
},
)
}
}
}
HorizontalSpacer(width = 4.unit)
}
However, I don't know how to reimplement a lot of the behavior that are built into the ScrollableTabRow like the proper rendering of the indicator and the proper scrolling of the tab row to the selected tab when the selected tab is not visible. Can anyone help me make this work for me?

Related

Jetpack Compose TabRow flickers when content is accompanist webview

I am experiencing flicker or overlapping when having a compose tabBar implementation with webviews as content. If I change the webviews with another view (ex. Box{Text}) it does not happen.
It seems as if the webview is filling more than it's border for a short while (See .gif below)
Update: I have been looking into if it was a recomposition issue (hence the simple test project) and I cannot identify any reason why it should recompose the tab bar.
When I add height to the tab bar, I can see the text is in the tab bar at all times.
A test project can be fetched here: https://github.com/msuhl/ComposeTabTest and is a very standard implementation
#Composable
private fun MyTabRow(
pagerState: PagerState,
coroutineScope: CoroutineScope,
) {
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
color = MaterialTheme.colors.secondary
)
},
) {
tabRowItems.forEachIndexed { index, item ->
Tab(
selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
icon = {
Icon(imageVector = item.icon, contentDescription = "")
},
text = {
Text(
text = item.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
},
)
}
}
HorizontalPager(
count = tabRowItems.size,
state = pagerState,
) {
ShowWebView("http://google.com")
}
}
It was related to the lazy loading of webview and I was not able to make a direct fix.
Instead I ended up with a working, although kind og hackish, solution
If a LazyColumn is introduced around the webview, the issue does not occur.
In code:
HorizontalPager(
count = tabRowItems.size,
state = pagerState,
) {
LazyColumn {
item {
ShowWebView(url)
}
}
}

how to animate button click in jetpack compose without consuming the click event

I saw similar questions with no great solutions so I thought this might be useful.
I had a problem when I wanted to animate a button the user clicked to make it larger when pressed; not as easy as it sounds because when you get the animation working the onClick event never fires. [Because it depends on the up event I guess]
I found a way to make both the animation and the click work for a icon button and I thought it might work for other cases.
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun RoundIconButton(
modifier: Modifier = Modifier,
imageVector: ImageVector,
onClick: () -> Unit,
tint: Color = Color.Black.copy(alpha = 0.8f),
backgroundColor: Color =
MaterialTheme.colors.background,
elevation: Dp = 4.dp,
contentDescription: String
) {
val interactionSource = remember {
MutableInteractionSource() }
val isPressed by
interactionSource.collectIsPressedAsState()
val transition = updateTransition(targetState
= isPressed, label = "")
val size by transition.animateDp(label = "")
{ state ->
when(state) {
false -> 40.dp
true -> 50.dp
}
}
Card(
modifier = modifier
.padding(all = 4.dp)
.clickable(interactionSource =
interactionSource,indication =
LocalIndication.current,onClick= onClick)
.then(Modifier.size(size)),
shape = CircleShape,
backgroundColor = backgroundColor,
elevation = elevation,
) {
Icon( imageVector = imageVector,
contentDescription = contentDescription,tint
= tint)
}
}

Show keyboard over Scaffold's bottomBar in Jetpack Compose and apply proper inset paddings

I'm using Scaffold for my main screen with a fixed bottomBar that is visible in every screen of the app, and I'm applying the innerPadding of the Scaffold to its content.
I want the keyboard to appear over the bottomBar, and for that I'm applying the imePadding() only to the Scaffold's content.
However, when the keyboard is opened, both the Scaffold's innerPading and imePadding() are applied to the contents padding.
I've tried to go through the Accompanist Insets migration, but no lucky.
Is there anyway that I can prevent it and apply only one or the other?
Here is a piece of my code:
Scaffold(
topBar = { },
bottomBar = { },
modifier = Modifier
.systemBarsPadding()
) { innerPadding ->
Content(
modifier = Modifier
.padding(innerPadding)
.imePadding()
)
}
And this is the result:
With the now, deprecated, Accompanist Insets, I was using the following solution:
val isImeVisible = LocalWindowInsets.current.ime.isVisible
val contentPadding = remember(isImeVisible) {
if (isImeVisible) PaddingValues(top = innerPadding.calculateTopPadding()) else innerPadding
}
According to Accompanist Insets migration, LocalWindowInsets.current.ime should be replaced with WindowInsets.ime.
It doesn't have isVisible for now, until this bug is fixed. Here's how I've re-created it for now:
val WindowInsets.Companion.isImeVisible: Boolean
#Composable
get() {
val density = LocalDensity.current
val ime = this.ime
return remember {
derivedStateOf {
ime.getBottom(density) > 0
}
}.value
}
Usage:
val isImeVisible = WindowInsets.isImeVisible
This should work with your old remember(isImeVisible) code.
Another solution would be to set BringIntoViewRequester to your content inside Scaffold. Then when textField is focused, you could call bringIntoViewRequester.bringIntoView(). This way you wouldn't need to set any paddings.
val bringIntoViewRequester = remember { BringIntoViewRequester() }
Column(
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
) {
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
delay(350)
bringIntoViewRequester.bringIntoView()
}
}
}
)
}
Try using something like this (WARNING: consumedWindowInsets is Experimental, but it's working):
Scaffold(
topBar = { },
bottomBar = { },
modifier = Modifier
.systemBarsPadding()
) { innerPadding ->
Content(
modifier = Modifier
.consumedWindowInsets(innerPadding)
.padding(innerPadding)
.imePadding()
)
}

How to make custom tab indicator to be a rounded bar in Jetpack Compose

The following picture shows what I want to achieve. I want the tab indicator to be a short rounded bar.
I looked up the implementation of TabRowDefaults.Indicator(), and just made my own one. I just tried to add the clip() modifier, but it didn't work. And I tried to change the order of the modifiers, but still no luck.
And here is my code implementation:
#Composable
fun TabLayout(
tabItems: List<String>,
content: #Composable () -> Unit
) {
var tabIndex by remember { mutableStateOf(0) }
Column {
ScrollableTabRow(
selectedTabIndex = tabIndex,
edgePadding = 0.dp,
backgroundColor = MaterialTheme.colors.background,
contentColor = Blue100,
indicator = { tabPositions ->
Box(
modifier = Modifier
.tabIndicatorOffset(tabPositions[tabIndex])
.height(4.dp)
.clip(RoundedCornerShape(8.dp)) // clip modifier not working
.padding(horizontal = 28.dp)
.background(color = AnkiBlue100)
)
},
divider = {},
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = tabIndex == index,
onClick = { tabIndex = index },
selectedContentColor = Blue100,
unselectedContentColor = Gray200,
text = {
Text(text = item, fontFamily = fontOutfit, fontSize = 18.sp)
}
)
}
}
Divider(
color = Gray50,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
content()
}
}
You applied Modifier.padding between Modifier.clip and Modifier.background, so the rounding is actually applied to the transparent padding. You need to move the padding in front of the clip, or specify the shape with the background:
.background(color = AnkiBlue100, shape = RoundedCornerShape(8.dp))
Read more about why the order of the modifiers matters in this answer

android compose lazy column separate by weight

Is it possible to do weights in Jetpack Compose with lazy column?
I'd like to set it menu item is weighted as 1/n (n = number of menus) of a layout, and the other takes up the remaining 1/n, also.
I want to list it at the same height as the number of menus.
MenuList
#Composable
fun MenuList(
loading: Boolean,
menus: List<Menu>,
onNavigateToMenuDetailScreen: (String) -> Unit
) {
Box(modifier = Modifier
.background(color = MaterialTheme.colors.surface)
.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
if (loading && menus.isEmpty()) {
LoadingShimmer(imageHeight = 800.dp)
}
else if (menus.isEmpty()) {
NothingHere()
}
else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1F)
) {
itemsIndexed(
items = menus
) { index, menu ->
MenuCard(
menu = menu,
onClick = {
}
)
}
}
}
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = Modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column {
Text(
text = menu.name,
fontSize = 30.sp,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.wrapContentHeight(Alignment.CenterVertically),
style = MaterialTheme.typography.h3
)
}
}
}
In summary, MenuCards are created as many as the number of menu on the MenuList screen, and I hope the height of each MenuCard will be 1/n.
(n = number of menu)
Like, when number of menu is 8,
Just same height to each menu.
Replace your LazyColumn code with BoxWithConstraints and regular Column.
BoxWithConstraints for getting the minHeight or in this case you can say screen height.
Change to something like the below.
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val height = minHeight/menus.size
Column(
modifier = Modifier
.fillMaxSize()
) {
menus.forEachIndexed { index, menu ->
MenuCard(
modifier = Modifier.height(height),
menu = menu,
onClick = {
}
)
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column(
modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = menu.name,
fontSize = 30.sp,
style = MaterialTheme.typography.h3
)
}
}
}
You will get this
3 items and 6 items
If all you want is a way to divide the screen height in equal proportions to all the n items, you can simply go for a column.
val items = ... //I assume you have access to this
val screenHeight = LocalConfiguration.current.screenHeightDp
Column{
items.forEach{
MenuCard(modifier = Modifier.height(1f / items.size * screenHeight))
}
}
Column and LazyColumn are fundamentally different. LazyColumn's sole purpose is to deal with large datasets by loading and caching only a small window off that set. Now, if there is nothing to be scrolled, LazyColumn is useless, in fact, it is worse.
EDIT:
You could use BoxWithConstraints to get the screen dimensions as well, but it, in this context, would be more of a workaround than a solution. It adds an extra Composable and decreases the readability of the code too, so for getting the screen dimensions, always use the API that was specifically built for the job.

Resources