Alexaスキル開発メモ – APL(Alexa Presentation Language)を噛み砕く編

Sponsored Links
Sponsored Links

Alexa Presentation Language(APL)を活用してAlexaスキル「あかちゃんきろく」を作成しました。

その際に学んだことのメモ・まとめでっす。

スキル「あかちゃんきろく」は↓で紹介しています。操作動画もあります。
Alexaスキル「あかちゃんきろく」~声でもタッチでも!手がふさがっててもしっかり育児記録&確認~

アレクサスキル開発のベースとなるASK SDK v2 Node.jsについても噛み砕いております↓。
Alexaスキル開発メモ – ASK SDK v2 Node.jsを噛み砕く編

APL大枠

  • 公式ドキュメント
  • APLは、イメージはJSONで書くフロントエンド(HTML/CSS的な感じ)
  • itemにコンポーネント(img, text, touchWrapper等)を配置して構成

NodejsでAPL画面出力

  • Responseに対してRenderDocumentDirectiveを追加することで画面描画
  • プロパティのdocumentにレイアウト構成、datasourcesにデータ定義を設定構成

ResponseBuilderのメソッドでAPL描画用命令をaddDirectiveしたコード例

builder.addDirective({
      type : 'Alexa.Presentation.APL.RenderDocument',
      version: '1.0',
      document: require('./homepage.json'),
      datasources: require('./data.json')
})

 

注意として、現状APLサポートがない端末にAlexa.Presentation.APL.RenderDocumentのDirectiveを返すと例外扱いとなる。
Eventリクエストデータのcontext.Systemから判定できるので、builderのgetResponse前で分岐を入れておく↓。

function isSupportAPL(handlerInput) {
  const interfaces = handlerInput.requestEnvelope.context.System.device.supportedInterfaces;
  return interfaces && interfaces["Alexa.Presentation.APL"];
}

function getResponse(handlerInput)
{
  ...(いろんな処理)

  let builder = handlerInput.responseBuilder
    .speak(speechOutput)
    .withShouldEndSession(false);
  if (isSupportAPL(handlerInput)) {
    builder.addDirective({
      type: 'Alexa.Presentation.APL.RenderDocument',
      version: '1.0',
      document: require('./actionList.json'),
      datasources: require('./actionList_data.json')
    });
  }
  return builder.getResponse();
}

documentプロパティの構成

上記RenderDocumentのコアとなるプロパティ「document」に描画内容・レイアウトを記述する。

以下が大枠。

"type", "version", "theme"などのヘッダ情報
"import" : [
   インポートするパッケージの指定
]
"resources" : [
   カラー、ディメンション(各サイズやPadding等)の定義(CSS部分)
],
"styles": {
   スタイルの定義。(CSS部分)
   resourcesで定義したカラーやサイズ等を利用したまとまったスタイルとして定義できる
},
"layouts": {
   レイアウトの定義。(HTML部分)
   レイアウトは、コンポーネントやレイアウトを階層定義したライブラリ的なもの。
},
"mainTemplate": {
   メインのレイアウト記述場所(HTML部分)
}

import

  • resources、styles、layoutsを定義した別のパッケージをインポートする
  • Alexa APLパッケージという、デフォルトで用意されたパッケージがある

以下の記載でAlexa APL パッケージが読み込まれる(基本的にサンプルに必ず書いてある)。

"import": [
    {
        "name": "alexa-layouts",
        "version": "1.0.0"
    }
],

パッケージの中身:Alexa APL パッケージ
具体的には以下が利用可能

  • Styles(カラーやテキストスタイル等)
  • Viewportプロファイル(デバイスごとの画面情報)
  • アイコンや戻るボタン、ヒントを搭載したAlexa Headerレイアウト、Footerレイアウト
    • AlexaHeader等を使う場合、あいだのコンテンツをContainerでくくって、shrinkで間に収まるようにする感じ

resources

 

現状は以下を自由な名前付き定義ができるという用途

  • カラー定義
  • ディメンション(フォントサイズ、パディングやマージン)
  • 文字列リソースを定義
  • bool値

// resources 定義
"colors" : {
   "mycolor" : "#ff0000"
}

// 使用時
"color" : "@mycolor"

styles

  • resourses などのプロパティを使ってまとめたStyleを作れる(CSSのイメージ)
  • resources と違ってこっちは使うときに @アノテーションが不要
  • AlexaのStylesを importすることで標準のいろいろなスタイルが使える
  • styleを継承した拡張も可能、AlexaデフォルトのStyleをちょっといじるとか可能

独自定義の例

// 定義(継承の例)
"listTextStyle": {
    "extend": "textStyleBody",
    "values": [
        {
            "when": "${@viewportSizeClass == @viewportClassSmallSmall}",
            "fontSize": "22"
        }
    ]
},

// 使用
{
  "type": "Text",
  "style": "listTextStyle",
}

layoutの書き方

  • 繰り返し利用するようなlayoutの固まり単位で定義できる(HTML/JSのイメージ)
  • item の type にLayout名指定で使える
  • layoutsの中で定義したlayoutを、layoutsの中でも利用可能
  • “item”には単一のコンポーネント、”items”にはコンポーネントの配列を書く
  • でも”item”も配列で書いて、whenでの分岐などを並列に書ける
  • parameters[] で中身のデータとバインディング

独自レイアウト定義。whenでviewportで定義分岐。

// 定義
"MyHeader": {
    "parameters": [
        "title"
    ],
    "item": [
        {
            "when": "${@viewportSizeClass == @viewportClassSmallSmall}",
            "type": "Text",
            "text": "${title}",
            ...
        },
        {
            "type": "Text",
            ...
        }
    ]
}

// 利用
"items":[
    {
        "type": "MyHeader",
        "title": "タイトル"
    },
]

mainTemplate

  • メインとなる描画エントリitemを記載。(HTMLのイメージ)
  • 上記layoutやresourcesを無視してゴリゴリ書いてもいいし、それらで効率的に書くもよし

datasourcesの中身

  • json形式のオブジェクトで自由に記載できる
  • 上記document(のlayoutやmainTemplate)内で、payloadから参照可能
  • payload.[Object名].[プロパティ名] みたいな感じでアクセス
  • APLにバインドするデータは一個Objectをかまさないとダメ
// だめな例
datasources : {
   title : "タイトル"
}
// 利用時:↓みたいには使えない
payload.title
// 正しい例
datasources : {
   data : {
      titlte : "タイトル"
   }
}
// 利用時
payload.data.title

viewport size などで分岐

  • Viewportのサイズや形などの定義を利用した分岐を行える
  • つまり、Echo Spot用とEcho Show用のStyleやLayoutを変えられる
  • サンプルに多いshapeよりはSizeやProfileで分岐スべきな気がする。 Mediaクエリっぽいのはこっち。
  • 記述例には@アノテーションがあるけど、実際はなくても動くみたい。

利用例

"listTextStyle": {
    "extend": "textStyleBody",
    "values": [
        {
            "when": "${@viewportSizeClass == @viewportClassSmallSmall}",
            "fontSize": "22"
        }
    ]
},

 
実装ポイント

  • できるだけresourcesの方で分岐すると楽ちん
    • レイアウトの分岐は大きくなりやすいから
    • HTML(レイアウト)じゃなくてCSS(スタイル定義)でメディアクエリするのと同じように
  • レイアウト構成自体を変えるならlayoutの方で分岐せざるを得ない

Component

描画対象である「item」を構成するのがコンポーネント。以下のように様々な種類がある。

以下各ポイントメモ。

共通プロパティ

  • id : Componentの名前。ExecuteCommandsなどでいじりたいComponentに指定したりする
  • style : 上記のStylesで定義したスタイルを適用
  • when : 展開(=Inflate)条件の定義
  • bindでコンポーネント内で自由な名前で使える(構文↓)
"bind" : [
   { "name": .. , "value" : ...}
]

Container : 複数のデータをバインドしたデータ定義

  • direction : rowで横、 columnで縦
  • alignItems : directionと逆側に対する並べ方設定。 (row でcenterなら縦方向の真ん中になる)
  • justifyContent : direction 方向に対するコンテントの並べ方。 (row で center なら 横方向の真ん中による)
  • rowでContainerを作った場合、 Text などは、折返しが効かなくなるので注意。colmunのContainerでサイズ区切ってラップすればいいはず。
  • いわゆるHTMLの<div>
  • Flexboxレイアウトの様にいい感じに配置、みたいなことはまだできない

TouchWrapper : タッチイベント

  • onPress でタッチ時に SendEventできる
  • 基本的に1つの item をラップする。並列に書く

定義例

{
    "type": "TouchWrapper",
    "item": {
        "type": "Text",
        "text": "ボタン0",
    },
    "onPress": {
        "type": "SendEvent",
        "arguments": [
            "button0"
        ]
    }
}

タッチイベント処理の詳しい実装は後述。

Image : 画像

  • width, height はすべて指定する、デフォルトは 100dp。
    • アスペクト比を保ったままの小さい方が優先されるので、両方のサイズを指定しないと小さくなる。
  • Imageのスケールとか
    • scale は あくまで、Image の width, height で指定されたBoundingBoxに対して、 元のイメージのサイズをあわせる という処理。
    • APLの解説に 「コンテナ」 に合わせるって言葉があるから上位のコンテナに合いそうだけどあわない
    • そのうち上のコンテナにあうようにしてくれそうだけど、 今は深追いしない
  • 画像はhttp 経由でもいいけど、パッケージに含めてキャッシュするほうがよさそう?

Sequence : 複数のデータセットをlist表示するもの

  • data バインディングを、 dataの配列定義とバインドして各リスト要素に自動で設定できる
  • dataバインディングを data プロパティで利用できる。以下の感じになる
    バインド → “data” : “${listdata}”
    利用 → ${data.各プロパティ名}

Pager:連続するデータを時系列やページ送りで表示

  • ユーザーはスワイプ操作でページ送りができる
  • navigation : ページ移動処理の設定。ループや、次送り、なしなど
  • navigationをなしにして、ExecuteCommandsで明示的にページ送りすることでアニメーションっぽいこともできる

その他コンポーネント

  • Text : テキスト
  • Frame : 枠をつける。ボタンとか
  • ScrollView : スクロール領域化
  • Video : ビデオプレイヤー

タッチイベント処理の実装

上記Componentで挙げた「TouchWrapper」を使う。

TouchWrapperにタッチしたときの requestEnvelope.requestの中身の例が以下。

{
  type: 'Alexa.Presentation.APL.UserEvent',
  requestId: 'amzn1.echo-api.request.xxxxxx',
  timestamp: '2019-02-20T01: 43: 40Z',
  locale: 'ja-JP',
  arguments: [
    '${actionType}'
  ],
  components: {},
  source: {
    type: 'TouchWrapper',
    handler: 'Press',
    id: 'SelectActionItem',
    value: false
  },
  token: ''
}
  • sourceに使用したコンポーネントと、handlerなどが入る。
  • sourceのhandlerは実機だと”Press”、Developper Consoleだと”onPress”となるのでまだイマイチ不確定

canHandleで以下のような感じで判定するとタッチによるイベント処理の判定が可能。

canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return request.type === 'Alexa.Presentation.APL.UserEvent' request.source.type === 'TouchWrapper' && request.arguments[0];
},
handle(handlerInput) {
    // タッチによるイベント処理...
    const touchType = request.arguments[0]
    ...
}

Commandsで様々な操作を行う

  • APLコマンド
  • ExecuteCommands 命令(ディレクティブ) でAPLに関する操作ができる

Sequence(リスト)の指定インデックスへスクロールするコマンド例

builder.addDirective({
    type : 'Alexa.Presentation.APL.ExecuteCommands',
    commands: [
        {
            type: "ScrollToIndex",
            componentId: "myList",
            index: 2,
            align: "center"
        }
    ]
})

できること

  • APL標準コマンド
  • Sequenceのスクロール
  • Pagerのページ送り
  • 音声コンテンツ再生
  • Sequentialで一連のコマンドを連続実行
  • Parallelで一連のコマンドを並列実行

SendEventもCommandの扱いだが、ExecuteCommandsでは実行できず、Alexa側でハンドルする他のとは異なる部類のコマンド扱い。

SequentialコマンドとPagerでアニメーション

簡易的なアニメーションっぽい演出を見せたいときに、現状よく使われるのがPagerコンポーネントに、SequentialコマンドでSetPageコマンドを連続実行する方法。

つまり、delayを入れながらPage送りすることで、パラパラ漫画のごとくアニメーション化。
Spriteみたく統一的な管理方法があるわけでもないし、画像ロードタイミングも制御できないし、実際にはアニメーションするにはかなり厳しい環境。

タッチ効果等も、CSSの擬似クラス(:active, :hover)的なものがTouchWrapperなりに指定できないといちいち実装はかなり厳しい印象。

importでパッケージを再利用

独自パッケージ読み込み

  • sourceにURL指定すれば使える
  • json同じパッケージ内のファイルパス(./TestLayout.jsonとか)で使いたかったけどそれはできなかった
    • datasourcesはクライアントで展開されてパッケージ内のパスは使えない的な?
  • Alexaシミュレータ上ではなぜか使えなかった(2019年2月)

今回、開発中シミュレータ(Developper Console)で使えなかったので、パッケージごとのファイル分割があまり有意義に使えなかったが、ちゃんと使えれば、大きくレイアウトが変化するときでもPagerでの遷移にまとめられそう。
Pager遷移に慣れば、演出的にも、画像のロード的にも良さそう。

デバッグポイント

  • 画面サイズごとの対応
    • LayoutレベルでViewportのwhen分岐すると、レイアウト変更時に両方に反映するのを忘れがち

実装時のノウハウ・注意点

  • レイアウトとdata設定のベストプラクティス
    • 共通で使うdata ソース定義は外から渡せるほうが共通化できていい
    • 一部だけコードから渡すってのが今の所むずかしそう
    • そのページ固有のリソースは layoutの中に直接書いたほうがわかりやすい。またはresources 定義を使う
  • Echo ShowはAlexa Headerを使うと勝手に戻るボタンができるがEcho Spotにはない
    • Echo Spotは実は左スワイプが「戻る」動作になっているが、ユーザーはほぼ知らないだろう
    • 独自ヘッダーを使って戻るボタンを実装してあげる
  • Echo Show時はAlexaHeaderは 18vhくらいも専有してしまう(Footerも同様)
    • しかも戻るボタン等があるわけじゃなく使いやすくもない
    • 独自のヘッダーを作るほうが現状ベター
  • コンテナなどのItemの隙間を空ける spacing は頭に入るのみ。後ろは入れられない
  • 親コンテナ(Box)に合わせた柔軟な配置(Flexbox) は全体的にまだ洗練されてない感じ。
    • Sequence を Headerの下に、100%で配置して 勝手にいいサイズになるとか できない。
    • なので、絶対量の目安が必要

セッション管理

ResponseBuilderのwithShouldEndSession設定がnullの場合、Directiveへの設定状況によりセッション維持されるかが変化する。
つまりデバイスにより変化したりするので、基本的に明示的にwithShouldEndSessionを毎回設定するのがよい。

shouldEndSessionが指定されていないかこの値にnullが設定されており、Display.RenderTemplate、Alexa.Presentation.APL.RenderDocument、またはAlexa.Presentation.APL.ExecuteCommandsディレクティブがアクティブな場合、セッションは開かれたままになり、画面付きデバイスは音声応答を行いません。ユーザーがウェイクワードとコマンドを言っても、発話はスキルのコンテキストで認識されます。

shouldEndSessionの動作を明確に制御するため、セッションを終了する場合はtrue、セッションを継続する場合はfalseにこのアトリビュートを設定してください。
Displayインターフェースのリファレンス

開発環境

どうしても階層が深いjsonを扱うことになる。
以下のエディタ機能(ショートカット)あたりは覚えておいたほうがいい。

  • フォーマッター(VSCodeなら Shift+Alt+F)
  • フォールディング(Vimなら zc, zr等)

コメント

  1. […] Alexaスキル開発メモ – APL(Alexa Presentation Language)を噛み砕く編 […]

タイトルとURLをコピーしました