Android Kaki

Build beautiful, usable products using required Components for Android.

Jetpack Composer Information: Animated Navigation Bar | by exyte | Could 2023


waste

ProAndroidDev

Methods to implement navbar with easy customized animation

In Exyte We like to problem ourselves and do elaborate design animations once we come throughout one thing we actually like. Typically it turns into an article in collection copy, and typically it turns into a library. This time we discovered a fantastic design by Yeasin Arafat @dribbble and determined to repeat it for each iOS And Android to match the convenience of implementation in SwiftUI and Jetpack Compose. As standard, right here is an accompanying article on how we do it, this one is for Jetpack Compose.

The principle thought is to create an animated resizing and place of the ball and indentation.

Bar
First, let’s check out the bar itself. In material3 design we now have APIs like:

@Composable
enjoyable NavigationBar(
modifier: Modifier = Modifier,
containerColor: Colour = NavigationBarDefaults.containerColor,
contentColor: Colour = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = NavigationBarDefaults.Elevation,
windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,
content material: @Composable RowScope.() -> Unit
): Unit

Instance of API utilization:

AnimatedNavigationBar(modifier = modifier) {
dropletButtons.forEachIndexed { index, it ->
DropletButton(
modifier = Modifier.fillMaxSize(),
isSelected = worth == index
)
}
}

With a view to animate the ball and indentation and place them of their right positions, we have to know the place of the gadgets (buttons). Location data could be obtained from the surface utilizing onGloballyPositionedhowever we do not wish to power library customers to put in writing extra boilerplate code, so we type the weather ourselves and get their place utilizing a customized format. For every format, you want a measurement coverage that defines how the weather are positioned. on this enjoyable, we simply hold the coverage in thoughts to keep away from pointless computations. The callback is required to keep up the place of the weather as they’re queued.


@Composable
enjoyable animatedNavBarMeasurePolicy(
onBallPositionsCalculated: (ArrayList) -> Unit
) = bear in mind {
barMeasurePolicy(onBallPositionsCalculated = onBallPositionsCalculated)
}

IN barMeasurePolicy we first measure the elements utilizing max.Widththen place them and fill the array with their horizontal coordinates.

MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints.copy(maxWidth = itemWidth))
}
format(constraints.maxWidth, peak) {
var xPosition = hole
val positions = arrayListOf()
placeables.forEachIndexed { index, _ ->
placeables[index].placeRelative(xPosition, 0)
positions.add(calculatePointPosition(xPosition, placeables[index].width))
xPosition += placeables[index].width + hole
}
onBallPositionsCalculated(positions)
}
}

We retailer information of things in itemPositions :

var itemPositions by bear in mind { mutableStateOf(listOf()) }
val measurePolicy = animatedNavBarMeasurePolicy {
itemPositions = it.map { xCord ->
Offset(xCord, 0f)
}
}

Then we calculate the offset of the chosen merchandise:

val selectedItemOffset by bear in mind(selectedIndex, itemPositions) {
derivedStateOf {
if (itemPositions.isNotEmpty()) itemPositions[selectedIndex] else Offset.Unspecified
}
}

Now we outline the animation factors for the ball and the indent, for this we now have to contemplate the form of the stick. In NavBar’s format we use coverage measures created above. animateIndentShapeAsState create a form and its change animation, and we use graphicsLayer to use shapes.

val indentShape = indentAnimation.animateIndentShapeAsState(
cornerRadius = cornerRadius.toPxf(density),
targetOffset = selectedItemOffset
)
Structure(
modifier = Modifier
.graphicsLayer {
clip = true
form = indentShape.worth
}
.background(Colour.Clear),
content material = content material,
measurePolicy = measurePolicy
)

Form
Let’s speak about constructing the form itself. First, we create a form by doing Form show:

class IndentRectShape(
personal val indentShapeData: IndentShapeData,
) : Form {
override enjoyable createOutline(
dimension: Dimension,
layoutDirection: LayoutDirection,
density: Density
): Define =
Define.Generic(
Path().addRoundRectWithIndent(dimension, indentShapeData, layoutDirection)
)
}

Let’s check out addRoundRectWithIndent operate – it creates a path. First, we add IndentPaththen we add extra traces and arcs to create a rounded rectangle contained in the indent. xOffset tells us the coordinates the place the indentation is situated. Complete addRoundRectWithIndent operate code obtainable This.

enjoyable Path.addRoundRectWithIndent(
dimension: Dimension,
indentShapeData: IndentShapeData,
): Path {
return apply {


addPath(
IndentPath(
Rect(
Offset(
x = xOffset,
y = 0f
),
Dimension(indentShapeData.width, indentShapeData.peak)
)
).createPath()
)
lineTo(width - cornerRadius, 0f)
arcTo(
rect = Rect(offset = Offset(width - cornerRadius, 0f), dimension = arcRectSize),
startAngleDegrees = 270f,
sweepAngleDegrees = sweepAngleDegrees,
forceMoveTo = false
)
//different arcs and contours
}
}

Let’s test the indent. Bezier curve factors are supplied by our designer, however they’re solely obtainable for a particular dimension of the design. So we develop a layer that adjusts the dimensions primarily based on a sure space — in different phrases, it scales the indent to the specified dimension of a given rectangle.

class IndentPath(
personal val rect: Rect,
) {
personal val maxX = 110f
personal val maxY = 34f
personal enjoyable translate(x: Float, y: Float): PointF {
return PointF(
((x / maxX) * rect.width) + rect.left,
((y / maxY) * rect.peak) + rect.prime
)
}


enjoyable createPath(): Path {
val begin = translate(x = 0f, y = 0f)
val center = translate(x = 55f, y = 34f)
val finish = translate(x = 110f, y = 0f)


val control1 = translate(x = 23f, y = 0f)
val control2 = translate(x = 39f, y = 34f)
val control3 = translate(x = 71f, y = 34f)
val control4 = translate(x = 87f, y = 0f)


val path = Path()
path.moveTo(begin.x, begin.y)
path.cubicTo(control1.x, control1.y, control2.x, control2.y, center.x, center.y)
path.cubicTo(control3.x, control3.y, control4.x, control4.y, finish.x, finish.y)


return path
}
}

Animations
Now we have created skins that assist in including several types of animations. For the indent place animation we created IndentAnimation show:

interface IndentAnimation {
/**
*@param [targetOffset] goal offset
*@param [shapeCornerRadius] nook radius of the navBar format
*/
@Composable
enjoyable animateIndentShapeAsState(
targetOffset: Offset,
shapeCornerRadius: ShapeCornerRadius
): State

}

Check out these interface-based animations:

Straight indent animation

Within the code beneath, we do the next:

  1. Set vertical offset, the ball is positioned barely above the navbar format by design.

2. Animate the offset.

3. Return productState when offset modifications.

4. Implement a assist operate to help the calculation of shadow deviation.

@Secure
class Straight(
personal val animationSpec: AnimationSpec
) : BallAnimation {
@Composable
override enjoyable animateAsState(targetOffset: Offset): State {
if (targetOffset.isUnspecified) {
return bear in mind { mutableStateOf(BallAnimInfo()) }
}
val density = LocalDensity.present
//1 vertical offset, the ball positioned barely above the nav bar format as meant by the designer
val verticalOffset = bear in mind { 2.dp.toPxf(density) }
val ballSizePx = bear in mind { ballSize.toPxf(density) }
//2 animate offset
val offset = animateOffsetAsState(
targetValue = calculateOffset(targetOffset, ballSizePx, verticalOffset),
animationSpec = animationSpec
)
//3 produce state, when the offset modifications
return produceState(
initialValue = BallAnimInfo(),
key1 = offset.worth
) {
this.worth = this.worth.copy(offset = offset.worth)
}
}


//4 helper operate that assists in calculating the ball's offset.
personal enjoyable calculateOffset(
offset: Offset, ballSizePx: Float, verticalOffset: Float
) = Offset(
x = offset.x - ballSizePx / 2f, y = offset.y - verticalOffset
)
}

Depth change animation

Now we have additionally included an animation of the depth of the indentation. It’s kind of extra difficult since we now have two animation steps.

  1. The indent depth decreases when the consumer leaves the present state of the navigation bar by urgent one other button.
  2. The indent depth then will increase when the consumer presses one other button and creates a brand new state of the navbar. First, we declare the fraction to be animated, and we specify the beginning and finish factors of the animation.
val fraction = bear in mind { Animatable(0f) }
var to by bear in mind { mutableStateOf(Offset.Zero) }
var from by bear in mind { mutableStateOf(Offset.Zero) }

Then the logic of the animation begins. On this case, when a brand new vacation spot (targetOffset) seems, we have to begin a brand new animation or change the factors. We animate as much as 2f as a result of every animation will get 1 float worth (from 0-1 to exit and 1-2 to enter). Let’s discover this a bit of additional:

LaunchedEffect(targetOffset) {
when {
isNotRunning(fraction.worth) -> {
setNewAnimationPoints()
}
isExitIndentAnimating(fraction.worth) -> {
changeToAnimationPointWhileAnimating()
}
isEnterIndentAnimating(fraction.worth) -> {
changeToAndFromPointsWhileAnimating()
}
}
fraction.animateTo(2f, animationSpec)
}

Let us take a look at the three totally different circumstances we used above. First is isExitIndentAnimating. It runs when the consumer has modified the vacation spot and the exit level (“from”) has not completed its animation.

personal enjoyable isExitIndentAnimating(fraction: Float) = (fraction <= 1f)

On this case, we simply want to vary the vacation spot. And the animation ends on the new level with none downside.

enjoyable changeToAnimationPointWhileAnimating() {
to = targetOffset
}

Now take into account a extra advanced case: isEnterIndentAnimating. As soon as the animation has occurred on the vacation spot and at that time a brand new animation targetOffset incoming worth (consumer selects a brand new merchandise of navbar). On this case, it’s mandatory to begin lowering the depth of the vacation spot after which begin the animation on the new vacation spot.

personal enjoyable isEnterIndentAnimating(fraction: Float) = (fraction > 1f)

We have to begin the inverse peak discount animation on the “arrival” level. So from the total a part of this animation we subtract the crimson half proven on the picture above and begin a brand new animation from this level.

The final case, isNotRunningis when there isn’t a present animation and we simply want to begin a brand new one.

personal enjoyable isNotRunning(fraction: Float) = fraction == 0f || fraction == 2f
droop enjoyable setNewAnimationPoints() {
from = to
to = targetOffset
fraction.snapTo(0f)
}

Lastly, we create an IndentRectShape state. produceState is named each time the fraction and shapeCornerRadius change.

return produceState(
initialValue = IndentRectShape(
indentShapeData = IndentShapeData(
ballOffset = ballSize.toPxf(density) / 2f,
width = indentWidth.toPxf(density),
)
),
key1 = fraction.worth,
key2 = shapeCornerRadius
) {
this.worth = this.worth.copy(
yIndent = calculateYIndent(fraction.worth,density),
xIndent = if (fraction.worth <= 1f) from.x else to.x,
cornerRadius = shapeCornerRadius
)
}
personal enjoyable calculateYIndent(fraction: Float, density: Density): Float {
return if (fraction <= 1f) {
lerp(indentHeight.toPxf(density), 0f, fraction)
} else {
lerp(0f, indentHeight.toPxf(density), fraction - 1f)
}
}

The ball and its animation

Now let’s discuss in regards to the ball and its animation. For this we use ballAnimInfoState, might be described later. See how we make the ball and use it in an artificial means enjoyable:

ColorBall(
ballAnimInfo = ballAnimInfoState.worth,
ballColor = ballColor,
sizeDp = ballSize
)
@Composable
personal enjoyable ColorBall(
modifier: Modifier = Modifier,
ballColor: Colour,
ballAnimInfo: BallAnimInfo,
sizeDp: Dp,
) {
Field(
modifier = modifier
.ballTransform(ballAnimInfo)
.dimension(sizeDp)
.clip(form = CircleShape)
.background(ballColor)
)
}

With ballTransform modifier, we modify its place and scale:

enjoyable Modifier.ballTransform(ballAnimInfo: BallAnimInfo) = this
.offset {
IntOffset(
x = ballAnimInfo.offset.x.toInt(),
y = ballAnimInfo.offset.y.toInt()
)
}
.graphicsLayer {
scaleY = ballAnimInfo.scale
scaleX = ballAnimInfo.scale
transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f)
}

Now let’s speak about animations. As with indent, we now have some predefined and fundamental animation choices BallAnimation show.

interface BallAnimation {
/**
*@param [targetOffset] goal offset
*/
@Composable
enjoyable animateAsState(targetOffset: Offset): State
}

BallAnimInfo accommodates offset and price data.

information class BallAnimInfo(
val scale: Float = 1f,
val offset: Offset = Offset.Unspecified
)

Straight shadow animation

Now let’s have a look at how straight animation works. We merely animate the offset and transition BallAnimInfo parameters, and that is all.

val offset = animateOffsetAsState(
targetValue = calculateOffset(targetOffset, ballSizePx, verticalOffset),
animationSpec = animationSpec
)
//produce state, when the offset modifications
return produceState(
initialValue = BallAnimInfo(),
key1 = offset.worth
) {
this.worth = this.worth.copy(offset = offset.worth)
}

Animated teleport ball

The precept of the ‘teleport’ animation is not any totally different from the indent peak animation, we once more set the “from” and “to” factors the identical means, solely the animation is totally different — on this case , we modify the ratio.

return produceState(
initialValue = BallAnimInfo(),
key1 = fraction.worth
) {
this.worth = this.worth.copy(
scale = if (fraction.worth < 1f) 1f - fraction.worth else fraction.worth - 1f,
offset = if (fraction.worth < 1f) from else to
)
}

Parabola ball animation

Probably the most fascinating case is the parabola animation. For a extra versatile and smoother arc trajectory, we use bezier curves.

Let’s outline the beginning and ending factors:

var from by bear in mind { mutableStateOf(targetOffset) }
var to by bear in mind { mutableStateOf(targetOffset) }
val fraction = bear in mind { Animatable(0f) }

Arrange the paths and variables wanted to measure them

val path = bear in mind { Path() }
val pathMeasurer = bear in mind { PathMeasure() }
val pathLength = bear in mind { mutableStateOf(0f) }
val pos = bear in mind { floatArrayOf(Float.MAX_VALUE, Float.MAX_VALUE) }
val tan = bear in mind { floatArrayOf(0f, 0f) }

As we mentioned earlier, circumstances have to be thought of when the consumer selects a brand new merchandise whereas the animation is going on. This code takes place inside LaunchedEffectoperate, introduced a bit of later. Right here we measure the place of the ball on the present time and set these coordinates for the place to begin (from). For “arrival”, we set the coordinates of the brand new vacation spot.

measurePosition()
from = Offset(x = pos[0], y = pos[1])
to = targetOffset
peak = maxHeightPx + pos[1]

When the offset is modified, LaunchedEffect is began and does the next:

  1. Calculate the brand new altitude for the orbit.

2. Set the place to begin — if the animation ends or if the animation is at present operating.

3. Create path with trajectory.

4. Begin the animation.

LaunchedEffect(targetOffset) {
//calculate the brand new peak
var peak = if (to != targetOffset) {
maxHeightPx
} else {
startMinHeight
}
//set factors
if (isNotRunning(fraction.worth)) {
from = to
to = targetOffset
} else {
//if animation is in progress
measurePosition()
from = Offset(x = pos[0], y = pos[1])
to = targetOffset
peak = maxHeightPx + pos[1]
}
//create path
path.createParabolaTrajectory(from = from, to = to, peak = peak)
pathMeasurer.setPath(path, false)
pathLength.worth = pathMeasurer.size
//begin animation
fraction.snapTo(0f)
fraction.animateTo(1f, animationSpec)
}

Generate correct offset when altering place, ballSizefraction.worth:

return produceState(
initialValue = BallAnimInfo(),
key1 = pos,
key2 = ballSizePx,
key3 = fraction.worth,
) {
measurePosition()
if (pos[0] == Float.MAX_VALUE) {
BallAnimInfo()
} else {
this.worth = this.worth.copy(
offset = calculateNewOffset(
pos,
ballSizePx,
verticalOffset
)
)
}
}

Let us take a look at the trajectory. We use quadratic Bezier curves to generate it:


personal enjoyable Path.createParabolaTrajectory(from: Offset, to: Offset, peak: Float) {
reset()
moveTo(from.x, from.y)
quadTo(
(from.x + to.x) / 2f,
from.y - peak,
to.x,
to.y
)
}

А the coordinate operate sends the mandatory data to the situation array:

enjoyable measurePosition() {
pathMeasurer.getPosTan(pathLength.worth * fraction.worth, pos, tan)
}

Icon

Now we have already made animated results for the icons and can discover them right here. Two of them are included within the library and the third one is added for instance so you can too create your individual animated icons.

shake icon

To carry out the specified animation, the button must be a bit bigger, whereas its inside wobbles round. The icon is drawn on the canvas, resized, and alpha utilized as wanted for the animation. We additionally use graphicsLayer, as a result of solely when creating a brand new layer will the colour mix mode be utilized. On the canvas, we first draw a background icon, then draw a wobbly circle and apply a SrcIn mix mode. Then we draw the define icon. For higher understanding, right here is an illustrative diagram.

Canvas(
modifier = modifier
.graphicsLayer(
alpha = wiggleButtonParams.worth.alpha,
scaleX = wiggleButtonParams.worth.scale,
scaleY = wiggleButtonParams.worth.scale
)
.fillMaxSize()
.onGloballyPositioned { canvasSize = it.dimension.toSize() },
contentDescription = contentDescription ?: ""
) {
// "wiggle" circle
with(backgroundPainter) {
draw(
dimension = Dimension(sizePx, sizePx),
colorFilter = ColorFilter.tint(colour = backgroundIconColor)
)
}
// background icon
drawCircle(
colour = wiggleColor,
middle = offset.worth,
radius = wiggleButtonParams.worth.radius,
blendMode = BlendMode.SrcIn
)
// define icon
with(painter) {
draw(
dimension = Dimension(sizePx, sizePx),
colorFilter = ColorFilter.tint(colour = outlineColor)
)
}
}

Now we have two totally different animations — one for wobbling and the opposite for entry/exit (alpha and scale).
First, enter and exit the animation:

val enterExitFraction = animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
animationSpec = enterExitAnimationSpec
)

Second, the wiggle animation:

val wiggleFraction = animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
animationSpec = wiggleAnimationSpec
)

The parameters for the animation are

var wiggleButtonParams by bear in mind { mutableStateOf(WiggleButtonParams()) }
val isAnimationRequired by rememberUpdatedState(newValue = isSelected)

We modify the parameters relying on the animation:

return produceState(
initialValue = WiggleButtonParams(),
key1 = enterExitFraction.worth,
key2 = wiggleFraction.worth
) {
this.worth = this.worth.copy(
scale = scaleInterpolator(enterExitFraction.worth),
alpha = alphaInterpolator(enterExitFraction.worth),
radius = if (isAnimationRequired) calculateRadius(
maxRadius = maxRadius * 0.8f,
fraction = radiusInterpolator(wiggleFraction.worth),
minRadius = mildRadius * maxRadius
) else mildRadius * maxRadius
)
}

There are interpolators within the code, however there’s nothing exceptional about them, since we simply want to use the precise interpolations that assist obtain the specified animation curve. For instance scale and radius animation:

enjoyable scaleInterpolator(fraction: Float): Float = 1 + fraction * 0.2f
enjoyable radiusInterpolator(
fraction: Float
): Float = if (fraction < 0.5f) {
fraction * 2
} else {
(1 - fraction) * 2
}

The teardrop button makes use of the identical thought as earlier than, besides this time it is even less complicated. You’ll be able to see the droplet button code right here: DropletButton.kt. let’s take into account ColorButtonsa bit of extra difficult.

ColorButton

Let’s take a better take a look at the animation: in every icon we now have a background (colour shapes), which is scaled and shifted to the specified level within the route the ball is coming from. Moreover, every icon has its personal kind of animation.

To calculate the place the background ought to transfer, we have to perceive the place the ball is coming from and the place the ball goes.

val isFromLeft = bear in mind(prevSelectedIndex, index, selectedIndex) 

The primary case is for the ball to level scenario, and the second case is when the ball leaves that time. We should always animate two backgrounds: leaving wallpaper and incoming wallpaper.

Since each backgrounds have to be animated collectively, there are additionally two circumstances: one for the outgoing background (backside of the picture) and one for the incoming background (the highest of the picture)

Animated fractions:

val fraction = animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
animationSpec = backgroundAnimationSpec,
label = "fractionAnimation",
)

Let’s calculate the required offset:

val offset by bear in mind(isSelected, isFromLeft) {
derivedStateOf {
calculateBackgroundOffset(
isSelected = isSelected,
isFromLeft = isFromLeft,
maxOffset = maxOffset,
fraction = fraction.worth
)
}
}

The signal of the offset depends upon the route, and the worth depends upon the fraction.

personal enjoyable calculateBackgroundOffset(
isSelected: Boolean,
isFromLeft: Boolean,
fraction: Float,
maxOffset: Float
): Float {
val offset = if (isFromLeft) -maxOffset else maxOffset
return if (isSelected) {
lerp(offset, 0f, fraction)
} else {
lerp(-offset, 0f, fraction)
}
}

Right here we’re drawing the background with required scale and offset.

Picture(
modifier = Modifier
.offset(x = background.offset.x + offset.toDp(), y = background.offset.y)
.scale(fraction.worth)
.align(Alignment.Heart),
painter = painterResource(id = background.icon),
contentDescription = contentDescription
)

Have a look at the animated bell icon. Now we have our fraction to rotate the movement.

val fraction = animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
animationSpec = backgroundAnimationSpec,
label = "fractionAnimation",
)

Draw icons:

Icon(
modifier = modifier
.rotationWithTopCenterAnchor(
if (isSelected) degreesRotationInterpolation(
maxDegrees,
rotationFraction.worth
) else 0f
),
painter = painterResource(id = icon),
contentDescription = null,
tint = colour.worth
)

To make it seem like a spinning pendulum we use degreesRotationInterpolation:

personal enjoyable degreesRotationInterpolation(maxDegrees: Float, fraction: Float) =
sin(fraction * 2 * PI).toFloat() * maxDegrees

To rotate within the required anchor (prime to middle) we created an extension modifier:

enjoyable Modifier.rotationWithTopCenterAnchor(levels: Float) = this
.graphicsLayer(
transformOrigin = TransformOrigin(
pivotFractionX = 0.5f,
pivotFractionY = 0.1f,
),
rotationZ = levels
)

Conclusion

Customized navigation bar animations in Android Jetpack Compose are an effective way to boost the consumer expertise of your apps. By introducing your individual animations, you possibly can create a novel look that displays your model or app theme. You’ll be able to customise the period, period and kind of animation to realize the specified impact. With the data gained from this text, you possibly can create customized animations for the navigation bar in your Android app or already use pre-made animations library we now have created.

After all, the principle complexity of this implementation lies within the math, not the Kotlin code. However hopefully it reveals how straightforward it’s to implement advanced animations in Jetpack Compose and you’ll reuse these strategies in your customized UI. As at all times, the ultimate answer is offered as libraryand we hope to see you quickly for extra library And instruct!

John Wick: Chapter 4 (FREE) FULLMOVIE The Super Mario Bros Movie avatar 2 Where To Watch Creed 3 Free At Home Knock at the Cabin (2023) FullMovie Where To Watch Ant-Man 3 and the Wasp: Quantumania Cocaine Bear 2023 (FullMovie) Scream 6 Full Movie
Updated: May 21, 2023 — 6:26 pm

Leave a Reply

Your email address will not be published. Required fields are marked *

androidkaki.com © 2023 Android kaki