JavaFXのListViewを拡張する

by orekyuu 0 Comments

ListViewのセルをカスタマイズする方法について書くよ。

1.ListViewのセルに埋め込むFXMLを作成する

例としてセルにテキストとボタンを入れてみる。
まずはセルに埋め込むFXMLをちゃっちゃと書いちゃいます。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>


<HBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
      xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="CellController">
    <children>
        <Label text="Label" fx:id="label"/>
        <Pane HBox.hgrow="ALWAYS"/>
        <Button mnemonicParsing="false" text="Button" fx:id="button"/>
    </children>
</HBox>

2.カスタムListCellを作る

ListViewに表示されるセルはListCellクラスでできています。
カスタマイズしたListCellを作りたい場合はこれを継承して新しいセルを作ります。
ここではCustomListCellという名前で作成します。

public class CustomListCell extends ListCell<Data>{
}

一緒にCellに表示するための情報を纏めたDataクラスを作成します。

public class Data {
    private final String text;

    public Data(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

3.FXMLを埋め込む

ListCellはListViewで表示されている範囲だけ作成されます。
ListViewのItemが100個あったとしても、実際に見えている部分が10個であればListCellは10個くらいしか存在しません。
スクロールやItemが更新された時にListCellのupdateitemが呼び出され、ListCellが表示内容を変更します。

public class CustomListCell extends ListCell<Data> {

    private CellController controller;

    @Override
    protected void updateItem(Data item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            //空になったので表示する内容はなし
            setGraphic(null);
            setText(null);
            controller = null;
            return;
        }

        if (controller == null || getGraphic() == null) {
            //コントローラかGraphicがなければFXMLからNodeと一緒に作成
            try {
                FXMLLoader fxmlLoader = new FXMLLoader();
                Node node = fxmlLoader.load(getClass().getResourceAsStream("cell.fxml"));
                controller = fxmlLoader.getController();
                setGraphic(node);
                controller.update(item);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        } else {
            //すでにある場合は新しく作らずに使い回し
            controller.update(item);
        }
    }
}

次にセルのコントローラを作成

public class CellController {
    @FXML
    private Label label;
    @FXML
    private Button button;

    public void update(Data item) {
        label.setText(item.getText());
        button.setOnAction(e -> System.out.println(item.getText()));
    }
}

4.CellFactoryを設定

ListView<Data> listView = new ListView<>();
listView.setCellFactory(param -> new CustomListCell());

おしまい。

関ジャバのJavaOne2014報告会に行ってきた

by orekyuu 0 Comments

関西Javaエンジニアの会スペシャル! JavaOne 2014 報告会行ってきました。
桜庭さんのProject ValhallaとProject Panamaの話が面白かった。
JavaFXの話がなかったのが残念・・・。

きつねさんの話を聞いてJavaOne行きたくなったけど、実際金銭的な問題だったりが難しい・・・あと英語力・・・。

懇親会では寺田さんや桜庭さんから面白い話を聞けて面白かった。

最後にサンフランシスコ土産争奪じゃんけん大会での戦利品。
これは家宝にするしか無い!
2014-11-23 00.46.37

JavaFXでアニメーションして画面遷移

by orekyuu 0 Comments

今作ってるアプリケーションでアニメーションして画面遷移がしたかったので調べてみました。

StackPaneを継承したControllablePaneを作成して、そこで画面遷移をさせようと思います。
画面遷移するたびにFXMLをロードするのは、FXMLやコントローラの処理によっては重くなりそうなので、事前にロードしておいてControllablePaneにインスタンスをおいておくことにします。

sample.controllable.ControllablePane.java

public class ControllablePane extends StackPane {
    private Map<String, Node> nodes = new HashMap<>();
    //FXMLをロード
    public void loadNode(String id, InputStream resource) {
        try {
            FXMLLoader loader = new FXMLLoader();
            Parent loadNode = loader.load(resource);
            nodes.put(id, loadNode);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
    //FXMLをアンロード
    public void unloadNode(String id) {
        nodes.remove(id);
    }
}

画面遷移用のsetNodeメソッドを定義します。
遷移するアニメーションの方法を変更できるようにしたいので、NodeTransitionAnimatorインターフェイスを作って、実装したクラスに移譲することにします。

sample.controllable.NodeTransitionAnimator.java

public interface NodeTransitionAnimator {
    void transition(ControllablePane pane, Node before, Node after);
}

sample.controllable.ControllablePane.java

    private NodeTransitionAnimator animator;

    public void setAnimator(NodeTransitionAnimator animator) {
        this.animator = animator;
    }
    //画面遷移
    public void setNode(String id) {
        if(nodes.get(id) == null) {
            throw new NullPointerException(id + " is not registered.");
        }

        animatePane(getChildren().isEmpty() ? null : getChildren().get(0), nodes.get(id));
    }

    //アニメーションさせる
    private void animatePane(Node before, Node after) {
        animator.transition(this, before, after);
    }

画面遷移用のアニメーションを作成します。
フェードして切り替わるFadeAnimatorを作りたいと思います。
フェードを作るときはjavafx.animation.FadeTransitionを使用すると楽です。
sample.controllable.animator.FadeAnimator.java

public class FadeAnimator implements NodeTransitionAnimator {

    private int duration;
    public FadeAnimator(int duration) {
        this.duration = duration;
    }
    @Override
    public void transition(ControllablePane pane, Node before, Node after) {
        if(before != null) {
            after.setOpacity(0.0);//一瞬1.0のまま表示されるのを防ぐ
            FadeTransition fadeOut = new FadeTransition(new Duration(duration), before);
            fadeOut.setFromValue(1.0);
            fadeOut.setToValue(0.0);
            fadeOut.setOnFinished(e -> {
                pane.getChildren().remove(0);
                pane.getChildren().add(0, after);
                FadeTransition fadeIn = new FadeTransition(new Duration(duration), after);
                fadeIn.setFromValue(0.0);
                fadeIn.setToValue(1.0);
                fadeIn.play();
            });
            fadeOut.play();
        } else {
            after.setOpacity(0.0);//一瞬1.0のまま表示されるのを防ぐ
            pane.getChildren().add(after);
            FadeTransition fadeIn = new FadeTransition(new Duration(duration), after);
            fadeIn.setFromValue(0.0);
            fadeIn.setToValue(1.0);
            fadeIn.play();
        }
    }
}

画面遷移はControllablePaneに入れているNodeを入れ替えればいいだけなので、fadeOutのアニメーションが終了したタイミングで古いNodeを削除し、新しいNodeを追加しています。
あとはfadeInするだけでいいですね。
else文の中身は特に説明はいらないと思います。

注意する点としては、afterをControllablePaneに追加する前に透明にしておかないと、一瞬不透明なNodeが表示されてしまい、ちらついている用に見えてしまいます。

ControllablePaneに入れられるNodeから画面遷移できるようにします。
FXMLロード時にControllerにControllablePaneのインスタンスを渡します。
sample.controllable.ControllablePane.java

    //FXMLをロード
    public void loadNode(String id, InputStream resource) {
        try {
            FXMLLoader loader = new FXMLLoader();
            Parent loadNode = loader.load(resource);
            //ここを追加
            Object obj = loader.getController();
            if (obj instanceof ControllablePaneController) {
                ControllablePaneController screen = (ControllablePaneController) obj;
                screen.setScreenParent(this);
            }
            nodes.put(id, loadNode);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

sample.controllable.ControllablePaneController.java

public interface ControllablePaneController {
    void setScreenParent(ControllablePane controllablePane);
}

では実際に動かしてみます。
sample.sample.fxml

<?import javafx.scene.layout.AnchorPane?>
<?import sample.controllable.ControllablePane?>
<AnchorPane fx:controller="sample.Controller"
          xmlns:fx="http://javafx.com/fxml">
    <ControllablePane fx:id="controllablePane" prefHeight="400.0" prefWidth="600.0"/>
</AnchorPane>

sample.Main.java

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("画面遷移");
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }


    public static void main(String[] args) {
        launch(args);
    }
}

sample.Controller.java

public class Controller implements Initializable {
    @FXML
    private ControllablePane controllablePane;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        controllablePane.loadNode(Screen1Controller.ID, getClass().getResourceAsStream("screen1.fxml"));
        controllablePane.loadNode(Screen2Controller.ID, getClass().getResourceAsStream("screen2.fxml"));
        controllablePane.loadNode(Screen3Controller.ID, getClass().getResourceAsStream("screen3.fxml"));
        controllablePane.setAnimator(new FadeAnimator(300));
        controllablePane.setNode(Screen1Controller.ID);
    }
}

sample.screen1.fxml

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Screen1Controller">
   <children>
      <Button onAction="#goToScreen2" layoutX="102.0" layoutY="350.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="272.0" text="画面2へ" AnchorPane.bottomAnchor="25.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
      <Button onAction="#goToScreen3" layoutX="272.0" layoutY="278.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="326.0" text="画面3へ" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
      <Label layoutX="285.0" layoutY="6.0" maxWidth="1.7976931348623157E308" minWidth="0.0" text="画面1" textAlignment="CENTER" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="6.0">
         <font>
            <Font size="32.0" />
         </font>
      </Label>
   </children>
</AnchorPane>

sample.Screen1Controller.java

public class Screen1Controller implements ControllablePaneController {

    private ControllablePane controllablePane;
    public static String ID = "Screen1";

    @Override
    public void setScreenParent(ControllablePane controllablePane) {
        this.controllablePane = controllablePane;
    }

    public void goToScreen2() {
        controllablePane.setNode(Screen2Controller.ID);
    }

    public void goToScreen3() {
        controllablePane.setNode(Screen3Controller.ID);
    }
}

sample.screen2.fxml

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Screen2Controller">
   <children>
      <Button onAction="#goToScreen1" layoutX="102.0" layoutY="350.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="272.0" text="画面1へ" AnchorPane.bottomAnchor="25.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
      <Button onAction="#goToScreen3" layoutX="272.0" layoutY="278.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="326.0" text="画面3へ" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
      <Label layoutX="285.0" layoutY="6.0" maxWidth="1.7976931348623157E308" minWidth="0.0" text="画面2" textAlignment="CENTER" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="6.0">
         <font>
            <Font size="32.0" />
         </font>
      </Label>
   </children>
</AnchorPane>

sample.Screen2Controller.java

public class Screen2Controller implements ControllablePaneController {

    private ControllablePane controllablePane;
    public static String ID = "Screen2";
    @Override
    public void setScreenParent(ControllablePane controllablePane) {
        this.controllablePane = controllablePane;
    }

    public void goToScreen1() {
        controllablePane.setNode(Screen1Controller.ID);
    }

    public void goToScreen3() {
        controllablePane.setNode(Screen3Controller.ID);
    }
}

sample.screen3.fxml

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Screen3Controller">
   <children>
      <Button onAction="#goToScreen1" layoutX="102.0" layoutY="350.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="272.0" text="画面1へ" AnchorPane.bottomAnchor="25.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
      <Button onAction="#goToScreen2" layoutX="272.0" layoutY="278.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="326.0" text="画面2へ" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
      <Label layoutX="285.0" layoutY="6.0" maxWidth="1.7976931348623157E308" minWidth="0.0" text="画面3" textAlignment="CENTER" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="6.0">
         <font>
            <Font size="32.0" />
         </font>
      </Label>
   </children>
</AnchorPane>

sample.Screen3Controller.java

public class Screen3Controller implements ControllablePaneController {

    private ControllablePane controllablePane;
    public static String ID = "Screen3";
    @Override
    public void setScreenParent(ControllablePane controllablePane) {
        this.controllablePane = controllablePane;
    }

    public void goToScreen1() {
        controllablePane.setNode(Screen1Controller.ID);
    }

    public void goToScreen2() {
        controllablePane.setNode(Screen2Controller.ID);
    }
}

完成したソースコードはgistにおいておきました。
完成したソースコード