Using Jetpack Compose to make such a beautiful countdown app

Insert picture description here

Compose Developer Challenge 2nd Week


To cater to the release of the Jetpack Compose beta version, Google officially launched the Compose Developer Challenge. Currently, the second week of android-dev-challenge-2 has entered the second week of Android-dev-challenge-2
Insert picture description here
. The topic of the second week is to use Compose to implement a countdown app. The question is very appropriate and not difficult, but it can guide you to learn some features of Compose in a targeted manner. For example, the implementation of this app requires everyone to learn and understand the use of state and animations.

Friends who are interested in the Compose Developer Challenge and how to participate can refer to: Jetpack Compose Development Challenge


Entry project: TikTik


Stop talking nonsense, first look at the project I submitted: TikTik

The most basic API of Compose is used in the project, and it does not take much time, but the completion effect is quite satisfactory. It can be seen that Compose does help to improve the efficiency of UI development. Here is a brief share of the implementation process with you.


Project realization


Screen composition

Insert picture description here
The app consists of two screens:

  • Input screen (InputScreen) :
    Input the time through the numeric soft keyboard. When a new number is input, all the numbers move to the left; when the backspace returns to the last input, all the numbers move to the right. Similar to the input and display logic of the calculator app.
  • Countdown Screen :
    Displays the current remaining time and is equipped with animation effects; the text format and size will change according to the remaining time: the text of 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 control page jump

Jump logic between pages:

  • After InputScreen completes the input, click Next at the bottom to jump to CountdownScreen to enter the countdown
  • CountdownScreen click Cancel at the bottom to return to InputScreen

Compose no Activity, Fragmentthis page management unit, the so-called page is just a full-screen Composable, you can usually use stateimplementation. Complex page scenes can use navigation-compose

enum class Screen {
    
    
    Input, Countdown
}

@Composable
fun MyApp() {
    
    

	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
                }
            }
        }
    }
}

  • screen : use state to save and monitor the changes of the current page,
  • Crossfade : CrossfadeYou can switch the internal layout with fade-in and fade-out; internally switch different pages according to the screen.
  • timeInSec : The input of InputScreen is stored timeInSecand carried to CountdownScreen

Input screen (InputScreen)

InputScreen includes the following elements:

  1. Input result: input-value
  2. 回退:backspace
  3. Soft keyboard: softkeyboard
  4. Bottom: next

According to the current input result, each element of the screen will change.

  • When there is an input result: next display, backspace activation, input-value highlight;
  • On the contrary, next is hidden, backspace is disabled, input-value is low

State drives UI refresh

If you use the traditional writing method, it will be more verbose, and you need to set up monitoring in the place that affects the input result. For example, in this example, you need to monitor backspace and next separately. When the input changes imperatively to modify the relevant elements, the complexity of the page will increase exponentially with the increase of the page elements.

Using Compose is much simpler. We only need to package the input results stateand monitor them. When the state changes, all Composablere-execute and update the state. Even if the number of elements increases, the existing code will not be affected, and the complexity will not increase.

var input by remember {
    
    
	mutableStateOf(listOf<Int>())
}
    
val hasCountdownValue = remember(input) {
    
    
	input.isNotEmpty()
}
  • mutableStateOf : mutableStateOfCreate a changeable state, bysubscribe through the proxy, and the current Composable will re-execute when the state changes.

  • remember{} : Since Composable will be executed repeatedly, use remembercan avoid repeated creation of state instances due to repeated execution of Composable.

  • When rememberthe parameter changes, the block will re-execute. In the above example, when the input changes, it is judged whether the input is empty and saved in hasCountdownValueit for reference by other Composables.

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(
                    	//根据hasCountdownValue显示不同亮度
                        if (hasCountdownValue) 1.0f else 0.5f
                    )
                )
            )
        }

		...

		//根据hasCountdownValue,是否显示next
        if (hasCountdownValue) {
    
    
            Image(
              imageVector = Icons.Default.PlayCircle,
                contentDescription = null,
                colorFilter = ColorFilter.tint(MaterialTheme.colors.primary)
            )
        }
    }

As above, add hasCountdownValuethe judgment logic while declaring the UI , and then wait for it to be refreshed again, and it will be OK. There is no need to set up monitors and update the UI imperatively like traditional writing.

Countdown Screen (CountdownScreen)

The countdown screen display mainly includes the following elements:

  1. Text part: display hour, second, minutes and ms
  2. Atmosphere part: many different types of circular animations
  3. Cancel at the bottom

Use animation to calculate countdown

How to accurately calculate the countdown?

The initial plan is to use the flowcountdown to calculate, and then convert the flow to state to drive the UI refresh:

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

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

After thinking, I decided to use animation to handle 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 {
    
     }
}
  • animateIntAsState : Compose's animations are also driven by state, animateIntAsStatedefining animations, calculating animation estimates, and converting them into state.
  • targetValue : The animation is triggered by the change of targetValue.
  • animationSpec : animationSpec is used to configure the animation type, for example, by tweenconfiguring a linear tween animation. durationSet to timeInSec * 1000, that is, the countdown time in ms.
  • DisposableEffect : DisposableEffect is used to perform side effects in pure functions. If the parameters change, the logic in the block will be executed every time it is redrawn (Composition). DisposableEffect(Unit)Since the parameters will never change, it means that the block will only be executed once when it is first displayed on the screen.
  • trigger : The initial state of the trigger is timeInSec (total duration of the countdown), and then set to 0 when the first screen is displayed. The targetValuechange triggers the animation:, the duration:timeInSec*1000 ; start:timeInSec*1000; end:0end of the animation is the end of the countdown, and it is absolutely accurate and there is no error.

Next, you only need to elapsedconvert it into a suitable text display and it's OK

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)
}
...

Dynamic changes of text

Changes in the remaining time bring about differences in text content and font size. This implementation is very simple, as long as it is set sizein Composable to determine 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) {
    
    //当剩余时间不足一小时时,不显示h
            DisplayTime(
                hou.formatTime(),
                "h",
                fontSize = size,
                labelSize = labelSize
            )
        }
        if (min > 0) {
    
    //剩余时间不足1分钟,不显示m
            DisplayTime(
                min.formatTime(),
                "m",
                fontSize = size,
                labelSize = labelSize
            )
        }
        DisplayTime(
              sec.formatTime(),
                "s",
                fontSize = size,
                labelSize = labelSize
        )
    }

Atmosphere animation

Atmosphere animation helps improve the texture of the app. The following animations are used in the app to enhance the atmosphere:

  • Round breathing light effect: 1 time/2 seconds
  • Half-circle marquee effect: 1 time/1 second
  • Radar animation: 100% scanning progress at the end of the countdown
  • Text zoom: countdown 10 seconds zoom, 1 time/1 second

transitionMultiple animations are synchronized here :

	val transition = rememberInfiniteTransition()
    var trigger by remember {
    
     mutableStateOf(0f) }

	//线性动画实现雷达动画
    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实现呼吸灯
    val animatedReverse by transition.animateFloat(
        initialValue = 1.05f,
        targetValue = 0.95f,
        animationSpec = infiniteRepeatable(tween(2000), RepeatMode.Reverse)
    )
	
	//infiniteRepeatable+reverse实现文字缩放
    val animatedFont by transition.animateFloat(
        initialValue = 1.5f,
        targetValue = 0.8f,
        animationSpec = infiniteRepeatable(tween(500), RepeatMode.Reverse)
    )
    
  • rememberInfiniteTransition : A repeatable is created transition. The transition animateXXXcreates multiple animations (state), and the animations created by the same transition are kept in sync. app created three animated: animatedRestart, animatedReverse,animatedFont
  • infiniteRepeatable : AnimationSpec can also be set in transition. What is configured in the app infiniteRepeatableis a repeat animation, which can be set by parameters durationandRepeatMode

Draw a ring shape

Next, you can draw various circular atmospheres based on the animation state created above. Realize the animation effect through 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)

		//跑马灯半圆
        drawArc(
                color = color,
                startAngle = animatedRestart,
                sweepAngle = 150f,
                size = size,
                style = Stroke(15f),
        )
        
        //呼吸灯整圆
        drawCircle(
            color = secondColor,
            style = strokeReverse,
            radius = radius * animatedReverse
        )

		//雷达扇形
        drawArc(
            startAngle = 270f,
            sweepAngle = animateTween,
            brush = Brush.radialGradient(
                radius = radius,
                colors = listOf(
                    purple200.copy(0.3f),
                    teal200.copy(0.2f),
                    Color.White.copy(0.3f)
                ),
            ),
            useCenter = true,
            style = Fill,
        )
    }
  • CanvasYou can draw custom graphics.
  • drawArcIt is used to draw an arc with an angle startAngleand sweepAngleset the actual position of the arc on the circle. Here, set startAngle to animatedRestartrealize the animation effect according to the change of state. The style is set to Strokeindicate that only the border is drawn, and the style is set to indicate Fillthat the arc-shaped area is filled to form a fan shape.
  • drawCircleUsed to draw a perfect circle, pass here animatedReverse, change the radius to achieve the breathing light effect

Note: For more information about animation, please refer to Jetpack Compose Animations Super Simple Tutorial


to sum up


The core of Compose is State-driven UI refresh, animation is also based on state-driven, so in addition to serving the vision, animation can also be used to calculate state. It was then that it suddenly became clear why the organizer suggested that animation might be used, and its main function is to accurately calculate the latest state of countdown.

Projects like CountdownTimer are very suitable for training new skills. The deadline for the second week of challenge is March 10th, and there are two more challenges later, which are mainly to encourage novices, so the difficulty should not be very high. If you haven’t been in contact with Compose, you might as well take this opportunity to try it out~

Project address: TikTik

Guess you like

Origin blog.csdn.net/vitaviva/article/details/114451891