Creating a Seven-Segment View in Jetpack Compose

Kappdev
7 min readDec 9, 2023

--

Welcome! Have you ever been intrigued by the idea of crafting a Seven-Segment View in Jetpack Compose? In this exploration, we’ll delve into this fascinating feature. While its practical utility might seem limited, the scope for creativity is boundless. Let’s dive in together and uncover the possibilities!

Image by Freepik

Section 1: Define support utilities.

SegmentsState

The SegmentsState class manages the state of each segment for a given digit. True indicates it’s illuminated for a segment, while false denotes it’s not.

data class SegmentsState(
val a: Boolean = false,
val b: Boolean = false,
val c: Boolean = false,
val d: Boolean = false,
val e: Boolean = false,
val f: Boolean = false,
val g: Boolean = false
)
Segments Scheme

Additionally, to cover the state of all digits, we create a map linking each digit (0 to 9) to its respective SegmentsState.

Note: All the states in SegmentsState are false by default, so we don’t need to include them here.

val SevenSegmentNumbers = mapOf(
0 to SegmentsState(a = true, b = true, c = true, d = true, e = true, f = true),
1 to SegmentsState(b = true, c = true),
2 to SegmentsState(a = true, b = true, d = true, e = true, g = true),
3 to SegmentsState(a = true, b = true, c = true, d = true, g = true),
4 to SegmentsState(b = true, c = true, f = true, g = true),
5 to SegmentsState(a = true, c = true, d = true, f = true, g = true),
6 to SegmentsState(a = true, c = true, d = true, e = true, f = true, g = true),
7 to SegmentsState(a = true, b = true, c = true),
8 to SegmentsState(a = true, b = true, c = true, d = true, e = true, f = true, g = true),
9 to SegmentsState(a = true, b = true, c = true, d = true, f = true, g = true),
)

Also, I’ve crafted an extension function to enhance code readability. When applied to an Int object, this function retrieves the associated SegmentsState.

fun Int.getSegmentsState(): SegmentsState {
return SevenSegmentNumbers.getOrElse(this) {
throw IllegalArgumentException("The digit must be in the range from 0 to 9")
}
}

SegmentData

The SegmentData class stores specific information about each segment, including its activation status, orientation (vertical or horizontal), and coordinates to draw—start and end offsets.

data class SegmentData(
val isActive: Boolean,
val isVertical: Boolean,
val startX: Float,
val endX: Float,
val startY: Float,
val endY: Float
)

Additionally, to manage segment spacing, I’ve created an extension function addSpacing() for the SegmentData class. This function adjusts the segment's start and end coordinates by a specified space, ensuring appropriate spacing between segments.

fun SegmentData.addSpacing(space: Float): SegmentData {
return when
isVertical -> this.copy(
startY = (this.startY + space),
endY = (this.endY - space)
)
else -> this.copy(
startX = (this.startX + space),
endX = (this.endX - space)
)
}
}

Other utility functions

The splitToDigits() function efficiently divides a number into individual digits, returning them as a list of integers. This function proves invaluable when handling numeric input for segment display.

fun Int.splitToDigits(): List<Int> {
return this.toString().map(Char::digitToInt)
}

The padToStart() function, an extension of the List class, ensures a list reaches a specified size by padding it with a designated value. Particularly useful when ensuring a consistent length for digit representation in the Seven-Segment View.

fun <T> List<T>.padToStart(size: Int, value: T): List<T> {
require(size >= 0) { "Size should not be negative" }
return if (this.size < size) {
List(size - this.size) { value } + this
} else {
this
}
}

Section 2: A single Seven-Segment View.

Firstly, let’s define an extension function drawSegment for DrawScope that will draw a single segment.

private fun DrawScope.drawSegment(
color: Color,
data: SegmentData,
halfWidth: Float // Half of the segment width
) {
// Create a Path object to draw the segment
val segmentPath = Path().apply {
// Using with() for improved readability by accessing SegmentData properties directly
with(data) {
// Move the cursor to the start of the segment
moveTo(startX, startY)
if (isVertical) {
// Drawing vertical segment path
lineTo(startX + halfWidth, startY + halfWidth) // 1
lineTo(startX + halfWidth, endY - halfWidth) // 2
lineTo(endX, endY)
lineTo(startX - halfWidth, endY - halfWidth) // 3
lineTo(startX - halfWidth, startY + halfWidth) // 4
} else {
// Drawing horizontal segment path
lineTo(startX + halfWidth, startY - halfWidth) // 1
lineTo(endX - halfWidth, startY - halfWidth) // 2
lineTo(endX, endY)
lineTo(endX - halfWidth, startY + halfWidth) // 3
lineTo(startX + halfWidth, startY + halfWidth) // 4
}
// Close the path
close()
}
}

// Draw the segment using the path and specified color
drawPath(segmentPath, color)
}
Segment Draw Scheme

Excellent! We’re now ready to utilize this function for drawing the view.

@Composable
fun SingleSevenSegment(
state: SegmentsState,
modifier: Modifier,
activeColor: Color,
inactiveColor: Color,
segmentWidth: Dp,
segmentsSpace: Dp
) {
Canvas(
// This modifier ensures that the view is displayed in a correct ratio of 2:1
modifier = modifier.aspectRatio(0.5f, matchHeightConstraintsFirst = true)
) {
val halfViewHeight = (size.height / 2)
val halfWidth = (segmentWidth.toPx() / 2)

val rightEdge = (size.width - halfWidth)
val bottomEdge = (size.height - halfWidth)

// Define data for each segment based on the SegmentsState and Canvas configurations
val segmentData = listOf(
SegmentData(state.a, isVertical = false, halfWidth, rightEdge, halfWidth, halfWidth),
SegmentData(state.b, isVertical = true, rightEdge, rightEdge, halfWidth, halfViewHeight),
SegmentData(state.c, isVertical = true, rightEdge, rightEdge, halfViewHeight, bottomEdge),
SegmentData(state.d, isVertical = false, halfWidth, rightEdge, bottomEdge, bottomEdge),
SegmentData(state.e, isVertical = true, halfWidth, halfWidth, halfViewHeight, bottomEdge),
SegmentData(state.f, isVertical = true, halfWidth, halfWidth, halfWidth, halfViewHeight),
SegmentData(state.g, isVertical = false, halfWidth, rightEdge, halfViewHeight, halfViewHeight)
)

// Draw the segments
segmentData.forEach { data ->
drawSegment(
color = if (data.isActive) activeColor else inactiveColor,
data = data.addSpacing(segmentsSpace.toPx()),
halfWidth = halfWidth,
)
}
}
}

While we’ve successfully drawn a single seven-segment view, its current limitation in accepting values only up to 9 restricts its practicality. To address this, we’ll implement the final view in the next section. This enhanced view will be able to display a wider range of desired numbers.

Section 3: The final Seven-Segment View.

Well, as was mentioned above, here’s the final function:

@Composable
fun SevenSegmentView(
number: Int,
modifier: Modifier,
activeColor: Color,
inactiveColor: Color = activeColor.copy(0.16f),
digitsNumber: Int = 1,
digitsSpace: Dp = 4.dp,
segmentWidth: Dp = 4.dp,
segmentsSpace: Dp = 0.dp
) {
// Validate input parameters
require(digitsNumber > 0) { "Digits number should be greater than 0" }
require(number >= 0) { "The number has to be positive" }

// Split the number into individual digits
var digits = remember(number) { number.splitToDigits() }

// Ensures the correct number of digits to display
// Alternative behaviors could be implemented, such as throwing an exception for mismatches
if (digits.size > digitsNumber)
digits = digits.takeLast(digitsNumber)
}

Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(digitsSpace),
verticalAlignment = Alignment.CenterVertically
) {
// Pad the digits to match the desired number of digits and display each digit using SingleSevenSegment
// Alternatively, replacing null with 0 ensures that unfilled segments display as 0 instead of remaining unilluminated
digits.padToStart(digitsNumber, null).forEach { digit ->
val state = digit?.getSegmentsState() ?: SegmentsState()
SingleSevenSegment(
state = state,
modifier = Modifier.fillMaxHeight(),
activeColor = activeColor,
inactiveColor = inactiveColor,
segmentWidth = segmentWidth,
segmentsSpace = segmentsSpace
)
}
}
}

Congratulations🥳! We’ve successfully built it👏. For the complete code implementation, you can access it on GitHub Gist🧑‍💻.

Advertisement

Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!

WordBook

Section 4: Usage example.

var number by remember { mutableStateOf(0) }

Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(50.dp)
) {
SevenSegmentView(
number = number,
digitsNumber = 4,
segmentsSpace = 1.dp,
segmentWidth = 8.dp,
digitsSpace = 16.dp,
activeColor = Color.Green,
modifier = Modifier.height(100.dp)
)

Button(
onClick = { number++ }
) {
Text("Increase counter")
}
}

Awesome! Take a look at the output:

Counter Demo

You might also like 👇

Thank you for reading this article!❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap👏 if you liked it or follow Kappdev for more exciting articles😊.

Happy coding!

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀