step progress bar with android compose - android

Currently I'm trying to find an easy way to implement a step progress bar with compose like this:
Does anyone have experience with that? Is there maybe a good library?

I don't think you need a library from this one, a simple and quick 'do it yourself' solution could be something like this:
#Composable
fun StepsProgressBar(modifier: Modifier = Modifier, numberOfSteps: Int, currentStep: Int) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
for (step in 0..numberOfSteps) {
Step(
modifier = Modifier.weight(1F),
isCompete = step < currentStep,
isCurrent = step == currentStep
)
}
}
}
#Composable
fun Step(modifier: Modifier = Modifier, isCompete: Boolean, isCurrent: Boolean) {
val color = if (isCompete || isCurrent) Color.Red else Color.LightGray
val innerCircleColor = if (isCompete) Color.Red else Color.LightGray
Box(modifier = modifier) {
//Line
Divider(
modifier = Modifier.align(Alignment.CenterStart),
color = color,
thickness = 2.dp
)
//Circle
Canvas(modifier = Modifier
.size(15.dp)
.align(Alignment.CenterEnd)
.border(
shape = CircleShape,
width = 2.dp,
color = color
),
onDraw = {
drawCircle(color = innerCircleColor)
}
)
}
}
#Preview
#Composable
fun StepsProgressBarPreview() {
val currentStep = remember { mutableStateOf(1) }
StepsProgressBar(modifier = Modifier.fillMaxWidth(), numberOfSteps = 5, currentStep = currentStep.value)
}
This will be the result:

I have created a similar function that takes customisable params.
#Composable
fun Track(
items: Int,
brush: (from: Int) -> Brush,
modifier: Modifier = Modifier,
lineWidth: Dp = 1.dp,
pathEffect: ((from: Int) -> PathEffect?)? = null,
icon: #Composable (index: Int) -> Unit,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.zIndex(-1f)
) {
val width = drawContext.size.width
val height = drawContext.size.height
val yOffset = height / 2
val itemWidth = width / items
var startOffset = itemWidth / 2
var endOffset = startOffset
val barWidth = lineWidth.toPx()
repeat(items - 1) {
endOffset += itemWidth
drawLine(
brush = brush.invoke(it),
start = Offset(startOffset, yOffset),
end = Offset(endOffset, yOffset),
strokeWidth = barWidth,
pathEffect = pathEffect?.invoke(it)
)
startOffset = endOffset
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(items) { index ->
Box(
contentAlignment = Alignment.Center,
) {
icon.invoke(index)
}
}
}
}
}

Add jitpack in your build.gradle:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Add compose-stepper library to your app/build.gradle
dependencies {
implementation 'com.github.maryamrzdh:compose-stepper:1.0.0-beta01'
}
Add a Stepper in a composable function
#Composable
fun StepperView() {
val numberOfSteps = 4
var currentStep by rememberSaveable { mutableStateOf(1) }
val titleList= arrayListOf("Step 1","Step 2","Step 3","Step 4")
Stepper(
modifier = Modifier.fillMaxWidth(),
numberOfSteps = numberOfSteps,
currentStep = currentStep,
stepDescriptionList = titleList,
unSelectedColor= Color.LightGray,
selectedColor = Color.Blue,
isRainbow = false
)
For more info go to Stepper using JetPack Compose

Related

Compose ModalBottomSheet with dynamic height and scrollable column could not dragging down when there is no extra space

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, "")
}
}
}
}
)
}
}
}
}

Jetpack compose width of row children

In Jetpack Compose, How to find the width of each child of a Row?
I tried using onGloballyPositioned like below, but the size is wrong.
Also, not sure if this is the optimal one.
I need the width of each child and further processing will be done based on that, the below is a simplified code for example.
#Composable
fun MyTabSample() {
MyCustomRow(
items = listOf("Option 1 with long", "Option 2 with", "Option 3"),
)
}
#Composable
fun MyCustomRow(
modifier: Modifier = Modifier,
items: List<String>,
) {
val childrenWidths = remember {
mutableStateListOf<Int>()
}
Box(
modifier = modifier
.background(Color.LightGray)
.height(IntrinsicSize.Min),
) {
// To add more box here
Box(
modifier = Modifier
.widthIn(
min = 64.dp,
)
.fillMaxHeight()
.width(childrenWidths.getOrNull(0)?.dp ?: 64.dp)
.background(
color = DarkGray,
),
)
Row(
horizontalArrangement = Arrangement.Center,
) {
items.mapIndexed { index, text ->
Text(
modifier = Modifier
.widthIn(min = 64.dp)
.padding(
vertical = 8.dp,
horizontal = 12.dp,
)
.onGloballyPositioned {
childrenWidths.add(index, it.size.width)
},
text = text,
color = Black,
textAlign = TextAlign.Center,
)
}
}
}
}
The size you get from Modifier.onSizeChanged or Modifier.onGloballyPositioned is in px with unit Int, not in dp. You should convert them to dp with
val density = LocalDensity.current
density.run{it.size.width.toDp()}
Full code
#Composable
fun MyCustomRow(
modifier: Modifier = Modifier,
items: List<String>,
) {
val childrenWidths = remember {
mutableStateListOf<Dp>()
}
Box(
modifier = modifier
.background(Color.LightGray)
.height(IntrinsicSize.Min),
) {
val density = LocalDensity.current
// To add more box here
Box(
modifier = Modifier
.widthIn(
min = 64.dp,
)
.fillMaxHeight()
.width(childrenWidths.getOrNull(0) ?: 64.dp)
.background(
color = DarkGray,
),
)
Row(
horizontalArrangement = Arrangement.Center,
) {
items.mapIndexed { index, text ->
Text(
modifier = Modifier
.onGloballyPositioned {
childrenWidths.add(index, density.run { it.size.width.toDp() })
}
.widthIn(min = 64.dp)
.padding(
vertical = 8.dp,
horizontal = 12.dp,
),
text = text,
color = Black,
textAlign = TextAlign.Center,
)
}
}
}
}
However this is not the optimal way to get size since it requires at least one more recomposition. Optimal way for getting size is using SubcomposeLayout as in this answer
How to get exact size without recomposition?
TabRow also uses SubcomposeLayout for getting indicator and divider widths
#Composable
fun TabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
containerColor: Color = TabRowDefaults.containerColor,
contentColor: Color = TabRowDefaults.contentColor,
indicator: #Composable (tabPositions: List<TabPosition>) -> Unit = #Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: #Composable () -> Unit = #Composable {
Divider()
},
tabs: #Composable () -> Unit
) {
Surface(
modifier = modifier.selectableGroup(),
color = containerColor,
contentColor = contentColor
) {
SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
val tabRowWidth = constraints.maxWidth
val tabMeasurables = subcompose(TabSlots.Tabs, tabs)
val tabCount = tabMeasurables.size
val tabWidth = (tabRowWidth / tabCount)
val tabRowHeight = tabMeasurables.fold(initial = 0) { max, curr ->
maxOf(curr.maxIntrinsicHeight(tabWidth), max)
}
val tabPlaceables = tabMeasurables.map {
it.measure(
constraints.copy(
minWidth = tabWidth,
maxWidth = tabWidth,
minHeight = tabRowHeight
)
)
}
val tabPositions = List(tabCount) { index ->
TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
}
layout(tabRowWidth, tabRowHeight) {
tabPlaceables.forEachIndexed { index, placeable ->
placeable.placeRelative(index * tabWidth, 0)
}
subcompose(TabSlots.Divider, divider).forEach {
val placeable = it.measure(constraints.copy(minHeight = 0))
placeable.placeRelative(0, tabRowHeight - placeable.height)
}
subcompose(TabSlots.Indicator) {
indicator(tabPositions)
}.forEach {
it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(0, 0)
}
}
}
}
}

Box overlay in jetpack compose

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)
)
}
}
}
}

How to customize Tabs in Jetpack Compose

I want to customize the look of the tabs in jetpack compose, here is my tabs look like right now
But I want to look my tabs like this:
I am creating tabs like this way
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White
) {
filters.forEachIndexed { index, filter ->
Tab(
text = {
Text(
text = filter.name.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
)
},
selected = pagerState.currentPage == index,
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
)
}
}
How can I achieve that look, I have searched a lot but didn't find any clue, can anyone help?
#Composable
fun CustomTabs() {
var selectedIndex by remember { mutableStateOf(0) }
val list = listOf("Active", "Completed")
TabRow(selectedTabIndex = selectedIndex,
backgroundColor = Color(0xff1E76DA),
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
.clip(RoundedCornerShape(50))
.padding(1.dp),
indicator = { tabPositions: List<TabPosition> ->
Box {}
}
) {
list.forEachIndexed { index, text ->
val selected = selectedIndex == index
Tab(
modifier = if (selected) Modifier
.clip(RoundedCornerShape(50))
.background(
Color.White
)
else Modifier
.clip(RoundedCornerShape(50))
.background(
Color(
0xff1E76DA
)
),
selected = selected,
onClick = { selectedIndex = index },
text = { Text(text = text, color = Color(0xff6FAAEE)) }
)
}
}
}
Result is as in gif.
In addition to Thracian's answer:
If you need to keep the state of tabs after the screen orientation change, use rememberSaveable instead of remember for selectedIndex.
Animated Tabs
Do you want to create something like this?
I tried to use the Material Design 3 library but it makes everything much more difficult, so I created the TabRow from scratch.
You can use this code to save you some time:
#Composable
fun AnimatedTab(
items: List<String>,
modifier: Modifier,
indicatorPadding: Dp = 4.dp,
selectedItemIndex: Int = 0,
onSelectedTab: (index: Int) -> Unit
) {
var tabWidth by remember { mutableStateOf(0.dp) }
val indicatorOffset: Dp by animateDpAsState(
if (selectedItemIndex == 0) {
tabWidth * (selectedItemIndex / items.size.toFloat())
} else {
tabWidth * (selectedItemIndex / items.size.toFloat()) - indicatorPadding
}
)
Box(
modifier = modifier
.onGloballyPositioned { coordinates ->
tabWidth = coordinates.size.width.toDp
}
.background(color = gray100, shape = Shapes.card4)
) {
MyTabIndicator(
modifier = Modifier
.padding(indicatorPadding)
.fillMaxHeight()
.width(tabWidth / items.size - indicatorPadding),
indicatorOffset = indicatorOffset
)
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
items.forEachIndexed { index, title ->
MyTabItem(
modifier = Modifier
.fillMaxHeight()
.width(tabWidth / items.size),
onClick = {
onSelectedTab(index)
},
title = title
)
}
}
}
}
#Composable
private fun MyTabIndicator(
modifier: Modifier,
indicatorOffset: Dp,
) {
Box(
modifier = modifier
.offset(x = indicatorOffset)
.clip(Shapes.card4)
.background(white100)
)
}
#Composable
private fun MyTabItem(
modifier: Modifier,
onClick: () -> Unit,
title: String
) {
Box(
modifier = modifier
.clip(Shapes.card4)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { onClick() },
contentAlignment = Alignment.Center
) {
Text(text = title)
}
}
Usage:
var selectedTab by remember { mutableStateOf(0) }
AnimatedTab(
modifier = Modifier
.height(40.dp)
.fillMaxSize(.9f),
selectedItemIndex = selectedTab,
onSelectedTab = { selectedTab = it },
items = listOf("first", "second")
)

How to bring a widget front when it is focused(or selected ) in jetpack compose

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
)
}
}

Categories

Resources