JavaFX Light Bulb with improved UI performance
- April 27th, 2010
- Posted in JavaFX
- Write comment
Update: JNLP file has been fixed. Should work now (me == stupid)…
I am sure most of you have seen the nice demo posted by Mark Anro Silva.
While this is a very nice demo it has a performance problem related to the slider that is very common in JavaFX.
The effectOpacity of the LightBulbEffect is bound to the value of the slider. While this is the correct logic, we run into problems, if those values are updated too often.
A slider is able to create several updates/events every second. When expensive effects are calculated every time, we will get a UI that is not very responsive (and feels slow).
Therefore it is a good idea to not to bind expensive actions to UI triggers directly. Instead you could use a timeline to delay the action for some milliseconds.
For that purpose I have created a DelayedAction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | package com.cedarsoft.fx; import javafx.animation.Timeline; import javafx.animation.KeyFrame; /** * A DeplayedAction. * This class can be used to delay actions. This is especially useful if used with sliders or other UI components * that update its values very fast. * When binding long running / processor intensive actions to those variables your application will start to feel * unresponsive.<p/> * * If maxDelay is set, the action is executed at least {@link #maxDelay} after the call of {@link #schedule()}. */ public class DelayedAction { /** * The action that is called after the delay has passed. */ public-init var action: function(): Void; /** * The delay */ public-init var delay: Duration = 50ms; /** * The maximum deplay the action is called after. * If the max delay */ public-init var maxDelay: Duration; public-read def maxDelaySet = bind maxDelay > 0ms on replace { if ( not maxDelaySet ) { maxDeplayTimeLine.stop(); } }; /** * if set to true every call to {@link #schedule} will executed immediately. * Pending requests will be executed immediately. */ public var executeImmediately: Boolean on replace { if ( executeImmediately and timeline.running or maxDeplayTimeLine.running ) { execute(); } }; def timeline: Timeline = Timeline { keyFrames: [ KeyFrame { time: bind delay action: function () { execute(); } } ] } def maxDeplayTimeLine: Timeline = Timeline { keyFrames: [ KeyFrame { override var time= bind maxDelay on replace { maxDeplayTimeLine.evaluateKeyValues(); } action: function () { execute(); } } ] } /** * The action is called after the given delay. Every call to schedule restarts the delay. * */ public function schedule() { if ( executeImmediately ) { execute(); return ; } if ( maxDelaySet ) { maxDeplayTimeLine.play(); } timeline.playFromStart(); } /** * Executes the action immediately. */ public function execute(): Void { maxDeplayTimeLine.stop(); timeline.stop(); action(); } } |
That action is part of the fx.commons-library (snapshot can be downloaded from
http://nexus.cedarsoft.com/content/groups/public-snapshots/com/cedarsoft/fx/commons/1.0.0-SNAPSHOT/).
How to use DelayedAction
The usage of the DelayedAction is quite simple. Instead of directly binding the variable, you assign the var within a delayed action.
1 2 3 4 5 6 7 8 9 | def lightOpacitySetAction: DelayedAction = DelayedAction { delay: 50ms maxDelay: 500ms executeImmediately: true //this is only necessary for this demo. Will be changed to false as soon as the slider becomes visible action: function () { println( "---> SETTING NEW VALUE! {slider.value}" ); lightOpacity2 = slider.value; } }; |
That action is triggered if the value of the slider is changed. Therefore we introduce a temporary var.
1 2 3 | var tmp = bind slider.value on replace{ lightOpacitySetAction.schedule(); } |
Of course it is necessary to remove the binding of lightOpacity2 to slider.value.
Updated Demo
I have updated the demo application accordingly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | package com.cedarsoft.fx; import javafx.scene.shape.QuadCurveTo; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.image.ImageView; import javafx.scene.Group; import javafx.scene.shape.Rectangle; import javafx.scene.paint.Color; import javafx.scene.effect.GaussianBlur; import javafx.scene.effect.Glow; import javafx.scene.shape.Path; import javafx.scene.control.Button; import javafx.scene.input.MouseEvent; import javafx.scene.control.Slider; import javafx.scene.control.Label; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.CustomNode; import javafx.animation.Timeline; import javafx.animation.KeyFrame; import javafx.animation.Interpolator; import java.lang.UnsupportedOperationException; function run(_ARGS:String[ ]){ var buttonstext ="On" on replace { if ( buttonstext == "Off" ) { lightOpacity1 = .1; lightOpacity3 = .3; lightOpacity4 = 1; controlVisible = true; slider.value = .3; lightOpacitySetAction.executeImmediately = false; } else { lightOpacity1 = 0; lightOpacity3 = 0; lightOpacity4 = 0; controlVisible = false; lightOpacitySetAction.executeImmediately = true; slider.value = 0; } }; var lightOpacity1: Number = 0; var lightOpacity2: Number = 0; var lightOpacity3: Number = 0; var lightOpacity4: Number = 0; var lightbulbImg = ImageView { image: Image { url: "{__DIR__}resources/lightbulb.jpg" } } def lightOpacitySetAction: DelayedAction = DelayedAction { delay: 50ms maxDelay: 500ms executeImmediately: true action: function () { println( "---> SETTING NEW VALUE! {slider.value}" ); lightOpacity2 = slider.value; } }; var controlVisible = false; var effects = Group { content: [ Rectangle { width: 412 height: 520 fill: Color.YELLOW opacity: bind lightOpacity1 } LightBulbEffect { effectOpacity: bind lightOpacity2 } LightBulbEffect { effectOpacity: bind lightOpacity3 } Rectangle { effect: GaussianBlur { radius: 10 input: Glow { } } translateY: 178 translateX: 165 width: 65 height: 8 arcWidth: 8 arcHeight: 8 fill: Color.WHITE stroke: Color.ORANGE strokeWidth: 1.8 opacity: bind lightOpacity4 } Path { effect: GaussianBlur { radius: 1 input: Glow { } } stroke: Color.WHITE strokeWidth: 1.8 opacity: bind lightOpacity4 elements: [ MoveTo { x: 172 y: 183 }, LineTo { x: 183 y: 182 }, QuadCurveTo { controlX: 198 controlY: 183 x: 213 y: 182 } QuadCurveTo { controlX: 214 controlY: 183 x: 225 y: 185 } ] } ] } var togglebutton = Button { translateY: 350 translateX: 260 text: bind buttonstext onMousePressed: function ( ev: MouseEvent ): Void { if ( buttonstext == "Off" ) { buttonstext = "On"; } else { buttonstext = "Off"; } } } var slider: Slider = Slider { translateY: 420 translateX: 128 max: .4 min: 0 visible: bind controlVisible } var tmp = bind slider.value on replace{ lightOpacitySetAction.schedule(); } var controls = Group { content: [ togglebutton, slider, Label { translateX: 120 translateY: 419 text: "-" visible: bind controlVisible } Label { translateX: 268 translateY: 419 text: "+" visible: bind controlVisible } ] } Stage { title: "JavaFX Light Bulb" scene: Scene { width: 402 height: 499 fill: Color.BLACK content: [ lightbulbImg, effects, controls ] } } } public class LightBulbEffect extends CustomNode { public var effectOpacity: Number; override function create() { Path { translateX: -10 translateY: -8 effect: GaussianBlur {input: Glow {level: 1} radius: 63} fill: Color.YELLOW stroke: Color.YELLOW strokeWidth: 5 opacity: bind effectOpacity elements: [ MoveTo {x: 127 y: 132}, QuadCurveTo { controlX: 206 controlY: 48 x: 287 y: 132 } QuadCurveTo { controlX: 320 controlY: 180 x: 295 y: 230 } QuadCurveTo { controlX: 290 controlY: 240 x: 270 y: 280 } QuadCurveTo { controlX: 270 controlY: 295 x: 260 y: 320 } LineTo {x: 153 y: 320} QuadCurveTo { controlX: 143 controlY: 295 x: 143 y: 280 } QuadCurveTo { controlX: 118 controlY: 240 x: 115 y: 230 } QuadCurveTo { controlX: 95 controlY: 180 x: 127 y: 132 } ] } } } |

Nice post, I noticed that the slider was really unresponsive when I first tried that demo a few times ago – I thought it was due to poor performance of the JavaFX runtime rather than inefficient application code.
The launch button on this page does not work at this time:
com.sun.deploy.net.FailedDownloadException: Unable to load resource: file:/home/johannes/NetBeansProjects/fx-commons/dist/fx-commons.jnlp
Thanks for the hint. I just used a JNLP created by Netbeans. Obviously I should have looked at it first… Thanks.
Another way to do it is to round the slider value to the nearest X. Where X depends on how many stops you want between min and max. Below is some example code showing how to limit a slider to 10 stops between min and max. This should be much lighter weight than using a Timer.
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.control.Slider;
var text:Text;
var slider:Slider;
def numberOfStops = 10;
def factor = bind 1/ ((slider.max – slider.min)/numberOfStops);
var sliderValue = bind ((slider.value*factor) as Integer) / factor;
Stage {
scene: Scene {
width: 250
height: 150
content: [
text = Text {
font : Font { size : 16 }
x: 10
y: 30
content: bind "Slider value = {slider.value}\n sliderValue={sliderValue}"
}
slider = Slider {
layoutX: 20
layoutY: 80
min: 0
max: 0.4
majorTickUnit: 2
minorTickCount: 1
snapToTicks: true
}
]
}
}
Very nice idea! I will take a closer look at your code. At least for sliders this seems to be a very good approach.
But I think for this demo particular demo it won’t work. The change of the opacity takes too long.
This great technique should work for Swing too.