How to implement sliding animation for selector in Jetpack Compose? - android-jetpack-compose

I have the following code:
#Composable
internal fun ActivityInputBottomSheet() {
val items = listOf("Calories", "Steps", "Water")
var currentActive by remember {
mutableStateOf(items[0])
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier
.clip(CircleShape)
.background(Color.Red),
verticalAlignment = Alignment.CenterVertically
) {
items.forEach {
Text(
it,
modifier = Modifier
.clip(CircleShape)
.background(if (it == currentActive) Color.Blue else Color.Transparent)
.clickable {
currentActive = it
}
.padding(
vertical = Padding.Padding8,
horizontal = Padding.Padding16
),
color = Color.White,
)
}
}
}
}
Here I implemented a selector with 3 options (when user clicks at the one option it becomes active, and all other become inactive). It works correctly, but I want to add animation of sliding from the one option to another while changing state of selector (for example if user clicks at the first option and then at the third, blue background will slide through the first, then second and stop at third option). How can it be achieved? Thanks in advance for any help!

Related

How do I fix ConstraintLayout that include Balloon(gives strange results) in jetpack compose?

I'm trying to make a help pop ups in my first app. the problem that came up after making the pop up work is that the icon which I'm using becomes a button taking up whole screen height.
I'm using the only code I found for balloon popups in jetpack compose.
the layout is fine until I add the BalloonAnchor.
this is the code:
#Composable
fun GiveHelp(helpText: String) {
Surface{
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
ConstraintLayout {
val (icon, text) = createRefs()
Icon(
modifier = Modifier
.constrainAs(icon) {
top.linkTo(parent.top)
start.linkTo(parent.start)
},
painter = painterResource(id = R.drawable.ic_help),
contentDescription = "help Icon"
)
Text(
modifier = Modifier
.constrainAs(text) {
top.linkTo(icon.top)
start.linkTo(icon.end)
bottom.linkTo(icon.bottom)
}
.padding(horizontal = 10.dp),
text = "Is your task:"
)
BalloonAnchor(
reference = icon,
modifier = Modifier
.aspectRatio(0.1f),
balloon = BalloonUtils.getTitleBalloon(
context = context,
title = helpText,
lifecycle = lifecycleOwner
),
onAnchorClick = { balloon, anchor -> balloon.showAlignTop(anchor) }
)
}
}
}
The problem here is the aspectRatio you are using in the Modifier of BalloonAnchor. Try something like Modifier.aspectRatio(0.99f). Using this, your Icon will not take the entire screen height. Or, your can use something like below code to get a desirable look.
BalloonAnchor(
reference = icon,
modifier = Modifier
.height(40.dp),
balloon = BalloonUtils.getTitleBalloon(
context = context,
title = helpText,
lifecycle = lifecycleOwner
),
onAnchorClick = { balloon, anchor -> balloon.showAlignTop(anchor) }
)

how to animate button click in jetpack compose without consuming the click event

I saw similar questions with no great solutions so I thought this might be useful.
I had a problem when I wanted to animate a button the user clicked to make it larger when pressed; not as easy as it sounds because when you get the animation working the onClick event never fires. [Because it depends on the up event I guess]
I found a way to make both the animation and the click work for a icon button and I thought it might work for other cases.
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun RoundIconButton(
modifier: Modifier = Modifier,
imageVector: ImageVector,
onClick: () -> Unit,
tint: Color = Color.Black.copy(alpha = 0.8f),
backgroundColor: Color =
MaterialTheme.colors.background,
elevation: Dp = 4.dp,
contentDescription: String
) {
val interactionSource = remember {
MutableInteractionSource() }
val isPressed by
interactionSource.collectIsPressedAsState()
val transition = updateTransition(targetState
= isPressed, label = "")
val size by transition.animateDp(label = "")
{ state ->
when(state) {
false -> 40.dp
true -> 50.dp
}
}
Card(
modifier = modifier
.padding(all = 4.dp)
.clickable(interactionSource =
interactionSource,indication =
LocalIndication.current,onClick= onClick)
.then(Modifier.size(size)),
shape = CircleShape,
backgroundColor = backgroundColor,
elevation = elevation,
) {
Icon( imageVector = imageVector,
contentDescription = contentDescription,tint
= tint)
}
}

Show keyboard over Scaffold's bottomBar in Jetpack Compose and apply proper inset paddings

I'm using Scaffold for my main screen with a fixed bottomBar that is visible in every screen of the app, and I'm applying the innerPadding of the Scaffold to its content.
I want the keyboard to appear over the bottomBar, and for that I'm applying the imePadding() only to the Scaffold's content.
However, when the keyboard is opened, both the Scaffold's innerPading and imePadding() are applied to the contents padding.
I've tried to go through the Accompanist Insets migration, but no lucky.
Is there anyway that I can prevent it and apply only one or the other?
Here is a piece of my code:
Scaffold(
topBar = { },
bottomBar = { },
modifier = Modifier
.systemBarsPadding()
) { innerPadding ->
Content(
modifier = Modifier
.padding(innerPadding)
.imePadding()
)
}
And this is the result:
With the now, deprecated, Accompanist Insets, I was using the following solution:
val isImeVisible = LocalWindowInsets.current.ime.isVisible
val contentPadding = remember(isImeVisible) {
if (isImeVisible) PaddingValues(top = innerPadding.calculateTopPadding()) else innerPadding
}
According to Accompanist Insets migration, LocalWindowInsets.current.ime should be replaced with WindowInsets.ime.
It doesn't have isVisible for now, until this bug is fixed. Here's how I've re-created it for now:
val WindowInsets.Companion.isImeVisible: Boolean
#Composable
get() {
val density = LocalDensity.current
val ime = this.ime
return remember {
derivedStateOf {
ime.getBottom(density) > 0
}
}.value
}
Usage:
val isImeVisible = WindowInsets.isImeVisible
This should work with your old remember(isImeVisible) code.
Another solution would be to set BringIntoViewRequester to your content inside Scaffold. Then when textField is focused, you could call bringIntoViewRequester.bringIntoView(). This way you wouldn't need to set any paddings.
val bringIntoViewRequester = remember { BringIntoViewRequester() }
Column(
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
) {
TextField(
value = "",
onValueChange = {},
modifier = Modifier
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
delay(350)
bringIntoViewRequester.bringIntoView()
}
}
}
)
}
Try using something like this (WARNING: consumedWindowInsets is Experimental, but it's working):
Scaffold(
topBar = { },
bottomBar = { },
modifier = Modifier
.systemBarsPadding()
) { innerPadding ->
Content(
modifier = Modifier
.consumedWindowInsets(innerPadding)
.padding(innerPadding)
.imePadding()
)
}

How to make custom tab indicator to be a rounded bar in Jetpack Compose

The following picture shows what I want to achieve. I want the tab indicator to be a short rounded bar.
I looked up the implementation of TabRowDefaults.Indicator(), and just made my own one. I just tried to add the clip() modifier, but it didn't work. And I tried to change the order of the modifiers, but still no luck.
And here is my code implementation:
#Composable
fun TabLayout(
tabItems: List<String>,
content: #Composable () -> Unit
) {
var tabIndex by remember { mutableStateOf(0) }
Column {
ScrollableTabRow(
selectedTabIndex = tabIndex,
edgePadding = 0.dp,
backgroundColor = MaterialTheme.colors.background,
contentColor = Blue100,
indicator = { tabPositions ->
Box(
modifier = Modifier
.tabIndicatorOffset(tabPositions[tabIndex])
.height(4.dp)
.clip(RoundedCornerShape(8.dp)) // clip modifier not working
.padding(horizontal = 28.dp)
.background(color = AnkiBlue100)
)
},
divider = {},
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = tabIndex == index,
onClick = { tabIndex = index },
selectedContentColor = Blue100,
unselectedContentColor = Gray200,
text = {
Text(text = item, fontFamily = fontOutfit, fontSize = 18.sp)
}
)
}
}
Divider(
color = Gray50,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
content()
}
}
You applied Modifier.padding between Modifier.clip and Modifier.background, so the rounding is actually applied to the transparent padding. You need to move the padding in front of the clip, or specify the shape with the background:
.background(color = AnkiBlue100, shape = RoundedCornerShape(8.dp))
Read more about why the order of the modifiers matters in this answer

android compose lazy column separate by weight

Is it possible to do weights in Jetpack Compose with lazy column?
I'd like to set it menu item is weighted as 1/n (n = number of menus) of a layout, and the other takes up the remaining 1/n, also.
I want to list it at the same height as the number of menus.
MenuList
#Composable
fun MenuList(
loading: Boolean,
menus: List<Menu>,
onNavigateToMenuDetailScreen: (String) -> Unit
) {
Box(modifier = Modifier
.background(color = MaterialTheme.colors.surface)
.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
if (loading && menus.isEmpty()) {
LoadingShimmer(imageHeight = 800.dp)
}
else if (menus.isEmpty()) {
NothingHere()
}
else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.weight(1F)
) {
itemsIndexed(
items = menus
) { index, menu ->
MenuCard(
menu = menu,
onClick = {
}
)
}
}
}
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = Modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column {
Text(
text = menu.name,
fontSize = 30.sp,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.wrapContentHeight(Alignment.CenterVertically),
style = MaterialTheme.typography.h3
)
}
}
}
In summary, MenuCards are created as many as the number of menu on the MenuList screen, and I hope the height of each MenuCard will be 1/n.
(n = number of menu)
Like, when number of menu is 8,
Just same height to each menu.
Replace your LazyColumn code with BoxWithConstraints and regular Column.
BoxWithConstraints for getting the minHeight or in this case you can say screen height.
Change to something like the below.
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val height = minHeight/menus.size
Column(
modifier = Modifier
.fillMaxSize()
) {
menus.forEachIndexed { index, menu ->
MenuCard(
modifier = Modifier.height(height),
menu = menu,
onClick = {
}
)
}
}
}
MenuCard
#Composable
fun MenuCard(
menu: Menu,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Card(
shape = MaterialTheme.shapes.small,
modifier = modifier
.padding(
bottom = 6.dp,
top = 6.dp
)
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 8.dp
) {
Column(
modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = menu.name,
fontSize = 30.sp,
style = MaterialTheme.typography.h3
)
}
}
}
You will get this
3 items and 6 items
If all you want is a way to divide the screen height in equal proportions to all the n items, you can simply go for a column.
val items = ... //I assume you have access to this
val screenHeight = LocalConfiguration.current.screenHeightDp
Column{
items.forEach{
MenuCard(modifier = Modifier.height(1f / items.size * screenHeight))
}
}
Column and LazyColumn are fundamentally different. LazyColumn's sole purpose is to deal with large datasets by loading and caching only a small window off that set. Now, if there is nothing to be scrolled, LazyColumn is useless, in fact, it is worse.
EDIT:
You could use BoxWithConstraints to get the screen dimensions as well, but it, in this context, would be more of a workaround than a solution. It adds an extra Composable and decreases the readability of the code too, so for getting the screen dimensions, always use the API that was specifically built for the job.

Resources