Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to draw lovely Weather Animation with Jetpack Compose

2025-03-28 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article mainly introduces how to use Jetpack Compose to draw lovely weather animation related knowledge, the content is detailed and easy to understand, the operation is simple and fast, has a certain reference value, I believe that everyone after reading this article on how to use Jetpack Compose to draw lovely weather animation article will have a harvest, let's take a look.

1. Project background

Recently participated in the ultimate challenge of the Compose Challenge, using Compose to complete a weather app. I have also participated in previous challenges, and I have learned a lot of new things each time. Now ushered in the final challenge, I hope to use the accumulation of this period of time to make more mature works.

Project Challenge

Since there is no artist's assistance, I consider implementing all the UI elements in app, such as various icon, through code, so that the UI will not be distorted at any resolution, and importantly, it can flexibly achieve a variety of animation effects.

In order to reduce the implementation cost, I define the UI element in app as a cartoon style, which can be more easily realized by ghosting:

The above animation does not use gif, lottie, or other static resources, and all the graphics are drawn based on Compose code.

2. MyApp:CuteWeather

App interface is relatively simple, using a single page presentation (challenge requirements), cartoon-style weather animation is relative to similar app features:

Project address: https://github.com/vitaviva/compose-weather

App interface composition

App is vertically divided into several functional areas, each of which involves the use of different Compose API

There are many technical points involved, this paper mainly introduces how to use Compose to draw custom graphics, and based on these graphics to achieve animation, other content has the opportunity to introduce separately.

3. Compose custom drawing

Like regular Android development, in addition to providing various default Composable controls, Compose also provides Canvas for drawing custom UI.

In fact, Canvas-related API is more or less the same on all platforms, but its use on Compose has the following characteristics:

Create and use Canvas declaratively

Provide necessary state and all kinds of APIs through DrawScope

API is easier to use.

Create and use Canvas declaratively

In Compose, Canvas, as a Composable, can be declaratively added to other Composable and configured through Modifier

Canvas (modifier = Modifier.fillMaxSize ()) {/ / this: DrawScope / / internal custom drawing}

The traditional way needs to get the Canvas handle to draw, while Canvas {.} executes the drawing logic and refreshes the UI in the block by state-driven.

Powerful DrawScope

Canvas {...} internally provides the necessary state through DrawScope to get the environment variables needed for current painting, such as size, which we use most often. DrawScope also mentioned a variety of commonly used rendering API, such as drawLine, etc.

Canvas (modifier = Modifier.fillMaxSize ()) {/ / get the width of the current canvas and height val canvasWidth = size.width val canvasHeight = size.height / / draw the line drawLine through size (start = Offset (x=canvasWidth, y = 0f), end = Offset (x = 0f, y = canvasHeight), color = Color.Blue, strokeWidth = 5F / / set line width)}

The drawing effect of the above code is as follows:

4. Easy-to-use API

Traditional Canvas API requires configuration such as Paint; API provided by DrawScope is simpler and more user-friendly.

For example, to draw a circle, the traditional API looks like this:

Public void drawCircle (float cx, float cy, float radius, @ NonNull Paint paint) {/ /...

API provided by DrawScope:

Fun drawCircle (color: Color, radius: Float = size.minDimension / 2.0f, center: Offset = this.center, alpha: Float = 1.0f, style: DrawStyle = Fill, colorFilter: ColorFilter? = null, blendMode: BlendMode = DefaultBlendMode) {.}

It seems that there are more parameters, but in fact, the appropriate default values have been set through size, etc., while eliminating the creation and configuration of Paint, which is more convenient to use.

Use native Canvas

At present, the API provided by DrawScope is not as rich as native Canvas (for example, it does not support drawText, etc.). When it does not meet the needs, you can also use native Canvas objects to draw directly.

DrawIntoCanvas {canvas-> / / nativeCanvas is a native canvas object, and the android platform is android.graphics.Canvas val nativeCanvas = canvas.nativeCanvas}

The basic knowledge of Compose Canvas is introduced above. Let's take a look at the actual use effect combined with the specific examples in app.

First of all, take a look at Rain Water's drawing process.

5. Rainy day effect

The key to rainy weather is how to draw the falling Rain Water.

Drawing of raindrops

Let's first draw the basic unit that makes up Rain Water: raindrops.

After disassembly, the Rain Water effect can be composed of three groups of raindrops, each of which is divided into upper and lower ends, so that a continuous Rain Water effect can be formed during movement. We use drawLine to draw each black line, set the appropriate stokeWidth, and set the circle effect of the endpoint through cap:

@ Composablefun rainDrop () {Canvas (modifier) {val x: Float = size.width / 2 / / x coordinates: 1 to 2 position drawLine (Color.Black, Offset (x, line1y1), / / start Offset (x, line1y2) of line1, / / end point of line1 strokeWidth = width / / set width cap = StrokeCap.Round// head circle) / / line2 as above drawLine (Color.Black, Offset (x, line2y1), Offset (x, line2y2), strokeWidth = width, cap = StrokeCap.Round)}} raindrop falling animation

After the completion of the basic graphics drawing, then for the two line segments to achieve cyclic displacement animation, forming the flow effect of Rain Water.

Take the gap between the two segments as the anchor point of the animation, set its y-axis position according to animationState, let it move from the top of the drawing area to the low end (0 ~ size.hight), and then restart the animation.

By drawing the upper and lower segments based on the anchor point, you can create a continuous raindrop effect.

The code is as follows:

@ Composablefun rainDrop () {/ / Animation (0f ~ 1f) val animateTween by rememberInfiniteTransition () .animateFloat (initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable (tween (durationMillis, easing = LinearEasing) RepeatMode.Restart / / start Animation) Canvas (modifier) {/ / scope: drawing area val width = size.width val x: Float = size.width / 2 / / width/2 is the width of strokCap Set aside the width of the strokCap at the scopeHeight to keep the raindrops round as they move out. Improve visual effect val scopeHeight = size.height-width / 2 / / space: gap between two segments val space = size.height / 2.2f + width / 2 / / clearance sizeval spacePos = scopeHeight * animateTween / / Anchor position changes with animationState val sy1 = spacePos-space / 2 val sy2 = spacePos + space / 2 / / line length val lineHeight = scopeHeight-space / / line1 val line1y1 = max (0f Sy1-lineHeight) val line1y2 = max (line1y1, sy1) / / line2 val line2y1 = min (sy2, scopeHeight) val line2y2 = min (line2y1 + lineHeight, scopeHeight) / / draw drawLine (Color.Black, Offset (x, line1y1), Offset (x, line1y2), strokeWidth = width ColorFilter = ColorFilter.tint (Color.Black), cap = StrokeCap.Round) drawLine (Color.Black, Offset (x, line2y1), Offset (x, line2y2), strokeWidth = width, colorFilter = ColorFilter.tint (Color.Black) Cap = StrokeCap.Round)}} 6.Compose custom layout

The above completes the graphics and animation of a single raindrop, and then we use three raindrops to form the effect of Rain Water.

First of all, it can be assembled using Row+Space, but this method lacks flexibility, and it is difficult to accurately lay out the relative position of three raindrops only through Modifier. So consider switching to Compose's custom layout to improve flexibility and accuracy:

Layout (modifier = modifier.rotate (30f), / / raindrop rotation angle content = {/ / define sub-Composable Raindrop (modifier.fillMaxSize ())}) {measurables, constraints-> / / List of measured children val placeables = measurables.mapIndexed {index, measurable-> / / Measure each children val height = when (index) {/ / make the height of the three raindrops different Increase the sense of mismatch 0-> constraints.maxHeight * 0.8f 1-> constraints.maxHeight * 0.9f 2-> constraints.maxHeight * 0.6f else-> 0f} measurable.measure (constraints.copy (minWidth = 0, minHeight = 0, maxWidth = constraints.maxWidth / 10) / / raindrop width maxHeight = height.toInt (),)} / / Set the size of the layout as big as it can layout (constraints.maxWidth, constraints.maxHeight) {var xPosition = constraints.maxWidth / ((placeables.size + 1) * 2) / / Place children in the parent layout placeables.forEachIndexed {index Placeable-> / / Position item on the screen placeable.place (x = xPosition, y = 0) / / Record the y co-ord placed up to xPosition + = (constraints.maxWidth / (placeables.size + 1) * 0.8f) .roundToInt ()}}

In Compose, the layout of Composable can be customized through Layout {...}, and the child Composable participating in the layout can be defined in content {...}.

Like traditional Android views, custom layouts go through two steps: measure and layout.

Measrue:measurables returns all child Composable,constraints to be measured, similar to MeasureSpec, encapsulating the layout constraints of the parent container on the child elements. Measure child elements in measurable.measure ()

Layout:placeables returns the measured child elements, calls placeable.place () in turn to lay out the raindrops, and reserves the interval of the raindrops on the x-axis through xPosition.

After the layout, rotate the Composable through modifier.rotate (30f) to achieve the final effect:

7.。 Snow weather effect

The key to the effect of snowy days lies in the falling of snowflakes.

The drawing of snowflakes

The drawing of snowflakes is very simple, using a circle to represent a snowflake

Canvas (modifier) {val radius = size / 2 drawCircle (/ / White fill color = Color.White, radius = radius, style = FILL) drawCircle (/ / black border color = Color.Black, radius = radius, style = Stroke (width = radius * 0.5f))} snowflake animation

The process of snowflakes falling is more complicated than that of raindrops, and consists of three animations:

Drop: achieved by changing the position of the y-axis (0f ~ 2.5f)

Drift left and right: realized through the offset of the x-axis of the table (- 1f ~ 1f)

Gradually disappear: implemented by changing alpha (1f ~ 0f)

Control multiple animations synchronously with InfiniteTransition, the code is as follows:

@ Composableprivate fun Snowdrop (modifier: Modifier = Modifier, durationMillis: Int = 1000 / / druation of snowflake falling animation) {/ / Transition val transition = rememberInfiniteTransition () / / 1\. Drop animation: restart animation val animateY by transition.animateFloat (initialValue = 0f, targetValue = 2.5f, animationSpec = infiniteRepeatable (tween (durationMillis, easing = LinearEasing), RepeatMode.Restart)) / / 2\. Drift left and right: reverse animation val animateX by transition.animateFloat (initialValue =-1f, targetValue = 1f, animationSpec = infiniteRepeatable (tween (durationMillis / 3, easing = LinearEasing), RepeatMode.Reverse)) / / 3\. Alpha value: restart animation, ending with 0f val animateAlpha by transition.animateFloat (initialValue = 1f, targetValue = 0f, animationSpec = infiniteRepeatable (tween (durationMillis, easing = FastOutSlowInEasing),) Canvas (modifier) {val radius = size.width / 2 / / the center position changes with AnimationState Realize the effect of snowflake falling val _ center = center.copy (x = center.x + center.x * animateX, y = center.y + center.y * animateY) drawCircle (color = Color.White.copy (alpha = animateAlpha), / / change the value of Alpha to realize the effect of snowflake disappearing center = _ center, radius = radius ) drawCircle (color = Color.Black.copy (alpha = animateAlpha), center = _ center, radius = radius, style = Stroke (width = radius * 0.5f))}}

AnimateY's targetValue is set to 2.5f to make the snowflake's trajectory longer and look more real.

Custom layout of snowflakes

Like raindrops, use Layout custom layout for snowflakes

@ Composablefun Snow (modifier: Modifier = Modifier, animate: Boolean = false,) {Layout (modifier = modifier, content = {/ / put three snowflakes, set different duration respectively Increase the randomness of Snowdrop (modifier.fillMaxSize (), 2200) Snowdrop (modifier.fillMaxSize (), 1600) Snowdrop (modifier.fillMaxSize (), 1800)}) {measurables, constraints-> val placeables = measurables.mapIndexed {index, measurable-> val height = when (index) {/ / snowflake has different height Also to increase randomness 0-> constraints.maxHeight * 0.6f 1-> constraints.maxHeight * 1.0f 2-> constraints.maxHeight * 0.7f else-> 0f} measurable.measure (constraints.copy (minWidth = 0, minHeight = 0) MaxWidth = constraints.maxWidth / 5, / / snowdrop width maxHeight = height.roundToInt ())} layout (constraints.maxWidth, constraints.maxHeight) {var xPosition = constraints.maxWidth / ((placeables.size + 1)) placeables.forEachIndexed {index, placeable-> placeable.place (x = xPosition Y =-(constraints.maxHeight * 0.2) .roundToInt () xPosition + = (constraints.maxWidth / (placeables.size + 1) * 0.9f) .roundToInt ()}

The final effect is as follows:

8. Sunny weather effect

A rotating sun represents the effect of a sunny day.

The drawing of the sun

The figure of the sun consists of a circle in the middle and an equal vertical line around the ring.

Composablefun Sun (modifier: Modifier = Modifier) {Canvas (modifier) {val radius = size.width / 6 val stroke = size.width / 20 / / draw circle drawCircle (color = Color.Black, radius = radius + stroke / 2, style = Stroke (width = stroke),) drawCircle (color = Color.White, radius = radius Style = Fill ) / / draw lineval lineLength = radius * 0.2f val lineOffset = radius * 1.8f (0.7). ForEach {I-> val radians = Math.toRadians (I * 45.0) val offsetX = lineOffset * cos (radians). ToFloat () val offsetY = lineOffset * sin (radians). ToFloat () val x1 = size.width / 2 + offsetX Val x2 = x1 + lineLength * cos (radians). ToFloat () val y1 = size.height / 2 + offsetY val y2 = y1 + lineLength * sin (radians). ToFloat () drawLine (color = Color.Black Start = Offset (x1, y1), end = Offset (x2, y2), strokeWidth = stroke, cap = StrokeCap.Round)}

A vertical line is drawn every 45 degrees. Cos calculates the x-axis coordinates and sin calculates the y-axis coordinates.

The rotation of the sun

The rotation animation of the sun is very simple, just rotate the Canvas constantly through Modifier.rotate.

@ Composablefun Sun (modifier: Modifier = Modifier) {/ / Loop animation val animateTween by rememberInfiniteTransition () .animateFloat (initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable (tween (5000), RepeatMode.Restart) Canvas (modifier.rotate (animateTween)) {/ / rotation animation val radius = size.width / 6 val stroke = size.width / 20 val centerOffset = Offset (size.width / 30) Size.width / 30) / / Center offset / / draw circle drawCircle (color = Color.Black, radius = radius + stroke / 2, style = Stroke (width = stroke), center = center + centerOffset / / Center offset) / /. Slightly}}

In addition, DrawScope also provides API for rotate, which can also achieve the effect of rotation.

Finally, we add an offset to the center of the sun to make the rotation more lively:

9. Combination and switching of animation

The above implements Rain, Snow, Sun and other graphics, and then uses these elements to combine into a variety of weather effects.

Combine graphics into weather

Compose's declarative syntax is very useful for the combination of UI:

For example, if it changes from cloudy to showers, we can place Sun, Cloud, Rain and other elements, and then adjust their positions through Modifier:

@ Composablefun CloudyRain (modifier: Modifier) {Box (modifier.size (200.dp)) {Sun (Modifier.size (120.dp) .offset (140.dp, 40.dp)) Rain (Modifier.size (80.dp) .offset (80.dp, 60.dp) Cloud (Modifier.align (Aligment.Center))}}

Make animation switching more natural

When switching between multiple weather animations, we want to achieve a more natural transition. The idea is to quantify the Modifier information of the elements that make up the weather animation, and then change it through Animation. State assumes that all weather can be composed of Cloud, Sun, and Rain, but with different values of offset, size, and alpha:

ComposeInfodata class IconInfo (val size: Float = 1f, val offset: Offset = Offset (0f, 0f), val alpha: Float = 1f,) / / Weather combination information That is, the location information of Sun, Cloud, Rain data class ComposeInfo (val sun: IconInfo, val cloud: IconInfo, val rains: IconInfo,) {operator fun times (float: Float): ComposeInfo = copy (sun = sun * float, cloud = cloud * float, rains = rains * float) operator fun minus (composeInfo: ComposeInfo): ComposeInfo = copy (sun = sun-composeInfo.sun Cloud = cloud-composeInfo.cloud, rains = rains-composeInfo.rains,) operator fun plus (composeInfo: ComposeInfo): ComposeInfo = copy (sun = sun + composeInfo.sun, cloud = cloud + composeInfo.cloud, rains = rains + composeInfo.rains,)}

As above, ComposeInfo holds the location information of various elements, and operator overloading enables it to calculate the current latest values in Animation.

Next, use ComposeInfo to define the location information of each element for different weather

/ / sunny day val SunnyComposeInfo = ComposeInfo (sun = IconInfo (1f), cloud = IconInfo (0.8f, Offset (- 0.1f, 0.1f), 0f), rains = IconInfo (0.4f, Offset (0.225f, 0.3f), 0f), / / cloudy val CloudyComposeInfo = ComposeInfo (sun = IconInfo (0.1f, Offset (0.75f, 0.2f), alpha = 0f), cloud = IconInfo (0.8f, Offset (0.1f, 0.1f)) Rains = IconInfo (0.4f, Offset (0.225f, 0.3f), alpha = 0f) / / rainy days val RainComposeInfo = ComposeInfo (sun = IconInfo (0.1f, Offset (0.75f, 0.2f), alpha = 0f), cloud = IconInfo (0.8f, Offset (0.1f, 0.1f)), rains = IconInfo (0.4f, Offset (0.225f, 0.3f), alpha = 1f), ComposedIcon

Next, define ComposedIcon and implement different weather combinations according to ComposeInfo

@ Composablefun ComposedIcon (modifier: Modifier = Modifier, composeInfo: ComposeInfo) {/ / ComposeInfo val (sun, cloud, rains) = composeInfo Box (modifier) {/ / apply ComposeInfo to Modifier val _ modifier = remember (Unit) {{icon: IconInfo-> Modifier .offset (icon.size * icon.offset.x) Icon.size * icon.offset.y) .size (icon.size) .alpha (icon.alpha)} Sun (_ modifier (sun)) Rains (_ modifier (rains)) AnimatableCloud (_ modifier (cloud))}} ComposedWeather

Finally, define the ComposedWeather to record the current ComposedIcon and overuse animation when it is updated:

@ Composablefun ComposedWeather (modifier: Modifier, composedIcon: ComposedIcon) {val (cur, setCur) = remember {mutableStateOf (composedIcon)} var trigger by remember {mutableStateOf (0f)} DisposableEffect (composedIcon) {trigger = 1f onDispose {}} / / create animation (0f ~ 1f) to update ComposeInfo val animateFloat by animateFloatAsState (targetValue = trigger, animationSpec = tween (1000)) {/ / when the animation ends Update ComposeWeather to the latest state setCur (composedIcon) trigger = 0f} / / calculate the current ComposeInfo val composeInfo = remember (animateFloat) {cur.composedIcon + (weatherIcon.composedIcon-cur.composedIcon) * animateFloat} according to AnimationState. This is the end of the article on "how to draw lovely weather animation with Jetpack Compose". Thank you for reading! I believe you all have a certain understanding of "how to draw lovely weather animation with Jetpack Compose". If you want to learn more, you are welcome to follow the industry information channel.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report