In the shopping list item composable, I am reducing the alpha value on the whole item by changing the alpha value on it's parent Row composable, but I want to exclude a child Icon composable from receiving the parent Row's alpha change and retain a 100% alpha value. I set the modifier on the child Icon to alpha(1f), but it is not working. The alpha change on the parent is also propagating to the child despite this. Is it possible exclude the child from the parent's alpha change?
Composable
#Composable
fun ShoppingListScreenItem(
rowModifier: Modifier = Modifier,
item: ShoppingListItem,
mainViewModel: ShoppingListScreenViewModel,
onNavigateToAddEditItemScreenFromItemStrip: (ShoppingListItem) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, bottom = 5.dp)
.then(rowModifier)
.alpha(if (item.isItemDisabled) 0.35f else 1f)
.clickable {
onNavigateToAddEditItemScreenFromItemStrip(item)
},
verticalAlignment = Alignment.CenterVertically,
) {
...
if(mainViewModel.shoppingListState.value!!.sortOrder != "Custom" && !item.isInCart) {
//I want to exclude this child composable
IconToggleButton(
checked = item.isItemDisabled,
onCheckedChange = {
scope.launch {
mainViewModel.updateShoppingListItemDisabledInDb(item, it)
}
},
) {
Icon(
modifier = Modifier.alpha(1f).size(26.dp),
painter = painterResource(id = R.drawable.baseline_disabled_visible_24),
contentDescription = "Toggle disable the item strip",
)
}
}
}
}
This can be achieved using Layout and placeable.placeWithLayer and Modifier.layoutId to select which Composable is to be used with default alpha as.
This is a custom Column, you can customize Layout as required, purpose is to show Modifier.layoutId usage and Placeable.placeRelativeWithLayer to apply any desired graphic layer property to specific Composable in layout phase.
Result
Usage
MyLayout(
alpha = .5f
) {
Text("Default Alpha", fontSize = 20.sp)
Text("Default Alpha", fontSize = 20.sp)
Text("Custom Alpha", fontSize = 20.sp), modifier = Modifier.layoutId("full_alpha"))
Text("Default Alpha", fontSize = 20.sp)
Image(painter = painterResource(id = R.drawable.landscape5), contentDescription = "")
}
Implementation
#Composable
private fun MyLayout(
modifier: Modifier = Modifier,
alpha: Float = 1f,
content: #Composable () -> Unit
) {
val measurePolicy = MeasurePolicy { measurables, constraints ->
val fullAlphaIndex = measurables.indexOfFirst {
it.layoutId == "full_alpha"
}
val placeablesWidth = measurables.map { measurable ->
measurable.measure(constraints)
}
val hasBoundedWidth = constraints.hasBoundedWidth
val hasFixedWidth = constraints.hasFixedWidth
val width =
if (hasBoundedWidth && hasFixedWidth) constraints.maxWidth
else placeablesWidth.maxOf { it.width }
val height = placeablesWidth.sumOf {
it.height
}
var posY = 0
layout(width, height) {
placeablesWidth.forEachIndexed { index, placeable ->
placeable.placeRelativeWithLayer(0, posY) {
if (index == fullAlphaIndex) {
this.alpha = 1f
} else {
this.alpha = alpha
}
}
posY += placeable.height
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
If you wish to create a Row you need to place items one after other horizontally instead of increasing y position
Result
#Composable
fun MyLayout(
modifier: Modifier = Modifier,
alpha: Float = 1f,
content: #Composable () -> Unit
) {
val measurePolicy = MeasurePolicy { measurables, constraints ->
val fullAlphaIndex = measurables.indexOfFirst {
it.layoutId == "full_alpha"
}
val placeablesWidth = measurables.map { measurable ->
measurable.measure(
constraints.copy(
minWidth = 0,
maxWidth = Constraints.Infinity,
minHeight = 0,
maxHeight = Constraints.Infinity
)
)
}
val hasBoundedWidth = constraints.hasBoundedWidth
val hasFixedWidth = constraints.hasFixedWidth
val hasBoundedHeight = constraints.hasBoundedHeight
val hasFixedHeight = constraints.hasFixedHeight
val width =
if (hasBoundedWidth && hasFixedWidth) constraints.maxWidth
else placeablesWidth.sumOf { it.width }.coerceAtMost(constraints.maxWidth)
val height =
if (hasBoundedHeight && hasFixedHeight) constraints.maxHeight
else placeablesWidth.maxOf { it.height }.coerceAtMost(constraints.maxHeight)
var posX = 0
layout(width, height) {
placeablesWidth.forEachIndexed { index, placeable ->
placeable.placeRelativeWithLayer(posX, 0) {
if (index == fullAlphaIndex) {
this.alpha = 1f
} else {
this.alpha = alpha
}
}
posX += placeable.width
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
Same usage but this time with Icons
MyLayout(
modifier = Modifier.drawChecker(),
alpha = .4f
) {
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.layoutId("full_alpha")
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
}
Checker Modifier if anyone wonders is as
fun Modifier.drawChecker() = this.then(
drawBehind {
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
}
)
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 want to create a composable (shown in below image) where one box is larger and one smaller box is placed at bottom center of large box and bottom line of large box is passing through center of small box. How do I do that?
You could use a custom layout:
#Composable
fun Boxes(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val largeBox = measurables[0]
val smallBox = measurables[1]
val looseConstraints = constraints.copy(
minWidth = 0,
minHeight = 0,
)
val largePlaceable = largeBox.measure(looseConstraints)
val smallPlaceable = smallBox.measure(looseConstraints)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight
) {
largePlaceable.placeRelative(0, 0)
smallPlaceable.placeRelative(
x = (constraints.maxWidth - smallPlaceable.width) / 2,
y = largePlaceable.height - smallPlaceable.height / 2
)
}
}
}
#Preview(widthDp = 420, heightDp = 720)
#Composable
fun BoxesPreview() {
ComposePlaygroundTheme() {
Surface(modifier = Modifier.fillMaxSize()) {
Boxes(
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(128.dp)
.background(Color.Green)
)
Box(
modifier = Modifier
.width(128.dp)
.height(64.dp)
.background(Color.Red)
)
}
}
}
}
Edit: restricting the boxes height, use this
#Composable
fun Boxes(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val largeBox = measurables[0]
val smallBox = measurables[1]
val looseConstraints = constraints.copy(
minWidth = 0,
minHeight = 0,
)
val largePlaceable = largeBox.measure(looseConstraints)
val smallPlaceable = smallBox.measure(looseConstraints)
layout(
width = constraints.maxWidth,
height = largePlaceable.height + smallPlaceable.height / 2,
) {
largePlaceable.placeRelative(
x = 0,
y = 0,
)
smallPlaceable.placeRelative(
x = (constraints.maxWidth - smallPlaceable.width) / 2,
y = largePlaceable.height - smallPlaceable.height / 2
)
}
}
}
#Preview(widthDp = 420, heightDp = 720)
#Composable
fun BoxesPreview() {
ComposePlaygroundTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column {
Text(
text = "Before",
modifier = Modifier.padding(all = 16.dp)
)
Boxes(
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(128.dp)
.background(Color.Green)
)
Box(
modifier = Modifier
.width(128.dp)
.height(64.dp)
.background(Color.Red)
)
}
Text(
text = "After",
modifier = Modifier.padding(all = 16.dp)
)
}
}
}
}
I managed to work this out, and setup 3 cards one on top of the other as seperate boxs compose elements with onclick and on drag properties.
The issue is now, that I'd like the card that I'm pressing/dragging to set to the front, so, I played with the z-index modifier, but, it looks like I'm doing something wrong. Any idea?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Test1Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
for (i in 1 until 4) {
DraggableBox(title = "Box_${+1}", initX = 100f*i.toFloat(), initY = 100f, content =
{
Text(text = "Box_${i}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
)
}
}
}
}
}
}
#Composable
fun DraggableBox(title: String, initX: Float = 0f, initY: Float = 0f, content: #Composable() () -> Unit) {
val cardInitWidth = 135f
val cardInitHeight = 190f
val expandValue = 20f
Box(
modifier = Modifier
.fillMaxSize()
) {
val shape = RoundedCornerShape(12.dp)
val coroutineScope = rememberCoroutineScope()
val enable = remember { mutableStateOf(true) }
var offsetX = remember { Animatable(initialValue = initX) }
var offsetY = remember { Animatable(initialValue = initY) }
val interactionSource = remember { MutableInteractionSource() }
val clickable = Modifier.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
) { }
val isPressed by interactionSource.collectIsPressedAsState()
val size = animateSizeAsState(
targetValue = if (enable.value && !isPressed) {
Size(width = cardInitWidth, height = cardInitHeight)
} else {
Size(width = cardInitWidth + expandValue, height = cardInitHeight + expandValue)
}
)
Box(
Modifier
.offset {
IntOffset(
x = offsetX.value.roundToInt(),
y = offsetY.value.roundToInt()
)
}
.zIndex(zIndex = if (enable.value && !isPressed) 5f else 0f)
.size(size.value.width.dp, size.value.height.dp)
.clip(shape)
//.background(Color(0xFF5FA777))
.background(color = MaterialTheme.colors.primary)
.border(BorderStroke(2.dp, Color.Black), shape = shape)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
enable.value = !enable.value
},
onDrag = { change, dragAmount ->
change.consumeAllChanges()
coroutineScope.launch {
offsetX.snapTo(targetValue = offsetX.value + dragAmount.x)
offsetY.snapTo(targetValue = offsetY.value + dragAmount.y)
}
spring(stiffness = Spring.StiffnessHigh, visibilityThreshold = 0.1.dp)
},
onDragEnd = {
enable.value = !enable.value
spring(stiffness = Spring.StiffnessLow, visibilityThreshold = 0.1.dp)
coroutineScope.launch {
launch {
offsetY.animateTo(
targetValue = initY,
animationSpec = tween(
durationMillis = 700,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
}
offsetX.animateTo(
targetValue = initX,
animationSpec = tween(
durationMillis = 700,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
}
}
)
}
.then(clickable)
) {
Row (modifier = Modifier
.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
)
{
Column (modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
)
{
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(text = "init-X: ${initX.toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
Text(text = "init-Y: ${initY.toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(text = "offset-X: ${offsetX.value.roundToInt().toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
Text(text = "offset-Y: ${offsetY.value.roundToInt().toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
content()
}
}
}
}
}
}
The Modifier.zIndex works only for children within the same parent.
In your case you should move this modifier to the topmost Box. To do so you have to move enable and isPressed one level up too, and I would move all the other variables as well - but that's just a matter of taste, I guess.
val enable = remember { mutableStateOf(true) }
val isPressed by interactionSource.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(zIndex = if (enable.value && !isPressed) 5f else 0f)
) {
// ...
I have a page like this:
When one box is focused, it will be scaled. I use Modifier.graphicsLayer() to scale it.
but the scaled box will be covered by other boxes(box01 is covered by box02,box04 and box 05)
what I actually need is: the scaled box covers other boxes,like this:
My Sample Code:
#Composable
fun FocusBox(
title:String,
requester: FocusRequester = FocusRequester(),
modifier: Modifier = Modifier
) {
var boxColor by remember { mutableStateOf(Color.White) }
var scale by remember { mutableStateOf(1f) }
Box(
Modifier
.focusRequester(requester)
.onFocusChanged {
boxColor = if (it.isFocused) Color.Green else Color.Gray
scale = if (it.isFocused) { 1.3f } else { 1f }
}
.focusable()
.graphicsLayer(
scaleX = scale,
scaleY = scale
).background(boxColor)
) {
Text(
text = title,
modifier = Modifier.padding(30.dp),
color = Color.White,
style = MaterialTheme.typography.subtitle2
)
}
}
#Composable
fun FocusScaleBoxDemo(){
Row(modifier = Modifier.padding(30.dp)){
Column{
FocusBox(title = "Box_01")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_02")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_03")
Spacer(modifier = Modifier.padding(5.dp))
}
Spacer(modifier = Modifier.padding(5.dp))
Column{
FocusBox(title = "Box_04")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_05")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_06")
Spacer(modifier = Modifier.padding(5.dp))
}
}
}
Basically you need zIndex to bring view under neighbours. But this modifier only works for one container. So if you only add it to the selected box, neighbour column will still be on top of that. You need to add it to the Column containing selected box too.
I also prettified you code a little bit: try to avoid code repetition as much as possible - you'll decrease mistake chances and increase modifications speed
#Composable
fun FocusScaleBoxDemo() {
val columnsCount = 2
val rowsCount = 3
var focusedColumnIndex by remember { mutableStateOf(0) }
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(30.dp)
) {
for (column in 0 until columnsCount) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.zIndex(if (column == focusedColumnIndex) 1f else 0f)
) {
for (row in 0 until rowsCount) {
val boxIndex = column * rowsCount + row
FocusBox(
title = "Box_${boxIndex + 1}",
onFocused = {
focusedColumnIndex = column
},
)
}
}
}
}
}
#Composable
fun FocusBox(
title: String,
onFocused: () -> Unit,
requester: FocusRequester = remember { FocusRequester() },
) {
var isFocused by remember { mutableStateOf(false) }
val scale = if (isFocused) 1.3f else 1f
Box(
Modifier
.focusRequester(requester)
.onFocusChanged {
isFocused = it.isFocused
if (isFocused) {
onFocused()
}
}
.focusable()
.graphicsLayer(
scaleX = scale,
scaleY = scale
)
.background(if (isFocused) Color.Green else Color.Gray)
.zIndex(if (isFocused) 1f else 0f)
) {
Text(
text = title,
modifier = Modifier.padding(30.dp),
color = Color.White,
style = MaterialTheme.typography.subtitle2
)
}
}
My problem is that i need a tab indicator to match exactly to the text that is above it (from designs):
However, all i managed to do is get something looking like this:
My code:
ScrollableTabRow(
selectedTabIndex = selectedSeason,
backgroundColor = Color.White,
edgePadding = 0.dp,
modifier = Modifier
.padding(vertical = 24.dp)
.height(40.dp),
indicator = { tabPositions ->
TabDefaults.Indicator(
color = Color.Red,
height = 4.dp,
modifier = Modifier
.tabIndicatorOffset(tabPositions[selectedSeason])
)
}
) {
item.seasonList().forEachIndexed { index, contentItem ->
Tab(
modifier = Modifier.padding(bottom = 10.dp),
selected = index == selectedSeason,
onClick = { selectedSeason = index }
)
{
Text(
"Season " + contentItem.seasonNumber(),
color = Color.Black,
style = styles.seasonBarTextStyle(index == selectedSeason)
)
}
}
}
}
Also a little bonus question, my code for this screen is inside lazy column, now i need to have this tab row to behave somewhat like a sticky header(when it gets to the top, screen stops scrolling, but i can still scroll the items inside it)
Thanks for your help
I had the same requirement, and came up with a simpler solution. By putting the same horizontal padding on the Tab and on the indicator, the indicator aligns with the tab's content:
ScrollableTabRow(selectedTabIndex = tabIndex,
indicator = { tabPositions ->
Box(
Modifier
.tabIndicatorOffset(tabPositions[tabIndex])
.height(TabRowDefaults.IndicatorHeight)
.padding(end = 20.dp)
.background(color = Color.White)
)
}) {
Tab(modifier = Modifier.padding(end = 20.dp, bottom = 8.dp),
selected = tabIndex == 0, onClick = { tabIndex = 0}) {
Text(text = "Tab 1!")
}
Tab(modifier = Modifier.padding(end = 20.dp, bottom = 8.dp),
selected = tabIndex == 1, onClick = { tabIndex = 1}) {
Text(text = "Tab 2!")
}
}
Have a look at the provided modifier, it internally computes a width value. If you change the Modifier yourself to the code below you can provide a width value.
fun Modifier.ownTabIndicatorOffset(
currentTabPosition: TabPosition,
currentTabWidth: Dp = currentTabPosition.width
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "tabIndicatorOffset"
value = currentTabPosition
}
) {
val indicatorOffset by animateAsState(
targetValue = currentTabPosition.left,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
)
fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(x = indicatorOffset + ((currentTabPosition.width - currentTabWidth) / 2))
.preferredWidth(currentTabWidth)
}
Now to the point of how to get the width of your Text:
Warning: I think it's not the way to do it but I can't figure out a better one atm.
At first, I create a Composable to provide me the width of its contents.
#Composable
fun MeasureWidthOf(setWidth: (Int) -> Unit, content: #Composable () -> Unit) {
Layout(
content = content
) { list: List<Measurable>, constraints: Constraints ->
check(list.size == 1)
val placeable = list.last().measure(constraints)
layout(
width = placeable.width.also(setWidth),
height = placeable.height
) {
placeable.placeRelative(x = 0, y = 0)
}
}
}
Now I can use it in your example (simplified):
// Needed for Android
fun Float.asPxtoDP(density: Float): Dp {
return (this / (density)).dp
}
fun main(args: Array<String>) {
Window(size = IntSize(600, 800)) {
val (selectedSeason, setSelectedSeason) = remember { mutableStateOf(0) }
val seasonsList = mutableListOf(2020, 2021, 2022)
val textWidth = remember { mutableStateListOf(0, 0, 0) }
// Android
val density = AmbientDensity.current.density
ScrollableTabRow(
selectedTabIndex = selectedSeason,
backgroundColor = Color.White,
edgePadding = 0.dp,
modifier = Modifier
.padding(vertical = 24.dp)
.height(40.dp),
indicator = { tabPositions ->
TabDefaults.Indicator(
color = Color.Red,
height = 4.dp,
modifier = Modifier
.ownTabIndicatorOffset(
currentTabPosition = tabPositions[selectedSeason],
// Android:
currentTabWidth = textWidth[selectedSeason].asPxtoDP(density)
// Desktop:
currentTabWidth = textWidth[selectedSeason].dp
)
)
}
) {
seasonsList.forEachIndexed { index, contentItem ->
Tab(
modifier = Modifier.padding(bottom = 10.dp),
selected = index == selectedSeason,
onClick = { setSelectedSeason(index) }
)
{
val text = #Composable {
Text(
text = "Season $contentItem",
color = Color.Black,
textAlign = TextAlign.Center
)
}
if (index == selectedSeason) {
MeasureWidthOf(setWidth = { textWidth[index] = it }) {
text()
}
} else {
text()
}
}
}
}
}
}
Edit (05.01.2021): Simplified Modifier code
Edit (09.01.2021): Fixed density problem on android and tested on Desktop and Android