# Compose Developer Challenge for 2 weeks

In conjunction with the release of the beta version of Jetpack Compose, Google has officially launched the Compose developer Challenge, which is now in its second week of running android Dev-Challenge-2

The topic for the second week is “Compose” for the countdown app. The topic is appropriate and not difficult, but it can lead you to learn about some of Compose’s features. For example, the implementation of this app requires you to learn about the use of State and animations.

Note: For composing developers who are interested in the Compose developer Challenge and how to participate, please refer to the “Jetpack Compose Developer Challenge”

The following are my completed projects:TikTik

Compose is composed’s basic API. It took a little time to complete, but the result was satisfactory. It is clear that Compose helps improve the efficiency of UI development.


# App implementation

1. Picture composition

The APP consists of two pictures:

  • InputScreen (InputScreen) : input time through digital soft keyboard, when new input numbers, all numbers move left; Backspace Reverses all numbers to the right when the last input is entered. Input and display logic similar to calculator app.
  • CountdownScreen: shows the current remaining time and has an animation effect; The text format and size changes depending on the time remaining: the text with the last 10 seconds countdown also has a more eye-catching zoom animation.
more than 1h more than 1m & less than 1h less than 1m

State Controls page redirection

Jump logic between pages:

  • After InputScreen is finished, click Next at the bottom to jump to CountdownScreen and enter the countdown
  • CountdownScreen Click Cancel at the bottom to return to InputScreen

Compose does not have page-management units such as activities and fragments. A page is simply a Composable, full-screen Composable, which can be implemented using state. Complex page scenarios can be navigation-compose

enum class Screen {
    Input, Countdown
}

@Composable
fun MyApp(a) {

	var timeInSec = 0
    Surface(color = MaterialTheme.colors.background) {
        var screen by remember { mutableStateOf(Screen.Input) }

        Crossfade(targetState = screen) {
            when (screen) {
                Screen.Input -> InputScreen {
                    screen = Screen.CountdownScreen
                }
                Screen.Countdown -> CountdownScreen(timeInSec) {
                    screen = Screen.Input
                }
            }
        }
    }
}

Copy the code
  • screen: Uses state to save and listen for changes to the current page,
  • Crossfade:CrossfadeCan fade in and out switch internal layout; Internally switch different pages according to screen.
  • timeInSec: Input storage of InputScreentimeInSecAnd carry it to CountdownScreen

2. InputScreen

The InputScreen contains the following elements:

  1. Input result: input-value
  2. The fallback: backspace
  3. Softkeyboard: softkeyboard
  4. Bottom: next

According to the current input results, the screen elements will change.

  • When a result is entered: Next is displayed, backspace is activated, and input-value is highlighted;
  • Otherwise, next is hidden, backspace is disabled, and input-value is low

State drives the UI refresh

Traditional writing is verbose, so you need to set up a listener where the input results are affected, such as backspace and next, respectively, in this example. Dictating changes to related elements as input changes, page complexity increases exponentially as the number of page elements increases.

Using Compose is much simpler, we simply wrap the input result as state and listen, and when the state changes, all the composables re-execute and update the state. Even if the number of elements increases, existing code will not be affected and complexity will not increase.

var input by remember {
	mutableStateOf(listOf<Int>())
}
    
val hasCountdownValue = remember(input) {
	input.isNotEmpty()
}
Copy the code
  • mutableStateOfCreate a variable state throughbyThe broker subscribes and the current Composable is re-executed when state changes.
  • Since the Composable executes repeatedly, useremember{}Repeated creation of state instances due to repeated execution of the Composable can be avoided.
  • whenrememberThe block is reexecuted when the input parameter changes. In the example above, when the input changes, the block checks whether the input is empty and saves it inhasCountdownValueFor other Composable reference.
Column() {

        ...
		
        Row(
            Modifier
                .fillMaxWidth()
                .height(100.dp)
                .padding(start = 30.dp, end = 30.dp)
        ) {
            //Input-value
            listOf(hou to "h", min to "m", sec to "s").forEach {
                DisplayTime(it.first, it.second, hasCountdownValue)
            }

            //Backspace
            Image(
                imageVector = Icons.Default.Backspace,
                contentDescription = null,
                colorFilter = ColorFilter.tint(
                    Color.Unspecified.copy(
                    	// Display different brightness according to hasCountdownValue
                        if (hasCountdownValue) 1.0 f else 0.5 f))}...// Whether to display next according to hasCountdownValue
        if (hasCountdownValue) {
            Image(
              imageVector = Icons.Default.PlayCircle,
                contentDescription = null,
                colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
            )
        }
    }
Copy the code

As mentioned above, add hasCountdownValue logic to declare the UI and wait for it to refresh again, without setting up a listener and updating the UI as required in the traditional way.

3. CountdownScreen

CountdownScreen consists of the following elements:

  1. Text: Display hour, second, minutes and ms
  2. Atmosphere section: several different types of circular animations
  3. Cancel at the bottom of the

Use animation to calculate the countdown

How do you calculate the countdown exactly?

The initial solution was to use flow to calculate the countdown, and then convert flow to state to drive UI refresh:

private fun interval(sum: Long, step: Long): Flow<Long> = flow {
    while (sum > 0) {
        delay(step)
        sum -= step
        emit(sum)
    }
}
Copy the code

However, after testing, it is found that delay is not accurate to process the countdown due to the overhead of coroutine switching.

After thinking, I decided to use animation to process the countdown

var trigger by remember { mutableStateOf(timeInSec) }

val elapsed by animateIntAsState(
	targetValue = trigger * 1000,
	animationSpec = tween(timeInSec * 1000, easing = LinearEasing)
)

DisposableEffect(Unit) {
	trigger = 0
	onDispose { }
}
Copy the code
  • Compose’s animation is also state-driven,animateIntAsStateDefine the animation, calculate the animation valuation, and convert to state.
  • Animation bytargetValueThe change triggers the start.
  • animationSpecUsed to configure the animation type, such as through heretweenConfigure a linear tween animation.durationSet totimeInSec * 1000, which is ms for the length of the countdown.
  • DisposableEffectUsed to perform side effects in pure functions. If the parameters change, the logic in the block is executed each time the Composition is redrawn.DisposableEffect(Unit)Since the parameters never change, it means that the block will only be executed once the first time it is on screen.
  • triggerStart with timeInSec and set it to 0 the first time it’s on screen,targetValueThe change triggers the animation: fromtimeInSec*1000Perform to0The duration oftimeInSec*1000, the end of the animation is the end of the countdown, and absolutely accurate, no error.

What you need to do is convert Elapsed to the appropriate text display

val (hou, min, sec) = remember(elapsed / 1000) {
    val elapsedInSec = elapsed / 1000
    val hou = elapsedInSec / 3600
    val min = elapsedInSec / 60 - hou * 60
    val sec = elapsedInSec % 60
    Triple(hou, min, sec)
}
...

Copy the code

Font dynamics

Remaining time changes bring different text content and font sizes. This implementation is as simple as determining the remaining time in the Composable when setting size.


 // Set the font size based on the remaining time
 val (size, labelSize) = when {
     hou > 0 -> 40.sp to 20.sp
     min > 0 -> 80.sp to 30.sp
     else -> 150.sp to 50.sp
 }
    
 ...
 Row() {
        if (hou > 0) {// Do not display h when the remaining time is less than one hour
            DisplayTime(
                hou.formatTime(),
                "h",
                fontSize = size,
                labelSize = labelSize
            )
        }
        if (min > 0) {// If the remaining time is less than 1 minute, m is not displayed
            DisplayTime(
                min.formatTime(),
                "m",
                fontSize = size,
                labelSize = labelSize
            )
        }
        DisplayTime(
              sec.formatTime(),
                "s",
                fontSize = size,
                labelSize = labelSize
        )
    }
Copy the code

Atmosphere of the animation

Atmosphere animation is very important to improve the texture of App. The following animations are used in App to enhance the atmosphere:

  • Full circle Breathing lamp effect: 1 shot /2 seconds
  • Half ring horse-light effect: 1 time /1 second
  • Radar animation: Scan 100% progress when countdown ends
  • Text zoom: countdown 10 seconds zoom, 1 time /1 second

Here we use transition to synchronize multiple animations

    val transition = rememberInfiniteTransition()
    var trigger by remember { mutableStateOf(0f)}// Linear animation implements radar animation
    val animateTween by animateFloatAsState(
        targetValue = trigger,
        animationSpec = tween(
            durationMillis = durationMills,
            easing = LinearEasing
        ),
    )

	//infiniteRepeatable+restart
    val animatedRestart by transition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Restart)
    )
    
	//infiniteRepeatable+reverse implements breathing lamps
    val animatedReverse by transition.animateFloat(
        initialValue = 1.05 f,
        targetValue = 0.95 f,
        animationSpec = infiniteRepeatable(tween(2000), RepeatMode.Reverse)
    )
	
	//infiniteRepeatable+reverse implements text scaling
    val animatedFont by transition.animateFloat(
        initialValue = 1.5 f,
        targetValue = 0.8 f,
        animationSpec = infiniteRepeatable(tween(500), RepeatMode.Reverse)
    )
    
Copy the code
  • rememberInfiniteTransitionCreate a REPEATabletransitionThe transition throughanimateXXXCreate multiple animations (state) and synchronize the animations created by the same transition. Three animations are created in the app:animatedRestart,animatedReverse,animatedFont
  • You can also set the animationSpec in Transition. Configured in appinfiniteRepeatableIs a repeat animation that can be set with parametersdurationAs well asRepeatMode

Draw a ring

The atmosphere of various circles can then be drawn based on the animation state created above, and animated by continuous compoition.

Canvas(
     modifier = Modifier
            .align(Alignment.Center)
            .padding(16.dp)
            .size(350.dp)
) {
        val diameter = size.minDimension
        val radius = diameter / 2f
        val size = Size(radius * 2, radius * 2)

        // Run the lantern half circle
        drawArc(
                color = color,
                startAngle = animatedRestart,
                sweepAngle = 150f,
                size = size,
                style = Stroke(15f),// Breathing lamp is round
        drawCircle(
            color = secondColor,
            style = strokeReverse,
            radius = radius * animatedReverse
        )

        // Radar sector
        drawArc(
            startAngle = 270f,
            sweepAngle = animateTween,
            brush = Brush.radialGradient(
                radius = radius,
                colors = listOf(
                    purple200.copy(0.3 f),
                    teal200.copy(0.2 f),
                    Color.White.copy(0.3 f)
                ),
            ),
            useCenter = true,
            style = Fill,
        )
    }
Copy the code
  • Canvas{}Can draw custom graphics.
  • drawArcTo draw an angled arc,startAngleandsweepAngleSet the actual position of the arc on the circle, setting startAngle toanimatedRestartAccording to the change of state, the animation effect is realized. The style is set toStrokeDraws only borders. Set toFillFill the arc to form a fan.
  • drawCircleIt’s used to draw a perfect circleanimatedReverse, change the radius to achieve breathing lamp effect

Note: For more on Compose Animations, see “Learn how to Use Jetpack Compose Animations”.


# summary

The core of Compose is that State drives UI refreshes, and animation relies on State to animate. Therefore, in addition to serving visual effects, animation can also be used to calculate state. Only then did it suddenly become clear to the organizer that animation might be used in the description of the title. Its main purpose is to accurately calculate the latest state of countdown.

CountdownTimer is a great way to get your hands on new technology. The second week of the challenge is due on March 10th, and there are two more challenges to follow, both of which are aimed at encouraging new players so it shouldn’t be too difficult. If you haven’t already touched Compose, take the opportunity to try something new

Project address: TikTik