↓ Skip to main content
  1. JavaFX/
  2. JavaFX Techniques/
  3. Custom Controls/

JavaFX Custom Control - The Triple Toggle Switch

7 mins·
Table of Contents

Introduction
#

There was a question that came up on StackOverflow about how to create a three way toggle switch that looked like something on an Apple AirPods menu:

AirPods

The question was deemed to complex for StackOverflow and was closed, but I thought the principle was something that should be explored in any case. I took a swing at building something, and this is what I came up with:

Custom Control

Which I think looks a lot like the original, at least in spirit.

Note: This article is still a “work in progress”, but I’m pushing it out live as soon as possible so that it might help the user that posted the original question on StackOverflow.com. I expect to change some of the code, and flush out the discussion about the approach in the next few days. That said, the code does work as it is and should be a reasonable starting place to build your own version.

The Design
#

It was pretty simple to build the layout. It’s just VBox with two HBoxes inside of it. The top HBox has a set of ToggleButtons and it’s styled to look like the track that a flip switch would travel along. The ToggleButtons are round, and they have a unique icon in each one.

The second HBox has the Labels that go along with the ToggleButtons. They need to all be spaced out evenly to make everything look right.

I decided to implement this as custom class, extending Pane. Pane was as high up as I could go, since it’s the first one that supports getChildren(), meaning that I could put things in it.

My goal was to create something that was generically useful. By this, I mean a stand-alone Node that could be deployed just like any other JavaFX Node, and would behave substantially like all those other Nodes. This meant that I would need to provide properties and methods that would allow the layout code to configure it as required, and they would need to be accessed in the same way as the other Nodes.

There’s a Value property, and it’s defined as a JavaFX Bean. This means that there’s a valueProperty() method, a setValue() method and a getValue() method. The latter two are delegates to the Value property’s getValue() and setValue().

I also created an Enum for this called ToggleSelection. It only has three values: LEFT, CENTRE and RIGHT. The layout code is going to need to translate this into something that relates to its Model.

I wanted to keep the constructor simple, so it just takes the Strings for the three labels. To change the icons, there are three StringProperties deployed as Beans: LeftIcon, CentreIcon and RightIcon.

The Code
#

class ToggleSwitch(private val label1: String, private val label2: String, private val label3: String) : Pane() {
   private val _value: ObjectProperty<ToggleSelection> = SimpleObjectProperty(ToggleSelection.CENTRE)
   var value: ToggleSelection
      get() = _value.get()
      set(newValue) = _value.set(newValue)
   private val standardWidthProperty: DoubleProperty = SimpleDoubleProperty(0.0)
   private val theToggles = ToggleGroup()
   private val _leftIcon: StringProperty = SimpleStringProperty("captainicon-146")
   var leftIcon: String
      get() = _leftIcon.get()
      set(value) = _leftIcon.set(value)
   private val _centreIcon: StringProperty = SimpleStringProperty("captainicon-150")
   var centreIcon: String
      get() = _centreIcon.get()
      set(value) = _centreIcon.set(value)
   private val _rightIcon: StringProperty = SimpleStringProperty("captainicon-158")
   var rightIcon: String
      get() = _rightIcon.get()
      set(value) = _rightIcon.set(value)
   private val leftButton = createButton(_leftIcon)
   private val centreButton = createButton(_centreIcon)
   private val rightButton = createButton(_rightIcon)

   fun leftIconProperty() = _leftIcon
   fun centreIconProperty() = _centreIcon
   fun rightIconProperty() = _rightIcon
   fun valueProperty() = _value

   init {
      children += createContent()
      minWidth = 400.0
      _value.bind(Bindings.createObjectBinding(this::checkValue, theToggles.selectedToggleProperty()))
      centreButton.isSelected = true
   }

   private fun checkValue(): ToggleSelection {
      return when (theToggles.selectedToggle) {
         leftButton -> ToggleSelection.LEFT
         centreButton -> ToggleSelection.CENTRE
         rightButton -> ToggleSelection.RIGHT
         else -> ToggleSelection.CENTRE
      }
   }

   private fun createContent(): Region = VBox(10.0).apply {
      children += buttonBox()
      children += labelBox()
      isFillWidth = false
      alignment = Pos.CENTER
   }

   private fun labelBox(): Region = HBox(20.0).apply {
      val labels = listOf(toggleLabel(label1), toggleLabel(label2), toggleLabel(label3))
      children += labels
      standardWidthProperty.bind(Bindings.createDoubleBinding({ labels.maxOfOrNull { label -> label.width } },
                                                              *(labels
                                                                 .map { label -> label.widthProperty() }
                                                                 .toTypedArray())))
      labels.forEach { label -> label.minWidthProperty().bind(standardWidthProperty) }
      alignment = Pos.CENTER
   }

   private fun toggleLabel(labelText: String): Region = Label(labelText).apply {
      styleClass += "toggle-label"
      alignment = Pos.CENTER
   }

   private fun buttonBox(): Region = HBox(20.0).apply {
      children += listOf(leftButton, centreButton, rightButton)
      spacingProperty().bind(standardWidthProperty.subtract(20.0))
      styleClass += "button-box"
   }

   private fun createButton(iconName: StringProperty) = ToggleButton().apply {
      styleClass += "toggle-switch-button"
      theToggles.toggles += this
      graphicProperty().bind(Bindings.createObjectBinding({ createIcon(iconName.value) }, iconName))
   }

   private fun createIcon(iconName: String) = FontIcon(iconName).apply {
      iconSize = 18
      iconColor = Color.WHITE
   }
}

enum class ToggleSelection {
   LEFT, CENTRE, RIGHT
}

You can test it with this:

class ToggleSwitchDemo : Application() {

   private val nameProperty: StringProperty = SimpleStringProperty("Not Started")
   private var counter: Int = 0

   override fun start(primaryStage: Stage) {
      primaryStage.scene = Scene(createContent()).addWidgetStyles().apply {
         object {}::class.java.getResource("/css/toggleswitch.css")?.toString()?.let { stylesheets += it }
      }
      primaryStage.show()
   }

   private fun createContent(): Region = BorderPane().apply {
      center = ToggleSwitch("Noise Cancellation", "Off", "Transparency").apply {
         valueProperty().addListener { _, _, newValue -> println("New Value: $newValue") }
      }
   } padWith 20.0
}

fun main() = Application.launch(ToggleSwitchDemo::class.java)

And…you’ll need this Style Sheet:

.root {
   -toggle-background: rgba(215,225,245,1);
}

.toggle-switch-button {
    -fx-min-width:  40.0;
    -fx-min-height: 37.0;
    -fx-background-radius: 40.0;
    -fx-background-color: derive(-toggle-background, -3%);
}

.toggle-switch-button:selected {
  -fx-background-color: cornflowerblue;
}

.toggle-label {
  -fx-text-fill: midnightblue;
  -fx-font-weight: bold;
  -fx-font-size: 11px;
}

.button-box {
   -fx-background-radius: 20;
   -fx-background-color: -toggle-background;
   -fx-padding: 4px;
}

Notes on the Code
#

First of all, this is Kotlin. If you’re confused by the Kotlin, then you can take a look at this article and this article for some help.

The first part of the code is setting up the Java Beans. I’m using a technique that was described in this article. The net result is that you get the three public functions that you need for a Bean.

The init{} section is kind of like a constructor, and it calls the layout function and connects the Value property to the ToggleGroup.

createContent() gets the layout started. It creates a VBox and the calls two builder methods to create the HBoxes with the Buttons and Labels.

The only tricky part of the layout is getting the Labels spread out evenly in a generic manner, and keeping the ToggleButtons right on top of them.

To keep the Labels spread out, they need to all set to the same width, and that width needs to be enough to hold the biggest label. The obvious approach is the stream through the labels, and grab the biggest width, then set the MinWidth property of all three labels to that value.

However…

When the layout is built, nothing’s actually in the GUI, so any calls to look at the size of the Label.getWidth() will just return “0.0”. So that’s not gonna work.

However…

The Width of the Labels is a Property. This means that we can set a Binding on it. Yeah! And this is what we do here…create a Binding that’s dependent on the widthProperty() of all three labels, and that binding then streams through the Labels to grab the biggest width.

We’re also going to need that biggest width to space out our ToggleButtons. The best way to implement this is to create a private DoubleProperty field in the class, then directly bind that Property to our biggest width Binding. Then we can bind the minWidth() of each Label to this DoubleProperty.

For the ToggleButtons, we need to use this biggest width DoubleProperty to provide the spacing between the Buttons. This is easy, because we have HBox.spacingProperty(), and we can bind that to our biggest width Property with a slight adjustment for the actual width of the Buttons.

Ikonli
#

This code uses FontAwesome through the Ikonli library. You’ll need to add the dependencies to your project to use it:

implementation('org.kordamp.ikonli:ikonli-javafx:12.2.0')
implementation 'org.kordamp.ikonli:ikonli-captainicon-pack:12.3.1'

The first dependency is the actual Ikonli library, and the second is the icon pack that I used.

This is using a slightly older version of the Ikonli library, and it looks like the newer versions handle

Related

Understanding the JavaFX Bindings Class

12 mins
The Bindings class is a utility library that provides a huge number of helper methods that can create and manipulate Bindings. Understanding how to use this library can make your code much simpler and easier to read.

Kotlin For JavaFX

18 mins
Introduction # I’ve been using Kotlin for a while now, and I’ve been using it to write JavaFX applications. What I’ve found is that the things that make Kotlin better than Java, make Kotlin way, way better for JavaFX.

Dealing With Dialogs

20 mins
Introduction # We’re all familiar with Dialog boxes, they pop up with some important information, ask you, “Are you sure?”, let us pick a file, or make some other important decision before processing can continue. In the beginning, JavaFX didn’t have Dialogs and we were left to cook up our own solutions or use third party libraries. But they were added around JavaFX 8, and work quite well.

Layout Classes 101

18 mins
In order to put anything onto the GUI you’ll need to use one of the layout Nodes to control how the screen is organized. There are a variety of different layout classes, each of which handles the organization of its content differently. This article gives you a summary of how each different layout class works, and explains how each one differs from the others.

Kotlin For Java Programmers

19 mins
Introduction # The first time I ever looked at a Kotlin program was when one of my developers found a cool JavaFX library called “DirtyFX”. We wanted to understand it worked, but when we looked at the source code on GitHub we were stymied because it was in Kotlin.

Unravelling MVC, MVP and MVVM

35 mins
Taking a look at the three most common design patterns for building systems with user interfaces. How are they different? Which one is best? Is there anything better?

How to Swap Scenes Properly

13 mins
Taking a look at the three most common design patterns for building systems with user interfaces. How are they different? which one is best? Is there anything better?