1. Qiita
  2. 投稿
  3. Java

JavaFXで動くプロ生ちゃんデスクトップマスコットを作る

  • 6
    いいね
  • 0
    コメント

この記事はJavaFX Advent Calendar 2016の8日目です。昨日は@Ziphilさんの「FXML で相対サイズ指定」です。明日は@arachan@githubさんです。
プロ生ちゃん Advent Calendar 2016ではありません。

概要

JavaFXの透明Stageとアニメーションを使って無駄に動くプロ生ちゃんデスクトップマスコットを作る、ただそれだけです。

プロ生ちゃんとは何か

知らない人もそういないとは思いますが、知らない人は公式サイトを見ましょう。

作ったもの

GitHubにあります。

  • まばたきする
  • ドラッグした時に表情が変わる
  • ダブルクリックでまばたき、口パクが切り替わる

という機能を作りました。
プロ生ちゃんのSD画像素材を一部使用しています。

JavaFXで非矩形ウインドウを表示する。

まずはこんな感じのレイアウトを用意します。
※画像はそのままだと大きすぎるので206*500に縮小しています。

スクリーンショット 2016-12-08 15.53.42.png

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

<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.Pane?>

<Pane xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.skht777.pronama.MascotController">
    <children>
        <ImageView fx:id="view">
         <image>
            <Image url="/sd_eye0.png" />
         </image>
        </ImageView>
    </children>
</Pane>

コントローラも適当に作ります。

MascotController.java
package com.skht777.pronama;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.ImageView;

import java.net.URL;
import java.util.ResourceBundle;

public class MascotController implements Initializable {

    @FXML
    private ImageView view;

    @Override
    public void initialize(URL location, ResourceBundle resources) {

    }
}

あとはApplication::startでStageのスタイルをTRANSPARENTにしてSceneの背景をnullにすれば、透過された画像が表示されます。
ちなみに、ちゃんと見えている部分だけマウスイベントを拾ってくれます。

Main.java
package com.skht777.pronama;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.io.IOException;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {
        primaryStage.initStyle(StageStyle.TRANSPARENT);
        Scene scene = new Scene(FXMLLoader.load(getClass().getResource("mascot.fxml")));
        scene.setFill(null);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

スクリーンショット 2016-12-08 16.58.04.png

画像をアニメーションする、切り替える

Timelineクラスを使ってImageViewのImageを一定時間ごとに切り替えることでアニメーションができます。
Timelineの使い方は桜庭さんが詳細な解説を書いてくれているのでそちらを御覧ください。

切り替えるのが面倒なので、簡単にバリエーションを増やせるようにこんなのを作りました。

State.java
package com.skht777.pronama.state;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.image.ImageView;
import javafx.stage.Window;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class State {

    private static ImageView target;
    private static StateType current;
    private Timeline time;
    private double width, height;

    public static void setTarget(ImageView target) {
        State.target = target;
    }

    public static void setState(StateType type) {
        if (type == current) return;
        Optional.ofNullable(current).map(StateType::getState).ifPresent(s -> s.time.stop());
        current = type;
        current.getState().setTarget();
    }

    // 呼び出す度に状態を変えたい時用
    public static void setState(StateType type, StateType... alt) { 
        if (type == current) setState(alt[0]);
        else setState(IntStream.range(0, alt.length - 1).filter(i -> alt[i] == current)
                .mapToObj(i -> alt[i + 1]).findFirst().orElse(type));
    }

    State(int rate, Frame... frames) {
        width = frames[0].getImage().getWidth();
        height = frames[0].getImage().getHeight();
        List<KeyFrame> keys = Arrays.stream(frames)
                .map(frame -> new KeyFrame(frame.getDuration(), e -> nextFrame(frame)))
                .collect(Collectors.toList());
        time = new Timeline(rate);
        time.setCycleCount(Timeline.INDEFINITE); // ループさせる
        time.getKeyFrames().addAll(keys);
    }

    private void setTarget() {
        time.play();
        // 一応書いてるけどあまり気にしなくてもいいかもしれない
        Window window = target.getScene().getWindow();
        window.setWidth(width);
        window.setHeight(height);
    }

    private void nextFrame(Frame frame) {
        if (target.getImage() != frame.getImage()) target.setImage(frame.getImage());
    }

}
Frame.java
package com.skht777.pronama.state;

import javafx.scene.image.Image;
import javafx.util.Duration;

import java.util.HashMap;
import java.util.Map;

class Frame {

    private static Map<String, Image> image;
    private int time;
    private String imageName;

    static {
        image = new HashMap<>();
    }

    Frame(int time, String imageName) {
        this.time = time;
        this.imageName = imageName;
        image.putIfAbsent(imageName, new Image(imageName));
    }

    Duration getDuration() {
        return Duration.millis(time);
    }

    Image getImage() {
        return image.get(imageName);
    }

}
StateType.java
package com.skht777.pronama.state;

public enum StateType {

    // 列挙定数を増やすと扱えるアニメーションが増える
    NORMAL(3000,
            new Frame(0, "/sd_eye1.png"),
            new Frame(100, "/sd_eye2.png"),
            new Frame(200, "/sd_eye1.png"),
            new Frame(300, "/sd_eye0.png"),
            new Frame(3000, "/sd_eye0.png")),
    DRAG(Integer.MAX_VALUE,
            new Frame(1, "/sd04.png")),
    SPEECH(3000,
            new Frame(0, "/sd_mouth1.png"),
            new Frame(200, "/sd_mouth2.png"),
            new Frame(400, "/sd_mouth1.png"),
            new Frame(600, "/sd_mouth0.png"));

    private State state;

    StateType(int rate, Frame... frames) {
        state = new State(rate, frames);
    }

    public State getState() {
        return state;
    }

}

あとはトリガーとなるマウスイベントなんかでState::setStateをStateTypeを指定して呼び出せばアニメーションを切り替えられます。

おまけ

吹き出しとか付けると良さそうだったんですが、プロが惜しみない技術を尽くして吹き出しを作っているのを見て諦めました。

参考

プロ生ちゃん公式サイト
JavaFX 2ではじめる、GUI開発 第13回 タイムラインを使ったアニメーション
JavaFX in the Box
JavaFXの練習5:Drag&DropでNodeを移動する