Skip to content

Instantly share code, notes, and snippets.

@ardakazanci
Created July 19, 2025 06:32
Show Gist options
  • Save ardakazanci/5546bb29d13a1eeedbea99ba9693e87f to your computer and use it in GitHub Desktop.
Save ardakazanci/5546bb29d13a1eeedbea99ba9693e87f to your computer and use it in GitHub Desktop.

Revisions

  1. ardakazanci created this gist Jul 19, 2025.
    174 changes: 174 additions & 0 deletions StretchTabComponent.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,174 @@
    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
    )
    }
    }
    }
    }