Created
July 19, 2025 06:32
-
-
Save ardakazanci/5546bb29d13a1eeedbea99ba9693e87f to your computer and use it in GitHub Desktop.
Stretch Tab Jetpack Compose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data class TabPosition(val left: Float, val right: Float) | |
@Composable | |
fun StretchTabComponent() { | |
val tabs = listOf("SALE", "RENT") | |
var selectedIndex by remember { mutableIntStateOf(0) } | |
val tabPositions = remember { mutableStateListOf<TabPosition>() } | |
val startX = remember { Animatable(0f) } | |
val endX = remember { Animatable(0f) } | |
val scope = rememberCoroutineScope() | |
val density = LocalDensity.current | |
val maxRippleRadius = with(density) { 80.dp.toPx() } | |
LaunchedEffect(tabPositions.size) { | |
if (tabPositions.size == tabs.size) { | |
val target = tabPositions[selectedIndex] | |
startX.snapTo(target.left) | |
endX.snapTo(target.right) | |
} | |
} | |
Box( | |
Modifier | |
.fillMaxSize() | |
.background(Color.White), | |
contentAlignment = Alignment.Center | |
) { | |
Box( | |
Modifier | |
.shadow(4.dp, RoundedCornerShape(12.dp)) | |
.background(Color(0xFF1A1C1D), RoundedCornerShape(12.dp)) | |
.width(250.dp) | |
.height(60.dp) | |
) { | |
Row( | |
Modifier | |
.fillMaxSize() | |
.padding(horizontal = 8.dp), | |
horizontalArrangement = Arrangement.SpaceEvenly, | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
tabs.forEachIndexed { index, title -> | |
val rippleRadius = remember { Animatable(0f) } | |
val rippleAlpha = remember { Animatable(0f) } | |
val textColor by animateColorAsState( | |
targetValue = if (selectedIndex == index) | |
Color.White else Color.LightGray.copy(alpha = 0.5f), | |
animationSpec = tween(durationMillis = 300) | |
) | |
Box( | |
Modifier | |
.weight(1f) | |
.wrapContentWidth(Alignment.CenterHorizontally) | |
.onGloballyPositioned { coords -> | |
val x = coords.positionInParent().x | |
val w = coords.size.width.toFloat() | |
if (tabPositions.size <= index) | |
tabPositions.add(TabPosition(x, x + w)) | |
else | |
tabPositions[index] = TabPosition(x, x + w) | |
} | |
.pointerInput(Unit) { | |
detectTapGestures { | |
if (selectedIndex == index) return@detectTapGestures | |
scope.launch { | |
rippleRadius.snapTo(0f) | |
rippleAlpha.snapTo(0.5f) | |
launch { | |
rippleRadius.animateTo( | |
maxRippleRadius, | |
spring( | |
dampingRatio = Spring.DampingRatioMediumBouncy, | |
stiffness = Spring.StiffnessVeryLow | |
) | |
) | |
} | |
launch { | |
rippleAlpha.animateTo( | |
0f, | |
tween(durationMillis = 1000) | |
) | |
} | |
} | |
val oldIdx = selectedIndex | |
val newIdx = index | |
val target = tabPositions[newIdx] | |
val overshoot = 30f | |
val moveRight = newIdx > oldIdx | |
scope.launch { | |
when { | |
moveRight -> { | |
endX.animateTo( | |
target.right + overshoot, | |
tween(150, easing = LinearEasing) | |
) | |
} | |
else -> { | |
startX.animateTo( | |
target.left - overshoot, | |
tween(150, easing = LinearEasing) | |
) | |
} | |
} | |
when { | |
moveRight -> { | |
startX.animateTo( | |
target.left, | |
tween(200, easing = FastOutSlowInEasing) | |
) | |
} | |
else -> { | |
endX.animateTo( | |
target.right, | |
tween(200, easing = FastOutSlowInEasing) | |
) | |
} | |
} | |
when { | |
moveRight -> { | |
endX.animateTo( | |
target.right, | |
tween(200, easing = FastOutSlowInEasing) | |
) | |
} | |
else -> { | |
startX.animateTo( | |
target.left, | |
tween(200, easing = FastOutSlowInEasing) | |
) | |
} | |
} | |
selectedIndex = newIdx | |
} | |
} | |
}, | |
contentAlignment = Alignment.Center | |
) { | |
Canvas(Modifier.matchParentSize()) { | |
drawCircle( | |
color = Color.White.copy(alpha = rippleAlpha.value), | |
radius = rippleRadius.value, | |
center = Offset(size.width / 2, size.height / 2) | |
) | |
} | |
Text( | |
text = title, | |
color = textColor, | |
fontSize = 16.sp, | |
fontWeight = FontWeight.Normal | |
) | |
} | |
} | |
} | |
Canvas( | |
Modifier | |
.fillMaxSize() | |
.padding(horizontal = 8.dp) | |
) { | |
drawLine( | |
color = Color(0xFFFF2C55), | |
start = Offset(startX.value, size.height - 8.dp.toPx()), | |
end = Offset(endX.value, size.height - 8.dp.toPx()), | |
strokeWidth = 4.dp.toPx(), | |
cap = StrokeCap.Round | |
) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment