Skip to content

Instantly share code, notes, and snippets.

@bmc08gt
Last active March 18, 2024 09:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmc08gt/1d857aef24d60df1524b27dd34c9d402 to your computer and use it in GitHub Desktop.
Save bmc08gt/1d857aef24d60df1524b27dd34c9d402 to your computer and use it in GitHub Desktop.
Compose Multiplatform Segmented Control
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import com.airbnb.sample.theme.dimens
import com.airbnb.sample.utils.ui.NoRippleInteractionSource
@Composable
actual fun <T> SegmentedControl(
modifier: Modifier,
options: List<T>,
selected: T,
onOptionClicked: (T) -> Unit,
titleForItem: (T) -> String,
) {
val contentMargin = MaterialTheme.dimens.staticGrid.x2
val offset by animateFloatAsState(
when (options.indexOf(selected)) {
0 -> 0f
1 -> 1f
2 -> 2f
else -> 0f
}
)
androidx.compose.foundation.layout.Row(
modifier = modifier
.height(IntrinsicSize.Min)
.background(
Color(0xFFEEEEEF),
CircleShape,
)
.drawWithContent {
drawRoundRect(
color = Color.White,
topLeft = Offset(
contentMargin.value + (size.width / 3f * offset),
contentMargin.value
),
size = Size(
this.size.width / 3f - contentMargin.value * 2,
this.size.height - contentMargin.value * 2
),
cornerRadius = CornerRadius(this.size.height / 2)
)
drawContent()
}
.clip(CircleShape),
verticalAlignment = Alignment.CenterVertically,
) {
options.onEachIndexed { index, segment ->
Box(
Modifier
.fillMaxHeight()
.clip(CircleShape)
.clickable(
enabled = true,
onClickLabel = titleForItem(segment),
indication = null,
role = Role.Button,
interactionSource = NoRippleInteractionSource()
) { onOptionClicked(segment) }
.padding(contentMargin)
// Divide space evenly between all segments.
.weight(1f),
contentAlignment = Alignment.Center,
) {
Text(
text = titleForItem(segment),
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.W600)
)
}
}
}
}
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.interop.UIKitView
import androidx.compose.ui.unit.dp
import com.airbnb.sample.utils.ui.addIf
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.convert
import kotlinx.cinterop.useContents
import platform.UIKit.UIAction.Companion.actionWithHandler
import platform.UIKit.UIImageView
import platform.UIKit.UISegmentedControl
import platform.darwin.NSInteger
import platform.darwin.NSUInteger
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun <T> SegmentedControl(
modifier: Modifier,
options: List<T>,
selected: T,
onOptionClicked: (T) -> Unit,
titleForItem: (T) -> String,
) {
val controlManager = remember(options) {
SegmentedControlManager(
options = options,
titleForItem = titleForItem,
onSelectionChanged = onOptionClicked,
)
}
LaunchedEffect(selected) {
if (controlManager.selected != selected) {
controlManager.setSelected(selected)
}
}
UIKitView(
modifier = modifier
.addIf(controlManager.segmentedControlWidth > 0) {
Modifier.width(controlManager.segmentedControlWidth.dp)
}
.addIf(controlManager.segmentedControlHeight > 0) {
Modifier.height(controlManager.segmentedControlHeight.dp)
},
factory = {
return@UIKitView controlManager.controller
}
)
}
@OptIn(ExperimentalForeignApi::class)
class SegmentedControlManager<T> internal constructor(
private val options: List<T>,
private val titleForItem: (T) -> String,
private val onSelectionChanged: (option: T) -> Unit,
) {
var segmentedControlWidth by mutableStateOf(0f)
private set
var segmentedControlHeight by mutableStateOf(0f)
private set
val controller: UISegmentedControl = UISegmentedControl(options.map { it!!::class.simpleName })
init {
options.forEachIndexed { index, item ->
controller.setAction(action = actionWithHandler { onSelectionChanged(item) }, forSegmentAtIndex = index.convert<NSUInteger>())
controller.setTitle(titleForItem(item), forSegmentAtIndex = index.convert<NSUInteger>())
}
controller.selectedSegmentIndex = 0.convert<NSInteger>()
controller.frame.useContents {
segmentedControlWidth = this.size.width.toFloat()
segmentedControlHeight = this.size.height.toFloat()
controller.layer.cornerRadius = size.width / 2
controller.layer.masksToBounds = true
(controller.subviews[0] as? UIImageView)?.layer?.cornerRadius = controller.layer.cornerRadius
}
}
fun setSelected(option: T) {
options.indexOf(option).takeIf { it >= 0 }?.let {
controller.selectedSegmentIndex = it.convert<NSInteger>()
}
}
val selected: T?
get() = controller.selectedSegmentIndex.let {
runCatching { options[it.toInt()] }.getOrNull()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment