Jetpack Compose Reducing internal padding of Tabs - android

I want to reduce padding of a single tab. Following image shows what I want:
What I am getting:
I am currently using the "accompanist-pager" and "accompanist-pager-indicators" with version 0.16.0.
Code:
#Composable
fun Tabs(tabNames: List<String>, pagerState: PagerState, scrollToPage: (Int) -> Unit) {
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = Color.White,
contentColor = Color.Black,
divider = {
TabRowDefaults.Divider(
thickness = 4.dp
)
},
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.customTabIndicatorOffset(tabPositions[pagerState.currentPage]),
height = 4.dp,
color = EmeraldTheme.colors.primary
)
}
) {
tabNames.forEachIndexed { index, name ->
Tab(
text = {
Text(
text = name,
maxLines = 1,
style = globalSearchDefaultTextStyle,
fontWeight = if (pagerState.currentPage == index) FontWeight.Bold else FontWeight.Normal,
color = if (pagerState.currentPage == index) EmeraldColor.Black100 else colorResource(globalSearchR.color.darkGrey20),
)
},
selected = pagerState.currentPage == index,
onClick = {
scrollToPage(index)
}
)
}
Row { Spacer(Modifier.weight(1f, true)) }
}
}

With the current version of TabRow (or ScrollableTabRow) you will not be able to do it. You will need to create your own TabRow composable.
Also, you should probably use a ScrollableTabRow instead of TabRow because TabRow evenly distributes the entire available width for its Tabs. So the content padding for that doesn't matter that much.
You can pretty much copy-paste the entire code for ScrollableTabRow, but modify the bit that sets up the tabConstraints.
It should no longer use the minTabWidth:
val minTabWidth = ScrollableTabRowMinimumTabWidth.roundToPx()

Custom tab is the way to go.
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.Tab
import androidx.compose.material.Text
Tab(selected, onClick) {
Column(
Modifier.padding(10.dp).height(50.dp).fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween
) {
Box(
Modifier.size(10.dp)
.align(Alignment.CenterHorizontally)
.background(color = if (selected) Color.Red else Color.White)
)
Text(
text = title,
style = MaterialTheme.typography.body1,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#Tab(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)

You can use java reflection to change the value of ScrollableTabRowMinimumTabWidth.
And you can upvote here -> https://issuetracker.google.com/issues/226665301
try {
Class
.forName("androidx.compose.material3.TabRowKt")
.getDeclaredField("ScrollableTabRowMinimumTabWidth").apply {
isAccessible = true
}.set(this, 0f)
} catch (e: Exception) {
e.printStackTrace()
}

Related

How to employ BackdropScaffoldState.offset correctly to offset front contentlayer of compose BackdropScaffold

My current android applications main screen contains a androidx.compose.material.BackdropScaffold. The backdrop looks great and functions exactly as it says on the tin.
however I have an issue with the frontLayerContent which contains a list of items.
I allow the user to interact with the frontLayerContent list while the backdrop is revealed, the issue is with the backdrop in the revealed state the user cannot scroll down to see the last item in the frontLayerContent list.
The solution to this issue is to use backdropState.offset in the modifier of the frontLayerContent which I obtain as follows:-
val backdropState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
var offset by (backdropState.offset as MutableState)
and set as follows:-
onBackdropReveal = {
if (!backdropState.isAnimationRunning) {
scope.launch {
if (backdropState.isConcealed) {
offset = backdropState.offset.value
backdropState.reveal()
} else {
offset = backdropState.offset.value
backdropState.conceal()
}
}
}
}
and make the offset value available to other composables with:-
frontLayerContent = {
CompositionLocalProvider(LocalBackdropStateOffset provides offset) {
frontLayerContent()
}
}
Then in my frontLayerContent I retrieve the offset value as use as follows:-
val context = LocalContext.current
val offset = LocalBackdropStateOffset.current
Scaffold(
topBar = { Spacer(modifier = Modifier.height(0.dp)) },
modifier = Modifier.fillMaxSize(),
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.offset { IntOffset(x = 0, y = -offset.toInt()) },
verticalArrangement = Arrangement.Top
) {
LazyVerticalGrid(
modifier = Modifier.padding(10.dp),
columns = GridCells.Adaptive(125.dp),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(state.size) { index ->
this solution is close to what is required, however it looks as though I have made a mistake somewhere as my frontLayerContent list is always vertically offset even when the backdrop is concealed. In fact revealing or concealing the backdrop does not change the amount of offset of my frontLayerContent list.
how do I fix this issue?
how can I correctly set the frontLayerContent list vertical offset depending on whether the backdrop is concealed or revealed?
UPDATE
I only need to "fix" when the backdrop is revealed. therefore i need a conditional configuration of my frontLayerContent Modifier.offset(y = -offset)
how are you supposed to use the backdropState.offset value when correcting the frontLayerContent offset?
as the compose coordinate systems origin is the top left hand corner (x = 0, y = 0) and y dimension increases down the screen and x dimension increases Left to Right. when the backdrop is revealed the offset is a value (on my pixel 5) of approx 600.dp and concealed value of approx 300.dp. why is the concealed offset not 0.dp? is this taking into account the screens TopAppBar?
UPDATE (2)
Heres a GIF I made earlier
UPDATE (3)
This basic sample shows the problem i am having.
where have i made my mistake that stops me being able to scroll down
to see the complete last item when the backdrop is revealed?
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.rememberBackdropScaffoldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.elsevier.daisy.ui.theme.MyTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalMaterial3Api::class)
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
val scope = rememberCoroutineScope()
MyTheme {
BackdropScaffold(
scaffoldState = scaffoldState,
frontLayerScrimColor = Color.Unspecified,
frontLayerElevation = 5.dp,
gesturesEnabled = false,
appBar = {
TopAppBar(
title = { Text("Backdrop") },
navigationIcon = {
if (scaffoldState.isConcealed) {
IconButton(
onClick = {
scope.launch { scaffoldState.reveal() }
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu"
)
}
} else {
IconButton(
onClick = {
scope.launch { scaffoldState.conceal() }
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
)
},
backLayerContent = {
Column {
Text(
text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
Text(
text = "Menu Item 1", modifier = Modifier.padding(8.dp), color = Color.White, style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
}
},
frontLayerContent = {
LazyColumn(modifier = Modifier.fillMaxSize()) {
// Add 5 items
items(55) { index ->
Text(
text = "Item: ${index + 1}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(5.dp)
)
}
}
},
peekHeight = 60.dp,
) {
}
}
}
}
}
I would suggest adding a padding to the LazyColumn/LazyVerticalGrid itself rather than playing with the offset.
Given your example, I would simply add a conditional padding to the LazyColumn:
val padding = if (
scaffoldState.isRevealed &&
!scaffoldState.isAnimationRunning
) {
64.dp
} else {
0.dp
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(bottom = padding)
) {
// Your items here
}
I've used 64.dp here but use whatever you feel is necessary according to your design.
If instead of using that magic number you want to calculate the height of the backLayerContent you can by using the onGloballyPositioned modifier and save the height in a state.
Add this before your BackdropScaffold
var backLayerContentHeight by remember { mutableStateOf(0.dp) }
Get the backLayerContent height:
backLayerContent = {
val localDensity = LocalDensity.current
Column(
modifier = Modifier.onGloballyPositioned {
val heightInPx = it.size.height
val heightInDp = with(localDensity) { heightInPx.toDp() }
backLayerContentHeight = heightInDp
}
) {
...
}
},
Then you can use this value for the padding:
val padding = if (
scaffoldState.isRevealed &&
!scaffoldState.isAnimationRunning
) {
backLayerContentHeight
} else {
0.dp
}
Alternative to BackdropScaffold
You may want to create a custom implementation to get a similar behavior without workarounds.
This custom component should help:
#Composable
fun CustomBackdropScaffold(
modifier: Modifier = Modifier,
appBar: #Composable () -> Unit,
backLayerContent: #Composable () -> Unit,
frontLayerContent: #Composable () -> Unit,
isRevealed: Boolean,
) {
Surface(modifier = modifier) {
Column {
appBar()
AnimatedVisibility(
visible = isRevealed,
enter = expandVertically(
expandFrom = Alignment.Top,
),
exit = shrinkVertically(
shrinkTowards = Alignment.Top,
),
) {
backLayerContent()
}
frontLayerContent()
}
}
}
Here as an example of how to use it:
var isRevealed by remember { mutableStateOf(false) }
CustomBackdropScaffold(
appBar = {
TopAppBar(
title = { Text("Backdrop") },
navigationIcon = {
if (!isRevealed) {
IconButton(
onClick = {
isRevealed = true
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu"
)
}
} else {
IconButton(
onClick = {
isRevealed = false
}
) {
Icon(
Icons.Default.Close,
contentDescription = "Close"
)
}
}
}
)
},
backLayerContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
) {
Text(
text = "Menu Item 1",
modifier = Modifier.padding(8.dp),
color = Color.White,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
Text(
text = "Menu Item 1",
modifier = Modifier.padding(8.dp),
color = Color.White,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Black
)
)
}
},
frontLayerContent = {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
// Add 5 items
items(55, key = { it }) { index ->
Text(
text = "Item: ${index + 1}",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(5.dp)
)
}
}
},
isRevealed = isRevealed
)

Fix Text position in center of Jetpack Compose Button

I'm currently working on a button, which has 3 elements: An icon (with a fixed size), a title (f.e Buy Now!) and the price of the item.
The price which should be displayed is adaptive, this could be €2,00 or €2000,00. The title is supposed to be centered, based on the Button itself, rather than the area it can occupy.
The price of object has the priority within the button, and should always be fully displayed with a set style. Due to this, the size of this object is variable, and can not be determined beforehand.
When the length of the price object increases, naturally the available space of the title decreases. However, when attempting to center the text, I could only get it to center based on the available space, which resulted in the text being off-center.
How could one approach this issue, allowing for the text to be centered based on the parent (button), rather than the available text size?
I tried to prepare an understandable example for you, if it was useful, please select my answer as the correct answer
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AppBarDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintSet
import androidx.constraintlayout.compose.Dimension
import stackoverflow.answers.ui.theme.StackOverflowAnswersTheme
#Composable
private fun StandardToolbar(
onProfileButtonClick: () -> Unit,
onFilterButtonClick: () -> Unit,
onBackButtonClick: () -> Unit
) {
val constraintSet = ConstraintSet {
val startReference = createRefFor("startReference")
val endReference = createRefFor("endReference")
val titleReference = createRefFor("titleReference")
constrain(startReference) {
start.linkTo(parent.start, 16.dp)
top.linkTo(parent.top, 16.dp)
bottom.linkTo(parent.bottom, 16.dp)
width = Dimension.value(48.dp)
}
constrain(endReference) {
end.linkTo(parent.end, 16.dp)
top.linkTo(parent.top, 16.dp)
bottom.linkTo(parent.bottom, 16.dp)
width = Dimension.value(48.dp)
}
constrain(titleReference) {
start.linkTo(startReference.end, 8.dp)
end.linkTo(endReference.start, 8.dp)
top.linkTo(parent.top, 16.dp)
bottom.linkTo(parent.bottom, 16.dp)
width = Dimension.fillToConstraints
}
}
Surface(
elevation = AppBarDefaults.TopAppBarElevation,
shape = RoundedCornerShape(
bottomStart = 50f,
bottomEnd = 50f
),
color = Color(0XFF2F364E),
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
) {
ConstraintLayout(
modifier = Modifier.fillMaxSize(),
constraintSet = constraintSet
) {
Box(
modifier = Modifier
.layoutId("startReference")
.size(48.dp)
.background(Color.Blue)
) {
}
Text(
modifier = Modifier
.layoutId("titleReference"),
text = "Title",
style = MaterialTheme.typography.h5.copy(fontWeight = FontWeight.Bold),
color = Color.White,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
maxLines = 1
)
Box(
modifier = Modifier
.layoutId("endReference")
.size(48.dp)
.background(Color.Green)
) {
Text(text = "E 20,000", modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.caption)
}
}
}
}
#Composable
#Preview
fun StandardToolbarPreview() {
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
StackOverflowAnswersTheme {
StandardToolbar(
onProfileButtonClick = { },
onFilterButtonClick = { },
onBackButtonClick = {}
)
}
}
}
You can try this:
Button(
modifier = Modifier
.wrapContentHeight()
.padding(horizontal = 8.dp),
onClick = {}
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround
) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.TopStart
) {
Icon(
imageVector = Icons.Default.ImageSearch,
contentDescription = null
)
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Text(
text = "Buy Now"
)
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.TopEnd
) {
Text(
text = "€ 2.00"
// text = "€ 2000.00"
)
}
}
}
The Button has a content parameter you can use to set its content, in this case we use a Row to set contents in the horizontal axis.
Note that each of the components, Icon Text and Text are wrapped inside a Box with a weight of 1f, making each those boxes as their container that also takes equal divided space of the parent Row.
The middle Box positions its child in the center, while the first and last Box positions their children (i.e Icon and Text) in TopStart and TopEnd alignment, though you don't need to worry the "top" positioning as its neglected here because the parent Row aligns all its children center-vertically
If we put a background color on each Box,
Modifier.background(Color.LightGray/Gray/DarkGray)
we can clearly see their equal width

Why are all my composables being recomposed?

I am creating a screen with a color picker. Here's the code for the whole screen, including the color picker code. The reason why I'm including the whole screen's code instead of just the color picker's is because I want to give more context.
package com.gitlab.djsushi123.kingenscoreboard.presentation.screen.newplayer
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.gitlab.djsushi123.kingenscoreboard.domain.model.Player
import com.google.accompanist.systemuicontroller.SystemUiController
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
#Composable
fun NewPlayerScreen(
viewModel: NewPlayerScreenViewModel = getViewModel(),
appNavController: NavHostController,
systemUiController: SystemUiController,
modifier: Modifier = Modifier
) {
val coroutineScope = rememberCoroutineScope()
val state = viewModel.state
val animatedSelectedColor by animateColorAsState(
targetValue = state.playerColor,
animationSpec = tween(durationMillis = 600)
)
val primaryVariantColor = MaterialTheme.colors.primaryVariant
val darkIconsDisposable = MaterialTheme.colors.isLight
val useDarkIcons = with(animatedSelectedColor) {
(red + green + blue) / 3f > 0.5f
}
DisposableEffect(key1 = animatedSelectedColor) {
systemUiController.setStatusBarColor(
color = animatedSelectedColor,
darkIcons = useDarkIcons
)
onDispose {
systemUiController.setStatusBarColor(
color = primaryVariantColor,
darkIcons = darkIconsDisposable
)
}
}
Scaffold(
topBar = {
TopBar(
color = animatedSelectedColor,
onColor = if (useDarkIcons) Color.Black else Color.White,
saveButtonEnabled = state.saveButtonEnabled,
onBackButtonClicked = { appNavController.popBackStack() },
onSave = {
val player = Player(
name = state.playerName,
abbreviation = state.playerAbbreviation,
color = state.playerColor
)
coroutineScope.launch {
viewModel.savePlayer(player)
appNavController.popBackStack()
}
}
)
},
modifier = modifier
) { paddingValues ->
Surface(color = MaterialTheme.colors.surface, modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()) // so that we can scroll with the keyboard open
.padding(paddingValues)
.padding(start = 8.dp, top = 8.dp, end = 8.dp)
) {
// Player name field with error
OutlinedTextField(
value = state.playerName,
onValueChange = {
viewModel.onEvent(
NewPlayerFormEvent.PlayerNameChanged(name = it)
)
},
isError = state.playerNameError != null,
label = { Text(text = "Name") },
placeholder = { Text(text = "John Cena") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Words
),
modifier = Modifier.fillMaxWidth()
)
Text(
text = state.playerNameError ?: "",
color = MaterialTheme.colors.error,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth()
)
// Player abbreviation field
OutlinedTextField(
value = state.playerAbbreviation,
onValueChange = {
viewModel.onEvent(
NewPlayerFormEvent.PlayerAbbreviationChanged(abbreviation = it)
)
},
isError = state.playerAbbreviationError != null,
label = { Text(text = "Abbreviation") },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Characters,
autoCorrect = false
),
modifier = Modifier.fillMaxWidth()
)
Text(
text = state.playerAbbreviationError ?: "",
color = MaterialTheme.colors.error,
textAlign = TextAlign.End,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
ColorPicker(
selected = state.playerColor,
onSelect = { color ->
viewModel.onEvent(
NewPlayerFormEvent.PlayerColorChanged(color = color)
)
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
#Composable
private fun TopBar(
color: Color,
onColor: Color,
saveButtonEnabled: Boolean,
onBackButtonClicked: () -> Unit,
onSave: () -> Unit
) {
val saveButtonAlpha by animateFloatAsState(
if (saveButtonEnabled) ContentAlpha.high else ContentAlpha.disabled
)
TopAppBar(
title = { Text(text = "New player", color = onColor) },
navigationIcon = {
IconButton(onClick = onBackButtonClicked) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = onColor
)
}
},
actions = {
IconButton(onClick = onSave, enabled = saveButtonEnabled) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Save",
tint = onColor.copy(alpha = saveButtonAlpha)
)
}
},
backgroundColor = color
)
}
#Composable
private fun ColorPicker(
selected: Color,
onSelect: (color: Color) -> Unit,
modifier: Modifier = Modifier
) {
val colors: List<Color> = remember {
(0..340 step 8).map { hue ->
Color.hsl(hue = hue.toFloat(), saturation = 0.5f, lightness = 0.5f)
}.dropLast(3)
}
Column(modifier = modifier) {
for (row in 0..(colors.size - 1) / 4) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
for (tile in 0 until 4) {
val color = colors[row * 4 + tile]
ColorPickerTile(
color = color,
selected = color == selected,
onClick = { onSelect(color) },
modifier = Modifier
.width(70.dp)
.height(70.dp)
)
}
}
}
}
}
#Composable
private fun ColorPickerTile(
color: Color,
selected: Boolean,
onClick: () -> Unit,
shape: Shape = MaterialTheme.shapes.small,
modifier: Modifier = Modifier
) {
val scale = remember { Animatable(initialValue = 1f) }
val borderWidth by animateDpAsState(
targetValue = if (selected) 6.dp else 0.dp,
animationSpec = tween(durationMillis = 350)
)
LaunchedEffect(key1 = selected) {
if (selected) {
launch {
scale.animateTo(
targetValue = 0.90f,
animationSpec = tween(
durationMillis = 50
)
)
scale.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
}
} else {
launch { scale.animateTo(1f) }
}
}
Box(
modifier = modifier
.scale(scale.value)
.border(
width = borderWidth,
color = if (borderWidth.value == 0f) Color.Transparent else MaterialTheme.colors.onSurface,
shape = shape
)
.clip(shape)
.background(color = color)
.clickable(onClick = onClick)
)
}
My color picker is composed of ColorPickerTiles, which are stacked in a grid that is 4 tiles wide. This grid is a made-up grid from Rows inside a big Column. My problem is related to performance. When we check the "layout inspector" tab in Android Studio, we can see that when the user selects a different color tile in the app, every single ColorPickerTile gets recomposed MULTIPLE TIMES because of an animation I've added. I don't understand why this is necessary and why is this even possible, since the ColorPickerTile composable is restartable and also skippable and none of its parameters changed AFAIK. Here's a screenshot of what I mean:
The fact that these recompositions happen for every single ColorPickerTile many times during an animation means that the whole animation lags, especially on low-end devices. And the lag increases exponentially, with more ColorPickerTiles. My question is, what is causing these useless recompositions? And how would I go about solving the issue?

Left aligned custom tabs with Jetpack Compose

I need to make tabs with Jetpack Compose, looking like horizontal buttons. Tabs should be left aligned, and not centered. Just like in the image.
Also selected tab shouldn't show underline.
Jetpack compose has Scaffold for such case, something like this should work for you
enum class Tab {
Day,
Week,
Month,
}
#Composable
fun TestView(
) {
var selectedTab by remember { mutableStateOf(Tab.Day) }
Scaffold(topBar = {
Row(Modifier.padding(5.dp)) {
Tab.values().forEach { tab ->
BottomBarButton(
tab.name,
selected = selectedTab == tab,
onSelect = {
selectedTab = tab
},
)
}
}
}) {
when (selectedTab) {
Tab.Day -> Text("$selectedTab content")
Tab.Week -> Text("$selectedTab content")
Tab.Month -> Text("$selectedTab content")
}
}
}
#Composable
fun BottomBarButton(
text: String,
selected: Boolean,
onSelect: () -> Unit
) {
Text(
text,
modifier = Modifier
.background(
if (selected)
Color.Green
else
Color.Transparent
)
.clickable(onClick = onSelect)
.padding(10.dp)
)
}
If you need bottom bar, just replace topBar = { with bottomBar = {
See more about Scaffold
How about this one?
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = colorResource(id = R.color.white),
divider = { TabRowDefaults.Divider(color = colorResource(id = R.color.transparent)) },
edgePadding = 0.dp
) {
//draw your tab
}
Try this out
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
#Composable
fun RecentTabs(tabs: List<CustomTabs>, onSelectedTab: (CustomTabs) -> Unit) {
var selectedTab by remember { mutableStateOf("") }
#Composable
fun RecentTabItem(text: String, selectedColor: Color = Color.Green, onSelect: () -> Unit) {
val selected = text == selectedTab
Text(
text,
modifier = Modifier
.clip(CircleShape)
.background(
if (selected)
selectedColor
else
Color.Transparent
)
.clickable(
onClick = {
selectedTab = text
onSelect.invoke()
}
)
.padding(vertical = 8.dp, horizontal = 18.dp)
)
}
Row(
Modifier
.scrollable(rememberScrollState(), orientation = Orientation.Horizontal)
.padding(horizontal = 5.dp, vertical = 8.dp)) {
tabs.forEach {
RecentTabItem(text = it.name, selectedColor = it.color.toColor(Color.Magenta)) { onSelectedTab.invoke(it) }
Spacer(modifier = Modifier.width(5.dp))
}
}
}
Usage:
RecentTabs(tabs = listOf(
CustomeTabs(1,"Tab1", color = Color.Blue.toString()),
CustomeTabs(2,"Tab2", color = Color.Gray.toString()),
CustomeTabs(3,"Tab3", color = Color.Red.toString())
), onSelectedTab = {
Log.d(TAG, "RecentScreen() called ${it.toString()}")
})

Jetpack Compose Fullscreen Dialog

I tried to make a fullscreen dialog using Jetpack Compose using this code:
Dialog(onDismissRequest = { /*TODO*/ }) {
NewPostDialog()
}
It ended up looking something like this. How can I remove the margin at the side (marked red)?
UPDATE: As #Nestor Perez mentioned, since compose 1.0.0-rc01 you can set usePlatformDefaultWidthin DialogProperties to make a dialog fill the whole screenwidth:
Dialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest...
){
Surface(modifier = Modifier.fillMaxSize()) {
DialogContent()
}
}
Compose Dialog uses ContextThemeWrapper so you should be able to theme your dialog with a custom style.
themes.xml:
<style name="Theme.YourApp" parent="Theme.MaterialComponents.Light.NoActionBar">
//theme content...
<item name="android:dialogTheme">#style/Theme.DialogFullScreen</item>
</style>
<style name="Theme.DialogFullScreen" parent="#style/ThemeOverlay.MaterialComponents.Dialog.Alert">
<item name="android:windowMinWidthMajor">100%</item>
<item name="android:windowMinWidthMinor">100%</item>
</style>
And in code:
#Composable
fun FullScreenDialog(showDialog:Boolean, onClose:()->Unit) {
if (showDialog) {
Dialog(onDismissRequest = onClose ) {
Surface(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(16.dp),
color = Color.LightGray
) {
Box(
contentAlignment = Alignment.Center
) {
Text(modifier = Modifier.align(Alignment.TopCenter),
text = "top")
Text("center")
Text(
modifier = Modifier.align(Alignment.BottomCenter),
text = "bottom")
}
}
}
}
}
Solution from jns didnt work too well form me, I leave another solution here if anyone is still looking:
Implement the theme as jns answer:
<style name="Theme.Outlay" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<!-- Customize your theme here. -->
<item name="android:dialogTheme">#style/Theme.DialogFullScreen</item >
</style>
<style name="Theme.DialogFullScreen" parent="#style/ThemeOverlay.MaterialComponents.Dialog.Alert">
<item name="android:windowMinWidthMajor">100%</item>
<item name="android:windowMinWidthMinor">100%</item>
</style>
For the dialog create an scaffold and add the experimental property "usePlatformDefaultWidth = false" on dialog properties:
Dialog(
onDismissRequest = onBackPressed,
properties = DialogProperties(
usePlatformDefaultWidth = false
)
) {
Scaffold(topBar = { TopBar(onBackPressed = onBackPressed) }) {
Content()
}
}
If you want to avoid having an xml theme entirely and also avoid doing this for all dialogs, you can set a requiredWidth modifier to be equal to LocalConfiguration.current.screenWidthDp.dp (multiplied by some fraction as you please).
An example that takes up 0.96f of the screen width:
#Composable
fun LargerDialog(
dialogOpen: MutableState<Boolean>
) {
Dialog(onDismissRequest = { dialogOpen.value = false }) {
Card( // or Surface
elevation = 8.dp,
modifier = Modifier
.requiredWidth(LocalConfiguration.current.screenWidthDp.dp * 0.96f)
.padding(4.dp)
) {
// content
}
}
}
To make a full-screen dialog using Jetpack Compose using this code:
EG1:
Full screen dialog screenshot
package compose.material.theme
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import compose.material.theme.ui.theme.Material3ComposeTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Material3ComposeTheme {
val openFullDialogCustom = remember { mutableStateOf(false) }
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.padding(20.dp)
.verticalScroll(rememberScrollState())
) {
//...................................................................
// * full screen custom dialog
Button(
onClick = {
openFullDialogCustom.value = true
},
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text(text = "No internet",style = MaterialTheme.typography.labelLarge)
}
}
}
//...............................................................................
//Full screen Custom Dialog Sample
NoInternetScreen(openFullDialogCustom)
}
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
private fun NoInternetScreen(openFullDialogCustom: MutableState<Boolean>) {
if (openFullDialogCustom.value) {
Dialog(
onDismissRequest = {
openFullDialogCustom.value = false
},
properties = DialogProperties(
usePlatformDefaultWidth = false // experimental
)
) {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.no_intrenet),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.height(200.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(20.dp))
//.........................Text: title
Text(
text = "Whoops!!",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 20.dp)
.fillMaxWidth(),
letterSpacing = 2.sp,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.height(8.dp))
//.........................Text : description
Text(
text = "No Internet connection was found. Check your connection or try again.",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, start = 25.dp, end = 25.dp)
.fillMaxWidth(),
letterSpacing = 1.sp,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
)
//.........................Spacer
Spacer(modifier = Modifier.height(24.dp))
val cornerRadius = 16.dp
val gradientColor = listOf(Color(0xFFff669f), Color(0xFFff8961))
GradientButton(
gradientColors = gradientColor,
cornerRadius = cornerRadius,
nameButton = "Try again",
roundedCornerShape = RoundedCornerShape(topStart = 30.dp,bottomEnd = 30.dp)
)
}
}
}
}
}
}
//...........................................................................
#Composable
fun GradientButton(
gradientColors: List<Color>,
cornerRadius: Dp,
nameButton: String,
roundedCornerShape: RoundedCornerShape
) {
Button(
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp),
onClick = {
//your code
},
contentPadding = PaddingValues(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
shape = RoundedCornerShape(cornerRadius)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(colors = gradientColors),
shape = roundedCornerShape
)
.clip(roundedCornerShape)
/*.background(
brush = Brush.linearGradient(colors = gradientColors),
shape = RoundedCornerShape(cornerRadius)
)*/
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text = nameButton,
fontSize = 20.sp,
color = Color.White
)
}
}
}

Categories

Resources