APL(Alexa Presentation Language)で画面表示と同時に発話&アニメーションを実現する

#はじめに

AlexaSkill開発で意外と大変だなぁと感じるのがこのAPL。本気で画面付きデバイスに対応しようと思うとAPLを使うことになるのですが・・・

**マジで使い方がわからん！**<br>
もくもく会に参加しても「サンプルコード見るといいよ」と言われます。
違うんだ。サンプルコードのその先で詰まっているんだ！！

そんな方多くいらっしゃるのではないでしょうか？
昨年はこのAPLがわけわからず、微妙なスキルリリースをしていました・・・でも、最近やっとAPLへの理解が深まってきて、自分なりにAPLを使いこなした感のある、しかし誰も起動しない~~クソ~~スキルをリリースしました（涙）

**"モグモグあいさつタイム"**
https://www.amazon.co.jp/dp/B086M6LD8F

ここではこのスキルでも使っている、
"画面を出しながら発話"と"アニメーションを動かす"をどう実現するのか、自分の備忘録をかねて記事にさせていただきます。
ちなみに、自分はバックグラウンドがハードウェアエンジニアなので、こいつ何言ってんの？という点はあるかもしれません。ご了承ください。

#APLに対する基本的な理解
APLは**document**と**datasources**という2つの塊から構成されるjsonファイルに全ての表示内容や、アニメーションなら動かし方が書いてあり、このjsonファイルをデザインする事で画面付きスキルに任意の動作をさせることができます。
**document** - 全ての画面の設計データが入っている。これでAPLの画面がどのように表示されるのかが決まる。
**datasource** - documentの中で示されている画像データの情報などを記述する。しかしdocumentに直書きしている場合はそもそもdatasourceは不要。

割と静的な画面であればdocumentのみでOKなのですが、アニメーションではdatasourceを使わないとコードの書き方が結構面倒。複雑になってくると、documentをテンプレにして、datasourceの中身を変えることで毎回違う画面を作るようにしていくのが最良のやり方かと思います。

なぜこんな話するのか。このことを最初に書いた理由は、APLの開発の中で一番難しいのがこの**datasourceの書き方**だと考えているからです。

datasourceマスターしたら、割と簡単に凝った画面を作ることができます。個人的に考えるAPLの**鬼門はdatasource**です！！


# 1. APLテンプレートを作成する
ここでは画面にイメージを表示すると同時に、スキルが発話を始めるという動作を実現したいと思います。動作イメージは紙芝居です。

これ、画面にイメージを出す処理を書いて、`.speak()`で喋るんでしょ？と考えてしまいますよね。一言喋って画面は1種類であれば違和感なくていいのですが、そのやり方では発話の途中で画面の切り替えができないんです。
ということで、ここでは紙芝居をするベースのやり方を説明します。

すごいシンプルですが、APLのテンプレ開発画面で下の図のようにContainerにImageとTextを用意します。
Imageは全画面で用意します。textは初期で入っている内容を消して、特にサイズを指定せずに設置します。
![スクリーンショット 2020-05-10 22.53.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/7060d7b7-07a6-aab2-eab0-3f6264d63447.png)

置いたら、画像やtextは**id**を指定して、datasourceから画像のソースや発話するテキストの文章を取得するように記述します。
書き方は以下の通りです。
![スクリーンショット 2020-05-10 23.24.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/60fa26e4-c648-caf7-1371-ac70f62ef991.png)

![スクリーンショット 2020-05-10 23.17.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/2fd16680-438c-bde1-80a9-251cc7fa38fb.png)

![スクリーンショット 2020-05-10 23.24.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/f7510dee-b376-e4ec-f039-8c0e6bbaa309.png)
![スクリーンショット 2020-05-12 17.07.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/068d659c-35f3-8f22-f0ae-68889b72ab3d.png)

ちなみに
**ID** -> そのオブジェクトが何であるか指定するための名前
**Source** ->　実際に表示する画像コンテンツのこと
**Speech** -> 喋らせる内容、コンテンツのこと。SSMLタグをつけた文章だけでなく、mp3のリンクを貼るのもOK。ただし、amazon公式のサウンドライブラリは動かない(誰か知ってたら教えて欲しいです)。
を意味しています。

**注意**
テキストには**text**という項目がありますが、こちらは**画面に表示する内容**になります。こっちに書いても発話しません！！

ちなみに、SouceやSpeechの内容、なんだこれ？という感じですよね。

`${payload.source.properties.speakingText}`

これはどういうことかというと、payload(= datasources)の中のsourceという括りの中のpropertiesという括りの中にあるコンテンツの名称"speakingText"を参照するよという意味になります。

これで出来上がったテンプレートをダウンロードすると以下のjsonファイルが入手できます。


``` apl_template.json
{
    "document": {
        "type": "APL",
        "version": "1.2",
        "settings": {},
        "theme": "dark",
        "import": [],
        "resources": [],
        "styles": {},
        "onMount": [],
        "graphics": {},
        "commands": {},
        "layouts": {},
        "mainTemplate": {
            "parameters": [
                "payload"
            ],
            "items": [
                {
                    "type": "Container",
                    "height": "100%",
                    "width": "100%",
                    "paddingTop": "16dp",
                    "paddingLeft": "16dp",
                    "paddingRight": "16dp",
                    "paddingBottom": "16dp",
                    "items": [
                        {
                            "type": "Image",
                            "id": "imagesID",
                            "width": "100vw",
                            "height": "100vh",
                            "source": "${payload.source.properties.images}",
                            "scale": "best-fill",
                            "position": "absolute"
                        },
                        {
                            "type": "Text",
                            "id": "texts",
                            "width": "0",
                            "height": "0",
                            "paddingTop": "12dp",
                            "paddingBottom": "12dp",
                            "fontSize": "16dp",
                            "text": "${payload.source.properties.speakingText}"
                        }
                    ]
                }
            ]
        }
    },
    "datasources": {}
}
```

一番下に **datasources** がありますが、APLのテンテンプレート作成画面の項目にある**DATA**の項目に追加をすると、ダウンロードしたときに表示されます。
APLの画面を作るときにイメージの位置や大きさを把握する上でセットしておくと良いと思いますが、スキルの開発で上書きすることになる(上書きをして使うと、簡単にコンテンツの変更ができる)ので、確認程度の使い方で良いと思います。

# 2. スキルのコードを書く
APLのテンプレートができたら、次は実際にスキルのコードを書きます。
今回はHosted skillとして開発をします。

また詳細省きますが、画面付きスキルを作るには、**ビルド** タブ内の **インターフェース** を開き、
- Displayインターフェース
- Alexa Presentation Language
これら2つの項目を有効にする必要があります。

まずコードエディタを開いて、`apl_template.json`ファイルを作成します。
（ファイル名は任意ですが、拡張子は.jsonです）
![スクリーンショット 2020-05-11 17.56.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/557fdead-ea8a-18ee-1677-a719a862a4a5.png)

次に先ほどダウンロードしたjsonファイルの中身をそのまま全部コピペします。
![スクリーンショット 2020-05-11 17.57.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/17191925-8f55-91d8-e4c4-4d7d8d73e6ef.png)

次に、`index.js`を開きます。
`index.js`には
1. 画面付きデバイスであるかどうかを判断する
2. 画面付きのデバイスの場合は画面に表示する内容を記述する(画像や発話、アニメーションを定義するのはここ！)
主にこの2つのコードを書きます。

LaunchRequestHandlerが呼ばれると先ほど作ったAPLテンプレートが呼び出されるコードを書くと次のようになります。
書く項目の説明はコード内にコメントとして記述しています。

```index.js
// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
// session persistence, api calls, and more.
const Alexa = require('ask-sdk-core');

//画面判定。このfunctionは必須です
const supportsApl = handlerInput => {
  const hasDisplay =
    handlerInput.requestEnvelope.context &&
    handlerInput.requestEnvelope.context.System &&
    handlerInput.requestEnvelope.context.System.device &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces &&
    handlerInput.requestEnvelope.context.System.device.supportedInterfaces[
      "Alexa.Presentation.APL"
    ];

  return hasDisplay;
};

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは。今日は2020年5月12日です';
        const speakOutputWith = "こんにちは。画面付きエコーデバイスです。今日は2020年5月12日です";
        
        //画面付きデバイスかどうかの判断をする
        if (supportsApl(handlerInput)) {
            const aplView = require("./apl_template.json");
            //apl_template.jsonを参照する
            handlerInput.responseBuilder
            .addDirective({//APLの静的な設定を追加する
                type: "Alexa.Presentation.APL.RenderDocument",
                version: "1.3",//最新のversion1.3で動かします。Animationは1.1からでないと動きません。
                token: "token",//tokenは今回は適当につけてます。
                document: aplView.document,//apl_templateのdocumentの項目を参照する
                datasources: {//datasourceはapl_template.jsonに無かったので、ここに追加する。
                    source: {
                        type: "object",
                        properties: {//このpropertiesの項目にテキストやイメージのリンク先や文章を記述する
                            images: "https://d2o906d8ln7ui1.cloudfront.net/placeholder_image.png", //APLテンプレで出てくる画像を選択する
                            speakingText: "<speak>" + speakOutputWith +"</speak>"//入力したテキストを再生するときは、ssmlのspeakタグが必須！
                        },
                        transformers: [//transformersはテキストを読み上げをするときに設定する項目です。これがないと発話しない。逆にmp3でオーディオを再生するだけなら不要な項目
                            {
                                inputPath: "speakingText",//propertiesの中に含まれる"speakingText"を入力に定義
                                transformer: "ssmlToSpeech",//speakタグが付いている= SSMLなので、ssmlToSpeachというコマンドで発話できるように変換する。という事が指示されている。
                            }
                        ]
                    }
                }
            })
            .addDirective({//もう一つaddDirectiveを追加します。今度は実際にどのように表示するのかというコマンドを書きます。
                type: "Alexa.Presentation.APL.ExecuteCommands",
                token: "token",
                commands: [{
                                type:"SpeakItem",
                                componentId:"texts"//先にjsonで設定したIDをここで指示します。イメージとしてはdocumentの項目のどれを使うのかを選ぶ感じです。
                }]
            });
        }else{//画面非対応デバイスの時の処理
              handlerInput.responseBuilder.speak(speakOutput);
        }
        
        //画面有無で処理をそれぞれ書いたら、responseBuilderをリターンする
        return handlerInput.responseBuilder
            .withShouldEndSession(true)
            .getResponse();
    }
};
```

このコードは一部のみなので、他の部分も書く必要がありますが、スキルを起動すると画面を表示し、発話してくれます。
コードをさくっと書いていますが、前述の**datasourcesが鬼門**を突破してここに行き着きました。
実はdatasoucesの書き方は自由度が高く、発話をしない場合はもっとシンプルに書いても問題なく画像やテキストの表示ができます。
ただ、画面表示とともに発話をさせようと思ったら、上記のpropertiesを使ってdatasourcesを書く必要があります。

# 3.アニメーションを追加する
先ほどのコードは発話するだけでした。今度は画像を実際に動かしながら発話をするようにしてみます。
先ほど書いたコードの、２つめの`Alexa.Presentation.APL.ExecuteCommands`が書かれた`addDirective()`の中にアニメーションをするコマンドを書いていきます。

ここでは画像が表示されると同時に画像が回転(リピート3回)。それとともに"回転します"と発話。500ミリ秒待った後、3倍サイズに3秒でなって終わりという流れのコードを書きます

コードの説明はコメントアウトを参照してください


```index.js
//2つ目の.addDirective()を変更しました
            .addDirective({//もう一つaddDirectiveを追加します。今度は実際にどのように表示するのかというコマンドを書きます。
                type: "Alexa.Presentation.APL.ExecuteCommands",
                token: "token",
                commands: [{
                    type:"Sequential",//sequentialというコマンドの中に書く。こうすると、1つのアクションが終わると次の動作が行われるようになる。(複数の動作が可能になる。)
                    commands:[
                        {
                            type: "Parallel",//pararellというコマンドを定義しました。こうすることでアニメーションと同時に他の事(今回は発話)ができるようになります。
                            delay:0, //pararellは同時に動かす事ができるだけでなく、delayをかけると、一番上の動作からdelayかけた分送らせて次の動作をさせる事ができます。これは前の動作が終わっていなくてもOK。
                                        //ちなみに、これはベースとなるdelayなので、個別にdelayを書くとその時間に合算される形になる。
                            commands:[
                                {
                                    type:"AnimateItem",//アニメーションコマンドをすると定義
                                    easing:"linear",//どんな感じでイージングするかを定義
                                    duration: 1500,//1回のアニメーションの開始から終わりまでの時間。
                                    repeatCount:3,//同じアニメーションを何回繰り返すか定義リピート回数が3ということは、4回動くということです。
                                    repeatMode:"restart",//前のアニメーションが終わったら最初の一に戻って再生を開始すると定義。(逆再生もある)
                                    componentId:"imagesID",//どのIDのコンテンツにこのアニメーションを加えるか。IDを指定
                                    value:[//valueでは画像の仕様やどういう内容のアニメーションにするのか定義する
                                        {
                                            property:"opacity",//不透明度
                                            to:1//1にする事で、最初からずっと不透明にする。
                                        },
                                        {
                                            property:"transform",//何かしらの変形をするときはtransformを定義。
                                            from:[{rotate:360}],//360度の位置からスタートして
                                            to:[{rotate:0}]//0度で終わる。 = 1周する
                                        }
                                    ]
                                },
                                {//アニメーションが始まると同時に(delay = 0だから)発話をする
                                    type: "SpeakItem",//発話コマンド
                                    componentId: "texts"//textsに定義したdatasouceの内容を話す。
                                },
                            ]
                        },
                        {   //ここで500msec待つ。
                            type: "Idle",
                            delay: 500,
                            screenLock: true
                        },
                        {
                            type:"AnimateItem",//アニメーションコマンドをすると定義
                            easing:"ease-out",//どんな感じでイージングするかを定義
                            duration: 3000,//1回のアニメーションの開始から終わりまでの時間。
                            repeatCount:0,//同じアニメーションを何回繰り返すか定義
                            repeatMode:"restart",//前のアニメーションが終わったら最初の一に戻って再生を開始すると定義。(逆再生もある)
                            componentId:"imagesID",//どのIDのコンテンツにこのアニメーションを加えるか。IDを指定
                            value:[//valueでは画像の仕様やどういう内容のアニメーションにするのか定義する
                                {
                                    property:"opacity",//不透明度
                                    to:1//1にする事で、最初からずっと不透明にする。
                                },
                                {
                                    property:"transform",//何かしらの変形をするときはtransformを定義。
                                    from: [{ scale : 1 }],//スケールの変更をする。等倍からスタート
                                    to: [{ scale: 3 }]//最終的に3倍になる
                                }
                            ]
                        }
                    ]
                }]
            });

```

実際にこの`addDirective()`で実行すると、こんな感じの動作をします。
gifなので、アニメーションだけですみません。ちゃんとアニメーションやりながら発話もしています。

![test animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324560/e66d0894-95c3-680a-770a-b7598279a522.gif)

#さいごに
APLの発話やアニメーションの触りしか触れていませんが、発話だったら、別のtextコンテンツを追加することで、別の発話をさせる事ができます。
1つの箱には1つのコンテンツしかできないみたいなので、中身を変えて発話させるといったことはできなさそうです。
またアニメーションもシンプルな動作しか書いていません。
回転させながらX軸プラス方向に動かすとか、大きくしながら回転するといったアニメーションも可能です。

今回このAPLを覚えたことで、今まで抵抗を感じていた画面付きスキルへの抵抗が大きく減りました！みなさんも、アニメーションなどを使って面白い画面付きスキルを作ってみてはいかがでしょうか？