On this article, I will present you the right way to construct the account switcher that Google makes use of in its apps.
When you’ve got a number of Google accounts, you’ll be able to merely change them by swiping on the account’s image.
Let’s first analyze how the element works. That is the way it seems to be on Gmail, however on different apps like Drive or Calendar, the interface might look a bit completely different, however the performance stays the identical.
Right here is similar animation in sluggish movement.
We will see that the present account’s picture slides out and the brand new account’s picture shrinks. In the event you swipe up, the present picture will slide down, should you swipe down, the present picture will slide up.
For simplicity I created a Account
class makes use of picture drawable however in an actual software it may be picture url.
information class Account(@DrawableRes val picture: Int)
non-public val accounts = listOf(
Account(picture = R.drawable.goat),
Account(picture = R.drawable.horse),
Account(picture = R.drawable.monkey)
)
Then I created AccountSwitcher
the element receives a listing of accounts, the present account, and a callback that known as when the account modifications.
@Composable
enjoyable AccountSwitcher(
accounts: Record,
currentAccount: Account,
onAccountChanged: (Account) -> Unit,
modifier: Modifier = Modifier
) {
...
}
Inside AccountSwitcher
I’ve outlined a couple of variables.
val imageSize = 36.dp
val imageSizePx = with(LocalDensity.present) { imageSize.toPx() }
val currentAccountIndex = accounts.indexOf(currentAccount)
var nextAccountIndex by bear in mind { mutableStateOf(null) }
var delta by bear in mind(currentAccountIndex) { mutableStateOf(0f) }
val draggableState = rememberDraggableState(onDelta = { delta = it })
val targetAnimation = bear in mind { Animatable(0f) }
imageSize
simply the dimensions you need the picture to be. It’s also possible to take that as a parameter within the constructor in order for you the element to be extra reusable. imageSizePx
identical dimension however in pixels, animation works with pixels as a substitute of descending.
currentAccountIndex
is the present account index, nextAccountIndex
comprises the index of the following account, which defaults to null as a result of it solely comprises the worth when the component is animating.
delta
is the draggable delta, it’s powered by rememberDraggableState
created beneath.
targetAnimation
is the worth that we are going to use for the animation, it ranges from 0 to -1 and from 0 to +1. It goes to -1 should you scroll up and +1 should you scroll down.
LaunchedEffect(key1 = currentAccountIndex) {
snapshotFlow { delta }
.filter { nextAccountIndex == null }
.filter { it.absoluteValue > 1f }
.throttleFirst(300)
.map { delta ->
if (delta < 0) { // Scroll down (Backside -> Prime)
if (currentAccountIndex < accounts.dimension - 1) 1 else 0
} else { // Scroll up (Prime -> Backside)
if (currentAccountIndex > 0) -1 else 0
}
}
.filter { it != 0 }
.acquire { change ->
nextAccountIndex = currentAccountIndex + change
targetAnimation.animateTo(
change.toFloat(),
animationSpec = tween(easing = LinearEasing, durationMillis = 200)
)
onAccountChanged(accounts[nextAccountIndex!!])
nextAccountIndex = null
targetAnimation.snapTo(0f)
}
}
Code inside LaunchedEffect
is what makes the animation occur. I’ll clarify line by line.
First we used snapshotFlow
To look at MutableState
as one Circulation
. Then we test if nextAccountIndex
is null, which suggests no animation is happening. We do not need to begin one other animation if an animation is already in progress.
Then I exploit filter
simply maintain delta
values higher than 1, which skips random reels. Then I exploit throttleFirst
solely get 1 worth each 300 milliseconds, that is as a result of delta
modifications so much as you swipe and I am solely within the first worth.
I exploit map
to test if scrollable and return +1 if consumer scrolls down and we have to animate the following account, -1 if the consumer scrolls up and we have to animate the earlier account then or 0 if there isn’t a account earlier than or after.
Then I simply filter the values if the change is non-zero, we need not animate something if nothing modifications.
Lastly in acquire
block animation from taking place. I begin by setting nextAccountIndex
is the present account index +1 or -1. That causes the structure to rearrange and present the following account’s picture beneath the present account’s picture as you will see beneath. then i referred to as targetAnimation.animateTo
it is a blocking name that solely returns when the animation ends.
After the animation ended, I referred to as onAccountChanged
to inform dad and mom that the account has modified, set nextAccountIndex
to null as a result of nothing else occurs and reset it targetAnimation
to 0.
enjoyable Circulation.throttleFirst(periodMillis: Lengthy): Circulation {
require(periodMillis > 0) { "interval ought to be optimistic" }
return circulate {
var lastTime = 0L
acquire { worth ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastTime >= periodMillis) {
lastTime = currentTime
emit(worth)
}
}
}
}
I discussed throttleFirst
within the earlier paragraph however it’s a customized extension perform i discovered on this publish. Additionally, you will have to repeat/paste it into your venture.
Now let’s discuss concerning the UI for the element.
Field(modifier = Modifier.dimension(imageSize)) {
nextAccountIndex?.let { index ->
Picture(
painter = painterResource(id = accounts[index].picture),
contentScale = ContentScale.Crop,
contentDescription = "Account picture",
modifier = Modifier
.graphicsLayer {
scaleX = abs(targetAnimation.worth)
scaleY = abs(targetAnimation.worth)
}
.clip(CircleShape)
)
}
Picture(
painter = painterResource(id = accounts[currentAccountIndex].picture),
contentScale = ContentScale.Crop,
contentDescription = "Account picture",
modifier = Modifier
.draggable(
state = draggableState,
orientation = Orientation.Vertical,
)
.graphicsLayer {
this.translationY = targetAnimation.worth * imageSizePx * -1.5f
}
.clip(CircleShape)
)
}
nextAccountIndex
can be completely different from null if we had been animating a brand new account. In that case we are able to present the following account’s picture beneath the present one as a result of that is how the animation works.
As I mentioned earlier than, targetAnimation
change from 0 to -1 and from 0 to +1. scaleX
And scaleY
should be optimistic numbers between 0 and 1 for us to get absolutely the worth of the animation.
The present picture simply slides out, to ensure that the entire picture to slip out we have now to maneuver it in accordance with its dimension. For this animation, it is 1 * imageSizePx
. I am multiplying it by a damaging worth as a result of when changing from 0 to 1, I would like translationY
go from 0 to -imageSizeP
x. I am multiplying it by 1.5 as a result of I would like the animation to finish first than the dimensions animation.
Ensure you apply clip(CircleShape)
Later graphicsLayer
in any other case it will not work.
We simply want to connect draggable
modifier for the present picture is supplied, the following picture will change into the present picture when recombinable.
That is all for AccountSwitcher
ingredient. If you wish to use it, you’ll be able to merely save the chosen account to a variable and replace it.
var selectedAccount by bear in mind { mutableStateOf(accounts[0]) }
AccountSwitcher(
accounts = accounts,
currentAccount = selectedAccount,
onAccountChanged = { selectedAccount = it }
)
Here is how this element seems to be:
It would not animate precisely just like the Google element however that is a private alternative. In order for you, you’ll be able to tweak the animation a bit to make it animate the best way you need.