Blog Infos
Author
Published
Topics
,
Published

The time is running out!

This article covers the approach and development of the timer app which simply shows a timer selection screen where user can select for how long to run a timer. The timer running part will be covered in Part II. In this part, focus is on designing and developing the timer selection screen along with some basic animations.

What are we designing?

The screen contains a timer label at the top, a timer display to show what time is user selecting, a keypad to select time, a play timer button and a bottom bar in which timer section is hard-selected.

Composable hierarchy

Looking at the screen, the first thought comes to mind is how are going to break into simpler parts and then club them together to make it work. Let’s take a look at the below image to understand better.

Composable hierarchy

We’ve a TimerScreen which contains ScreenTitle, TimerSelection, TimerRunner and BottomBar composables + ViewModel.

The composables contain their sub components which are stateless composables which simply set and return the data provided to them. We’ll make sure to handle our logic and state management in ViewModel and then pass it down to our stateless composables.

Stateless composables are the ones which does not alter the state of the view themselves.

TimeUnit, TimeDisplay, CircularKey, TimerKeypad, BottomBar, BottomTab, ScreenTitle are all stateless. Great! Let’s see how they look individually.

Stateless Composables

Stateless composables

As we can the how the individual component looks like and when they’re combined, what is the result. These components are simply accepting a state and responding with the expected data.

Thinking while designing

Why did we do what we did? Well whenever we design any UI, we should consider the following:

  • Look at it carefully to see the small chunks that we can keep independent, reusable and state free.
  • How each element is positioned and with respect to whom. So that if some view hides or screen size increases/decreases, the elements should remain in same position with respect to each other.
  • Which elements together makes a container so that we can group them in one and use the container in our main UI.
  • How the behavior of any element affect other element position/value.

Once we get clarity on above points then it becomes very easy to break the component into small chunks and build the bigger picture.

In our case, from top to bottom, we found the following chunks:

  • TimeUnit: the timer display required three set of two numeric views + one unit view. So we created a time unit composable which accepts two numbers and a unit for it to show. That’s it. Repeating it thrice makes our time display.

 

@Composable
fun TimeUnit(
    time: TimeUnit = TimeUnit(),
    unit: String,
    textColor: Color = GRAY_TEXT
)
  • CircularKey: the section below timer display and above bottom bar shows that everything is a circular view with a key in it. It could either text key or icon key. So we created a Circular key composable which accepts the key and an icon (nullable). We prioritize the text key over icon key because mostly keys are text. We repeated this key 12 times to make our Keypad. Isn’t cool?
@Composable
fun CircularKey(
    key: Keypad,
    modifier: Modifier = Modifier,
    backgroundColor: Color = GRAY_LIGHT,
    textColor: Color = GRAY_TEXT,
    icon: ImageVector? = null,
    onClick: (Keypad) -> Unit,
)
  • BottomTab: The last bit we realize is that our bottom bar contains a chunk of an icon and text arranged vertically and repeated five times horizontally in a row to make our bottom bar. Obviously it takes some flag to distinguish between selected and unselected state.
@Composable
fun BottomTab(
    modifier: Modifier = Modifier,
    title: String,
    icon: ImageVector,
    isSelected: Boolean = false,
    onClick: (String) -> Unit
)

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Finally we brought all the above elements together in our Timer screen to finally build what we wanted. So think in detail before you write your composables for any UI. It really saves you a lot of time.

Animations used

In the demo video above, we see that the on clicking the play button, the selection screen goes down and second screen slides in from top and on reversing second screen slides out to top and the selection screen slides in back to top from bottom. For that we’ve used simple animation API of compose.

AnimatedContent(
targetState = timerContent,
transitionSpec = {
if(targetState == TimerContent.SELECTION) {
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}
}
) {
target ->
if (target == TimerContent.SELECTION) {
TimerSelectionScreen(
modifier = Modifier.fillMaxWidth(),
timeState = timeState,
onKeyClick = { key ->
timerViewModel.onEvent(
UiEvent.OnKeyPressed(key)
)
}
)
} else {
TimerRunnerScreen(
modifier = Modifier.fillMaxWidth(),
onTimerStop = { key ->
timerViewModel.onEvent(
UiEvent.OnKeyPressed(key)
)
}
)
}
}
view raw TimerScreen.kt hosted with ❤ by GitHub

AnimatedContent is the animation api using which we can animate our content with initial and target state. Here we maintain our screen state and based on the state we show our composables and at the same time compose animate our both current and incoming composables with the animation spec defined under transition spec.

How does this transition happen?
if(targetState == TimerContent.SELECTION) {
    slideInVertically { height -> height } + fadeIn() with
            slideOutVertically { height -> -height } + fadeOut()
} else {
    slideInVertically { height -> -height } + fadeIn() with
            slideOutVertically { height -> height } + fadeOut()
}

Let’s understand this, so when we’ve clicked the play timer button, our screen state changes from SELECTION -> RUNNER. This triggers the transitionSpec to run and it checks in which state are we moving. It executes the else condition where it is defined as:

First slide in the new screen from top and in vertical direction and also apply fade in transition to it at the same time.

slideInVertically { height -> -height } + fadeIn()

Then along with the above, slide out the existing screen to the bottom and at the same time apply fade out transition too.

slideOutVertically { height -> height } + fadeOut()

That’s it and now we see our screens animating with their enter and exit animations. Majorly, slideInVertically is the enter transition and slideOutVertically is the exit transition for our composables. We combine them using the with keyword here.

What’s next?

In the following parts, we’ll see how are we managing states for various components and how to create a timer running with given time.

I would recommend checking out the code yourself and try to understand it as it is very easy. The functionality details I’ll explain in later posts as this is intended to be only for designing the UI.

Source code: https://github.com/aqua30/TimerApp

Branch: timer-selection

That’s it for now. Hope it’ll help!

Until next time..

Cheers!

This article was originally published on proandroiddev.com on May 27, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu