Thursday Feb 13, 2014

Dynamical layouts in FX

Since I took over JavaFX layout code, I had several bug reports that were related to dynamical layouts and noticed that many people struggle with the implementation. By dynamical layout I mean a layout Pane that not only resizes and positions it's children, but also manipulates with it's child list depending on the acquired size.

In a standard layout Pane, the typical workflow is this:

  1. Parent calls prefWidth/prefHeight; the preferred size is computed from the current set of children and Pane properties/constraints
  2. Parent assigns the actual width/height
  3. The layout is computed from the width/height and the same set of children used in step #1

A dynamical Pane would work like this:

  1. The preferred size is either fixed or depends just on Pane properties or some items (based on which the children are created)
  2. Parent assigns the actual width/height
  3. The layout is computed from the width/height and children are created accordingly

A simple dynamical Pane with fixed-size Rectangles may be implemented like this:


      private class GrowingPane extends Pane {

        private static final int PREF_NUMBER = 2;
        private static final int RECTANGLE_SIZE = 50;
        private static final int PADDING = 10;

        @Override
        protected double computePrefWidth(double height) {
            return PREF_NUMBER * RECTANGLE_SIZE + (2 + PREF_NUMBER - 1) * PADDING;
        }

        @Override
        protected double computePrefHeight(double width) {
            return RECTANGLE_SIZE + 2 * PADDING;
        }


        @Override
        protected void layoutChildren() {
            final double w = getWidth();

            getChildren().clear();
            // count the number of Rectangles that will fit
            final int num = (int) (w - PADDING) / (RECTANGLE_SIZE + PADDING);

            int curX = PADDING;
            // Do the layout
            for (int i = 0; i < num; ++i) {
                Rectangle rec = new Rectangle(RECTANGLE_SIZE, RECTANGLE_SIZE, Color.LIGHTGREEN);
                getChildren().add(rec);
                rec.relocate(curX, PADDING);
                curX += RECTANGLE_SIZE + PADDING;
            }

        }
    }
     

Now we look at a more sophisticated version with Buttons that are not-fixed size. For our sample, we generate the Button text - each subsequent button text is double the size. Notice the difference in computePrefWidth/computePrefHeight:


      private class GrowingPane2 extends Pane {

        private static final int PREF_NUMBER = 2;
        private static final String BASE_STRING = "A";
        private double prefWidth = -1;
        private double prefHeight = -1;
        private static final int PADDING = 10;

        @Override
        protected double computePrefWidth(double height) {
            if (prefWidth == -1) {
                computePrefSize();
            }
            return prefWidth;
        }

        private void computePrefSize() {
            String str = BASE_STRING;

            prefHeight = snapSpace(2 * PADDING + computeHeight());

            prefWidth = PADDING;
            for (int i = 0; i < PREF_NUMBER; ++i) {
                prefWidth += computeWidthForString(str) + PADDING;
                str += str;
            }
            prefWidth = snapSpace(prefWidth);

        }

        private double computeHeight() {
            Button b = new Button(BASE_STRING);
            getChildren().add(b);
            b.applyCss();
            double result = b.prefHeight(-1);
            getChildren().remove(b);
            return result;
        }

        private double computeWidthForString(String str) {
            Button b = new Button(str);
            getChildren().add(b);
            b.applyCss();
            double result = b.prefWidth(-1);
            getChildren().remove(b);
            return result;
        }

        @Override
        protected double computePrefHeight(double width) {
            if (prefHeight == -1) {
                computePrefSize();
            }
            return prefHeight;
        }


        @Override
        protected void layoutChildren() {
            final double w = getWidth();

            getChildren().clear();

            double widthLeft = w - PADDING;
            double curX = PADDING;
            String s = BASE_STRING;

            while(widthLeft >= PADDING) {
                double bw = computeWidthForString(s);
                if (bw + PADDING > widthLeft) {
                    break;
                }
                Button b = new Button(s);
                getChildren().add(b);
                b.applyCss();
                s += s;

                b.autosize();
                b.relocate(curX, PADDING);
                curX += bw + PADDING;
                widthLeft -= bw + PADDING;
            }

        }
    }
      

In real-life example, where you generate Buttons (or whatnot) based on some item list or other model, you'd invalidate the computed size on this model change.

Lets look at the computation. You may notice that we added and removed the child and also called applyCss() before computing it's preferred size. This is because the CSS pass is done after the layout pass, so modifying the children in layout pass require explicit execution of CSS pass for every new Node. The CSS depends on context (position in the scenegraph), so we need to add the child first. Of course, to be absolutely accurate, we should add the child to it's correct position and clear the list at the end of the computation. 

Tuesday Jan 21, 2014

Johan Vos blogged about Java FX and Android

If you're interested in latest development in Java FX for Android read Johan's blog entry here. I feel also that some tooling support would help in this stage of the project. So far the gradle script shipped in a binary worked well but who wants to stick with a script forever, right? Looking at the current IDEs that support Android ( Eclipse ADT, Netbeans NBAndroid and Google's Android Studio ) the natural choice would be a new gradle build system. This is a build system that Google has chosen for the future. Unfortunately it is still in a quite rough state and doesn't allow fine grained setup which is essential for us. To be more specific a dex task doesn't have capability that old good ant task had and we can't fix an issue with "too many methods" error. Positive is that there's an issue filed on Android gradle build system so there's a chance that Google will fix that. Until that happens using ant seems to be the only solution. Thanks to Radim Kubacki former NetBeans guy who's the father of NBAndroid we might have a chance to extend NBAndroid plugin for Netbeans. Add a wizard for creating Java FX projects, properly bootstrap it with FX libraries and use all great tools that NBAndroid and NetBeans and SceneBuilder have for Java FX developers. I would really glad to see that happen in near future.

Friday Dec 20, 2013

JavaFX on Android follow-up

Things moved a little bit forward with javaFX on Android and it's about the time to summarize them here. Few weeks ago in a this text "How to build OpenJFX on Android" I've put together a short tutorial for javaFX and Android enthusiasts. I hope you've enjoyed your homemade javaFX runtime for Android. But people usually appreciate when things are simpler and as smooth as possible. Thanks to Johan Vos this happened in javafxports project on bitbucket.org which contains everything required to build javaFX on Android plus publishes regularly built runtime. Javafxports repository android-graphics-rt is a direct fork of OpenJFX 8 Graphics RT synced often. It is a plan to make OpenJFX the upstream project e.q. push approved and reviewed changes back. But right now the latest and freshest features and fixes you find in javafxports.

Read carefully javafxports wiki  how to setup JFX runtime build or use prebuilt one with project creator (gradle script) to get your javaFX app deployed on Android at once. If you encounter some issues there's also newly created JavaFXAndroid forum for you to help. Don't hesitate to ask or share your results, feedback and help is welcome.

Since the first text "How to build OpenJFX on Android" there are important fixes available.

  • Fixed Multitouch support
  • Fixed issue "dex with too many method references". Thanks to patches from Stefan Fuchs and Sebastian Rheinnecker
  • Fixes in Ensemble app to make it build and run on Android.
  • Text input in TextField and TextArea from native softkeyboard.
  • Improved lifecycle behavior pause, resume, rotation.

I hope you'll enjoy more stable and capable javaFX on Android.

Friday Nov 29, 2013

JavaFX on Android multitouch and gestures support

A short entry this week. It looks that I'm on the right path to fix event processing  problems on dalvik, at last. I can't resist to publish short video . The fix hasn't been pushed to repository yet.

Thanks to Pavel Safrata for the sample application.

Friday Nov 22, 2013

Building JavaFX for Android with skia library.

I hope you've tried and succeeded to build JavFX runtime for Android following my blog post How to build OpenJavaFX for Android. Here's a short tweak how to make a freetype library significantly smaller. The result should be functionally identical just instead statically linking freetype of 1.5Mb size we will use on-device library skia. The powerfull skia graphics framework already contains freetype and harfbuzz (complex text layout). Although developers are discouraged to use skia directly since it hasn't public api we could give it a try because the change in build script is very minimal. Every kilobyte matters if you have to repeatedly deploy application to a device.

You will still need freetype headers for compilation but instead libfreetype.a you will use libskia.so. Firstly download libskia.so from your device.

adb pull /system/lib/libskia.so .

Open and edit android.gradle. Go to very bottom and look for ANDROID.fontNativeFreetype.linkFlags and change the name of library to link with. If you've done it like myself e.q. put it along with libfreetype.a to the same directory the script will look like that:

ANDROID.fontNativeFreetype.linkFlags = 
        [linkFlags, "-llog", 
         "-L$freetypeDir/lib", "$freetypeDir/lib/libskia.so"].flatten()
Now just run build and here we go.

Tuesday Nov 12, 2013

How to build Open JavaFX for Android.

Here's a short recipe for baking JavaFX for Android dalvik. We will need just a few ingredients but each one requires special care. So let's get down to the business.

 Sources

The first ingredient is an open JavaFX repository. This should be piece of cake. As always there's a catch. You probably know that dalvik is jdk6 compatible  and also that certain APIs are missing comparing to good old java vm from Oracle.  Fortunately there is a repository which is a backport of regular OpenJFX to jdk7 and going from jdk7 to jdk6 is possible. The first thing to do is to clone or download the repository from https://bitbucket.org/narya/jfx78. Main page of the project says "It works in some cases" so we will presume that it will work in most cases :)
As I've said dalvik vm misses some APIs which would lead to a build failures. To get them use another compatibility repository which is available on GitHub https://github.com/robovm/robovm-jfx78-compat. Download the zip and unzip sources into jfx78/modules/base.
We need also a javafx binary stubs. Use jfxrt.jar from jdk8.
The last thing to download are freetype sources from http://freetype.org. These will be necessary for native font rendering.

Toolchain setup

I have to point out that these instructions were tested only on linux. I suppose they will work with minimal changes also on Mac OS. I also presume that you were able to build open JavaFX. That means all tools like ant, gradle, gcc and jdk8 have been installed and are working all right. In addition to this you will need to download and install jdk7, Android SDK and Android NDK for native code compilation.  Installing all of them will take some time. Don't forget to put them in your path.

export ANDROID_SDK=/opt/android-sdk-linux
export ANDROID_NDK=/opt/android-ndk-r9b
export JAVA_HOME=/opt/jdk1.7.0
export PATH=$JAVA_HOME/bin:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK:$PATH

Freetype

Unzip freetype release sources first. We will have to cross compile them for arm. Firstly we will create a standalone toolchain for cross compiling installed in ~/work/ndk-standalone-19.
$ANDROID_NDK/build/tools/make-standalone-toolchain.sh  --platform=android-19 --install-dir=~/work/ndk-standalone-19

After the standalone toolchain has been created cross compile freetype with following script:

export TOOLCHAIN=~/work/freetype/ndk-standalone-19
export PATH=$TOOLCHAIN/bin:$PATH
export FREETYPE=`pwd`
./configure --host=arm-linux-androideabi --prefix=$FREETYPE/install --without-png --without-zlib --enable-shared
sed -i 's/\-version\-info \$(version_info)/-avoid-version/' builds/unix/unix-cc.mk
make
make install

It will compile and install freetype library into $FREETYPE/install. We will link to this install dir later on. It would be possible also to link openjfx font support dynamically against skia library available on Android which already contains freetype. It creates smaller result but can have compatibility problems.

Patching

Download patches javafx-android-compat.patch + android-tools.patch and patch jfx78 repository. I recommend to have look at patches. First one android-compat.patch updates openjfx build script, removes dependency on SharedSecret classes and updates LensLogger to remove dependency on jdk specific PlatformLogger. Second one android-tools.patch creates helper script in android-tools. The script helps to setup javaFX Android projects.

Building

Now is time to try the build. Run following script:

JAVA_HOME=/opt/jdk1.7.0
JDK_HOME=/opt/jdk1.7.0
ANDROID_SDK=/opt/android-sdk-linux
ANDROID_NDK=/opt/android-ndk-r9b
PATH=$JAVA_HOME/bin:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools:$ANDROID_NDK:$PATH
gradle -PDEBUG -PDALVIK_VM=true -PBINARY_STUB=~/work/binary_stub/linux/rt/lib/ext/jfxrt.jar \
-PFREETYPE_DIR=~/work/freetype/install -PCOMPILE_TARGETS=android

If everything went all right the output is in build/android-sdk :)

UPDATE: If you want to build without jfxrt.jar binary stub comment out jmx and sample apps from settings.gradle.

Create first JavaFX Android project

Use gradle script int android-tools. The script sets the project structure for you.   Following command creates Android HelloWorld project which links to a freshly built javafx runtime and to a HelloWorld application.

  • NAME is a name of Android project.
  • DIR where to create our first project.
  • PACKAGE is package name required by Android. It has nothing to do with a packaging of javafx application.
  • JFX_SDK points to our recently built runtime.
  • JFX_APP points to dist directory of javafx application. (where all application jars sit)
  • JFX_MAIN is fully qualified name of a main class.
gradle -PDEBUG -PDIR=/home/user/work -PNAME=HelloWorld -PPACKAGE=com.helloworld \
-PJFX_SDK=/home/user/work/jfx78/build/android-sdk -PJFX_APP=/home/user/NetBeansProjects/HelloWorld/dist \
-PJFX_MAIN=com.helloworld.HelloWorld createProject

Now cd to the created project and use it like any other android project. ant clean, debug, uninstall, installd will work. I haven't tried it from any IDE Eclipse nor Netbeans.

Special thanks to Stefan Fuchs and Daniel Zwolenski for the repositories used in this blog post.

Tuesday Jul 30, 2013

The peculiarities of JavaFX layout, pt.1

While working on JavaFX layouts, I have seen a number of bugs caused by a false assumption of the developer about the layouts. There are multiple types of Nodes that behave differently during the layout pass and use the layout information in a different way, which often leads to confusion as it might be quite hard to see the big picture just by reading the javadoc of the classes. That is why I decided to do a few blog posts that would describe the layout from a different point of view.

In this first part, we will look at the way layout works in detail, so I assume you already know the basics of JavaFX layout like types of layout classes, minimum/preferred/maximum sizes, content bias and resizability. If you don't, I can recommend you the JavaFX Class Tour tutorial from Amy Fowler. The subsequent part(s) will cover the common traps and pitfalls for custom layout/control developers and also for those who just want to do some simple adjustments of a layout in their applications. 

Note: there are still open issues that, once resolved, might change the behaviour described in this post. I will keep it up-to-date with the current 8.0 build available.  

How the particular classes use the layout

Some of the confusion originates in a different ways the layout classes are treated during the layout pass. Let's look at this table that describes the way layout bounds and size hints are computed. The last column shows the order in which the layout properties are used/set on the particular Node type. 

layout bounds computation minimum/preferred size resizeable layout pass order

Node
(leaf)

as "bounds in local"
without transformations, clip and effect
always equal to layout bounds no
  1. parent reads pref size to compute the area where the Node will be positioned
  2. the Node is positioned (node.relocate()) using it's current layoutBounds
Group
(autosize)

a sum of current bounds of it's children
(incl. transforms, clip and effect)
Note: querying layout bounds will trigger a layout
in order to compute the most up-to-date layout bounds 

always equal to layout bounds no
  1. parent reads pref size to compute the area where the Group will be positioned
    1. In order to compute the pref size, the Group must trigger the internal layout at this stage using Group.layout().
      Without this side-effect, the layout of a Group would require a second pass
  2. the Group is positioned using it's newly computed layoutBounds in step #1.1
Group
(not autosize)

a sum of current bounds of it's children
(incl. transforms, clip and effect)

always equal to layout bounds no
  1. parent reads pref size to compute the area where the Group will be positioned
  2. the Group is positioned using it's current layoutBounds
Region
Pane
Control
x,y : 0,0
width, height: set by the parent during the layout, by using resize() call
depends on the implementation
The parent of the Region uses the information to
compute the final size during the layout (which means
also the layout bounds)
yes
  1. parent reads pref size to compute the area where the Region will be positioned
    1. This is computed internally, without any side-effect
  2. the Region is resized to the newly computed size, changing it's layoutBounds
  3. the Region is positioned using it's new layoutBounds
  4. Region.layout() is triggered to compute the internal layout using the new size from step #2.


While this may seem to be very complicated, there's actually a good reason why the steps vary between the different types of Nodes. Let's look at how a generic layout pass of a layout Pane looks like:

  1. The layout is computed using the min./max./pref. size of the children. ( + content bias, baseline; we will cover that later)
  2. Resizeable child (means only Region/Pane/Control) is resized to the computed size.
  3. The child (any kind) is positioned according to it's layout bounds.

As you can see from the table, with the exception of Region, the min/pref size ( = layout bounds) is needed in order to correctly compute the layout . The Region however does not need to know it's layout bounds,  the layout bounds are being computed by the it's parent instead.

Moreover, autosize Group does need to layout itself before computing it's layout bounds, which means on the step #1 of it's parent's layout.  This again is absolutely different in Region, which does it's layout after step #3 (though theoretically it's possible after step #2), because the internal layout depends on the size available for the Region, which is known after the resize (at step #2). 

Here's a little diagram that shows the dependencies:


Layout Roots 

JavaFX has a concept of special Parents, called "layout roots". A layout root is a kind of a Parent that doesn't require it's own parent to be laid out when something changes in it's subtree.
Any Parent that satisfies one of these properties is a layout root:

  • managedProperty() is set to false
  • the Parent is a root of a Scene or a SubScene

You may think anything under a non-autosize Group should be also a layout root, but that is not true as a Group must be re-positioned when it's layout bounds change.

You can also use layout roots to validate the layout immediately by using layout() call on the layout root of the Node you want to validate. Very useful for instant measurements of the Node size. Example coming in the following blog post. :-)

The layout invalidation

Now you know how the layout pass works internally, but before anything can be laid-out, the current layout must be invalidated. This is done automatically for you when some child changes, but you may want to trigger a layout when some of your properties that define your layout changes. This is done by simply calling a requestLayout() method, like this:

    public final IntegerProperty helloProperty() {
        if (helloProperty == null) {
            helloProperty = new SimpleIntegerProperty(this, "hello") {
                @Override
                public void invalidated() {
                    requestLayout();
                }

            };
        }
        return helloProperty;
    }

The same method is often used for invalidating pre-computed data of the layout. It's very useful to pre-compute data for computePrefWidth/computePrefHeight and layoutChildren methods, so you don't have to compute the same 3-times. You can then invalidate this data in requestLayout, which is guaranteed to be called whenever a layout-affecting change is made in the subtree of a Parent.

Size bounds

The last source of confusion we will cover in this part are the bounds for width & height of a Region. The Region allows you to override computed size by calling setPrefWidth(double) / setMinWidth(double) / setMaxWidth(double) methods  ( + the equivalent for height). What many of you might find surprising is that the minimum size always takes precedence over maximum size, whether the maximum size is computed or overridden by the developer.

To get a Region/Control below it's (computed) minimum size, you have to override also the minimum size, not just maximum. As the minimum size should be really the smallest size when the Region/Control is usable, this logic serves to prevent layout from resizing the Region below it and keep it functional.

So much about the insides of layout processing. we will look more at some examples in the next post. 

About

JavaFX is a Java GUI toolkit, partially developed from Prague, Czech Republic. The Prague team uses this blog to post articles, code samples and insights about the range of topics the team members specialize in. This includes JavaFX Scenegraph (javafx.scene.*), JavaFX Core libraries & animations, iOS port & Android port.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
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
   
       
Today