Spring Boot で Thymeleaf テンプレートを使う

概要

Hello World をブラウザで表示できることを確認できたので、 HTML 文書をブラウザで表示できるようにします。 HTML を表示する際には静的な HTML としてではなく、 動的に生成した HTML を表示するようにするため、 テンプレートエンジンを使用します。 テンプレートエンジンには Thymeleaf (タイムリーフ) を使います。

確認環境

  • IntelliJ IDEA COMMUNITY 2016.2
  • Spring Boot 1.4.0

参考情報

Thymeleaf の使い方。日本語。

Thymeleaf と Spring の連携。英語。

手順

  1. build.gradle に org.springframework.boot:spring-boot-starter-thymeleaf を追加
  2. テンプレートを作成
  3. Controller を作成
  4. ブラウザで確認

Gradle の依存設定に Thymeleaf を追加する。下記は build.gradle ファイルの一部。

dependencies {
    ...
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    ...
}

テンプレートを作成する。形式は HTML であり拡張子は html として作成する。 今回は、user-request-list.html を src/main/resources/templates/ に以下の内容で作成した。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>機能一覧</title>
</head>
<body>

<table>
    <thead>
    <tr>
        <th>ID</th>
        <th>機能概要</th>
        <th>人気</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>FIX_1</td>
        <td>A機能の不具合を修正して欲しい</td>
        <td>50</td>
    </tr>
    <tr>
        <td>NEW_1</td>
        <td>B機能を追加して欲しい</td>
        <td>50</td>
    </tr>
    </tbody>
</table>

</body>
</html>

Controller を作成する。 @Controller アノテーションを付与したクラスと @RequestMapping を付与したメソッドを作成する。 今回は、UserRequestController.kt を src/main/kotlin/…/web に以下の内容で作成した。

package org.anyspirit.webapp.ask.request.web

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping

@Controller
class UserRequestController {

    @RequestMapping("/user-request-list")
    fun showList(): String = "user-request-list"
}

@RequestMapping の /user-request-list は URL の http://localhost:8080/user-request-list と対応する。 showList メソッドの戻り値の user-request-list はテンプレートの user-request-list.html と対応する。

ブラウザで http://localhost:8080/user-request-list を開くとエラーとなる。 ブラウザに表示されたエラーの内容は Exception parsing document: template=“user-request-list”, line 6 - column 3 となっており、 Terminal を確認すると下記のエラーが表示されている。

2016-08-29 17:30:13.343 ERROR 7100 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: Exception parsing document: template="user-request-list", line 6 - column 3] with root cause

org.xml.sax.SAXParseException: The element type "meta" must be terminated by the matching end-tag "</meta>".

meta タグを閉じなければならないとのこと。/ を追加して閉じタグがないことを明示したところ、エラーは表示されずに HTML が表示された。

    <meta charset="UTF-8" />

補足

meta タグを閉じなければならないことから、設定が XHTML になっていることを疑った。 適用されている auto-configuration を見るには debug スイッチを付与して起動するとある (Spring Boot Reference Guide)。 bootRun に --debug を付与して下記のように起動すると Gradle の DEBUG メッセージは表示されるが、 アプリケーションの DEBUG メッセージは表示されない。

$ ./gradlew --debug bootRun

アプリケーションの DEBUG メッセージを表示するには、build.gradle に下記を追加する方法がある。

bootRun {
    jvmArgs = ['-Ddebug']
}

欲しい情報はテンプレートの設定であり、これらの DEBUG メッセージには見当たらない。 テンプートの設定は Bean (Spring が生成を管理するオブジェクト) で行われると Tutorial: Thymeleaf + Spring に記載されている。 Bean の情報を取得してテンプレートのデフォルト設定を確認する。

Bean の情報を取得するために、main 関数に処理を加える。 SPRING INITIALIZER で作成したプロジェクトでは、ルートパッケージに XXXApplication.kt が作成され、main 関数が定義される。 下記のように記述することで Bean 名の一覧を取得できる。

fun main(args: Array<String>) {
    val ctx = SpringApplication.run(AskRequestApplication::class.java, *args)

    ctx.beanDefinitionNames.sorted().map {
        println(it)
    }
}

bootRun で起動すると Terminal に Bean 名の一覧が表示される。 template に関する記述を探すと defaultTemplateResolver が見つかる。

defaultTemplateResolver の詳細を確認する。 main 関数に下記の処理を加えて Terminal の表示を確認すると、 SpringResourceTemplateResolver クラスのインスタンスが取得されている。

fun main(args: Array<String>) {
    val ctx = SpringApplication.run(AskRequestApplication::class.java, *args)
    
    ...

    val bean = ctx.getBean("defaultTemplateResolver")
    println(bean.toString())
}

クラスを特定できたので、インスタンスが保持している情報をメソッドで取り出す。 main 関数に下記の処理を加えて Terminal の表示を確認する。

fun main(args: Array<String>) {
    val ctx = SpringApplication.run(AskRequestApplication::class.java, *args)

    ...

    val bean = ctx.getBean("defaultTemplateResolver") as SpringResourceTemplateResolver
    bean.initialize()
    println(bean.prefix)
    println(bean.suffix)
    println(bean.templateMode)
    println(bean.isCacheable)
}

defaultTemplateResolver の設定は下記の通り。

  • prefix : classpath:/templates/
  • suffix : .html
  • templateMode : HTML5
  • isCacheable : false

templateMode は HTML5 であり XHTML ではない。 Tutorial: Using Thymeleaf (ja) によると、 HTML5 モードは整形式 XML と書かれている。 もし、整形式 XML を使いたくなければ LEGACYHTML5 モードを使用する。

LEGACYHTML5 モードに設定を変更したい場合には、templateResolver の Bean 設定を行う。 Bean の設定は、@Configuration を付与したクラスで行う。 Tutorial: Thymeleaf + Spring では XML で記載されているが Spring Boot の慣習に則りアノテーションを使う。 SPRING INITIALIZER で作成したプロジェクトでは、ルートパッケージに XXXApplication.kt が作成され、@SpringBootApplication が付与されている。 @SpringBootApplication は @Configuration, @EnableAutoConfiguration, @ComponentScan をまとめて付与する。 つまり、XXXApplication.kt で Bean の設定を行えば良いということ。

下記は templateResolver の Bean 設定。

@SpringBootApplication
open class AskRequestApplication {

    @Bean
    open fun templateResolver() = SpringResourceTemplateResolver().apply {
        prefix = "classpath:/templates/"
        suffix = ".html"
        templateMode = "LEGACYHTML5"
        isCacheable = false // TODO: make it true before release
    }
}

ブラウザで確認をすると下記のようなエラーが表示される。

Cannot perform conversion to XML from legacy HTML: The nekoHTML library is not in classpath. nekoHTML 1.9.15 or newer is required for processing templates in “LEGACYHTML5” mode http://nekohtml.sourceforge.net. Maven spec: “net.sourceforge.nekohtml::nekohtml::1.9.15”. IMPORTANT: DO NOT use versions of nekoHTML older than 1.9.15.

LEGACYHTML5 を使うには nekoHTML が必要とのことなので build.gradle に下記を追加。

dependencies {
    ...
    compile('net.sourceforge.nekohtml:nekohtml:1.9.15') // for LEGACY HTML5 template
    ...
}

以上の設定を加えることで閉じタグを省略できるようになる。

Kotlin 補足

下記の Kotlin コードと Java コードは同じ。

    open fun templateResolver() = SpringResourceTemplateResolver().apply {
        prefix = "classpath:/templates/"
        suffix = ".html"
        templateMode = "LEGACYHTML5"
        isCacheable = false // TODO: make it true before release
    }
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode("LEGACYHTML5");
        resolver.setIsCacheable(false); // TODO: make it true before release
        return resolver;
    }

Kotlin の書籍

はじめての Sprint Boot in Kotlin

概要

Spring Boot での開発環境を構築する手順を説明します。 インストールから、ブラウザでページに Hello World を表示させるまでの手順です。 開発言語には Kotlin、ビルドツールには Gradle を使います。

確認環境

  • IntelliJ IDEA COMMUNITY 2016.2
  • Spring Boot 1.4.0

参考情報

背景

Kotlin は IntelliJ IDEA を開発している JetBrains 社によって開発された言語です。 2016 年にバージョン 1.0 がリリースされました。 Kotlin コンパイラJava バイトコードを生成するため、Kotlin で書かれたプログラムは Java VM で動かすことができます。 Java 互換をうたっており、Java のライブラリを使用することも、Java から呼び出すこともできます。

IntelliJ IDEA は Android Studio のベースになっている IDE です。 Android Studio では Gradle が標準のビルドツールになっており、 Android Studio + Gradle + Kotlin による開発環境を容易に構築することができます。

Web 開発といえば Eclipse の使用が定石とは思いますが、 Kotlin を使うことを前提とするため、 Android Studio + Gradle + Kotiln と親和性の高い環境である IntelliJ IDEA + Gradle + Kotlin を使います。

手順

以降は下記の順番に説明する。

  1. SPRING INITIALIZER を使って、Gradle プロジェクトを作成する
  2. IntelliJ IDEA に Gradle プロジェクトをインポートする
  3. ソースコードを編集する
  4. サーバーを起動してブラウザで確認する
  5. Automatic-restart を試す
  6. LiveReload を試す

SPRING INITIALIZER のページで、Gradle プロジェクトを作成する。 Generate Project ボタンの下にある、Switch to the full version と書かれたリンクをクリックする。 各項目は下記のように選択、もしくは入力した。

  • Generate a Gradle Project with Spring Boot 1.4.0
  • Group, Artifact, Name, Description, Package Name
    • Package Name は Group + Artifact と同じに
    • Artifact と Name は同じに
    • Description は適当に
  • Packaging : War
    • Jar にすると Tomcat を内包した実行可能な jar ができる(はず)
  • Java Version : 1.8
  • Language : Kotlin
  • Selected Dependenccies : DevTools, Web
    • DevTools : Spring Boot 開発ツール
    • Web : Tomcat と Spring MVC によるフルスタックウェブ開発

Generate Project ボタンをクリックすると ZIP ファイルがダウンロードされる。

IntelliJ IDEA にプロジェクトをインポートする。 ダンロードした ZIP ファイルを IdeaProjects ディレクトリに展開後、IntelliJ IDEA を起動。 Import Project を選択し、展開したディレクトリを選択。 Import project from external models で Gradle を選択し、デフォルト設定で Finish。

Hello World を表示するだけの RestController (HelloController.kt) を追加する。

package org.anyspirit.webapp.feature.web

import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController {

    @RequestMapping("/")
    fun index(): String = "Hello World"
}

Tomcat を起動して、Chrome で確認する。 Tomcat を起動するためには Gradle で bootRun タスクを実行する。 IDE の Gradle ツールから起動することもできるが、後で説明する Automatic-restart が期待通りに動作しないため Terminal から起動する。

$ ./gradlew bootRun

http://localhost:8080 をブラウザで開くと Hello World が表示される。

HelloController.kt の Hello World を他の文字に変更した時に自動的に反映されるようにしたい。 自動的にコンパイルするよう IDE の設定を変更する。 設定画面で Build, Execution, Deployment > Compiler > Make project automatically を ON にする。

Terminal で bootRun タスクを実行したままで、HelloController.kt を Hello World から Hello Beautiful World に変更する。 自動的にコンパイルされ、Automatic-restart が行われて Terminal の表示が更新される。 (Automatic-restart は DevTools の機能。) ブラウザの表示を更新すると Hello Beautiful World が表示される。

ブラウザの表示更新を自動化する。 http://livereload.com/extensions/ から Chrome 拡張のリンクを辿り、LiveReload 拡張をインストールする。 http://localhost:8080 を表示して、LiveRecorad 拡張のアイコンをクリックすることで LiveReload を有効にする。

Terminal で bootRun タスクを実行したままで、HelloController.kt を Hello Beautiful World から Hello Beautifull World!!! に変更する。 ブラウザの表示が自動的に更新され Hello Beautiful World!!! が表示される。

更新に時間がかかりエラーページが一時的に表示されるときがある。 どれだけ待っても更新されないような時は Spring Boot Reference Documentation を参考にして Spring Loaded を試してみると良いかもしれない。

アプリ内課金をする

概要

Android でアプリ内課金を行う方法についての要約です。詳しい内容は参考情報のページに書かれているので、全体を大まかに説明します。

確認環境

参考情報

公式の情報。

公式の情報の一部を翻訳。

注意点など。

要約

アプリ内課金をするにあたって、行うこと。

  • Google Play Developer Console (以下、Developer Console) に登録し、販売アカウントを作る
  • アプリに AIDL ファイルを追加する
  • AndroidManifest.xml<uses-permission android:name="com.android.vending.BILLING" /> を追加する
  • Developer Console で APK をアップロードする
  • Developer Console でアプリ内アイテムを追加する
  • Billing API を使って購入状況を取得する処理、購入する処理、購入したアイテムを消費する処理をコーディングする

販売アカウントを作成する際には、正式な住所の登録が求められる。 使いながら調べた結果から考えて、販売アカウントの住所は購入者にのみ提示されると考えて良さそう。 販売アカウントだけでなく Developer Console アカウントでも住所の登録が求められる。 こちらの住所は Google Play Store で公表される。 アプリやアプリ内アイテムを販売する場合には、連絡可能な現在の有効な住所を登録しないとアカウントまたはアプリの販売が停止される場合があるとのこと。 商店で製品を購入する際にパッケージには生産者の住所が記載されているのと同様、アプリにも住所を明記する必要があるということか。

AIDL ファイルは、Sample (Trivial Drive) にある IInAppBillingService.aidl ファイルをコピーする。 コピーする先は Sample と同じ、src/main/aidl/com/android/vending/billing ディレクトリ。 Sample を参照するには、Import an Android code sample を選択し、続けて Trivial Drive を選択する。

AIDL ファイルを APK に含んだ状態で、APK をアップロードするとアプリ内アイテムを追加できるようになる。 (確認してないので、間違えてたら指摘してください ^^;)

Billing API を使った処理フローは Use Google Play's billing system with AIDL  |  Android Developers を参照。 購入処理のフローが図示されている。下記は Billing API を使う処理とその前後の処理を含めたフロー。

  1. bindService
  2. isBillingSupported
    • アプリが実行されている環境でアプリ内課金がサポートされていることを確認
  3. getPurchases
    • 購入済みアイテムを取得
    • 1 回の呼び出しで 700 件までしか取得されず、700 件を超える場合には INAPP_CONTINUATION_TOKEN キーで設定された値を使って続きを取得する
  4. getSkuDetails
  5. getBuyIntent
    • 購入処理を依頼するための Intent を取得
    • 第 5 引数の developerPayload に渡した文字列は、購入処理完了後の onActivityResult にそのまま渡される
    • developerPayload は購入要求を識別するために使われる (Fight fraud and abuse  |  Android Developers)
    • ベストプラクティスには、消費されないアイテムではユーザーを特定する文字列(メールアドレスは変更されるので使わない)、消費されるアイテムはランダムの文字列を設定すると記載されている
  6. startIntentSenderForResult
    • getBuyIntent で取得した Intent を使用して、Google Play に購入処理を依頼
    • 自アプリがネットワークを介して通信するのではなく、Google Play がサーバーとの通信や購入処理を行う
    • 購入情報のキャッシュは Google Play が行う
  7. onActivityResult
    • 購入処理が完了(購入、もしくはキャンセル)した時に呼ばれる
    • developerPayload の値を検証する (どのような問題に対応するため?)

API をサポートしていない場合に対応したり、セキュリティを高めたり、700 件を超える場合は続きを取得するようにしたりと複雑な処理が増えてくる。 これらを自ら実装する手間を省きたい時には、Sample (Trivial Drive) の util パッケージのソースコードをコピーして使うと良い。 Activity では IabHelper クラスを使用する。 IabHelper クラスのコンストラクタの第 2 引数には、base64EncodedPublicKey (以下、公開鍵) を渡す。 公開鍵には Developer Console の「サービスと API」メニューから辿った先にある「このアプリのライセンスキー」を使う。 ただし、公開鍵はそのままソースコードに書かず変換した文字列を書き、プログラム中で変換する(Fight fraud and abuse  |  Android Developers)。 公開鍵は秘密情報ではないが、他の公開鍵に置き換えられることを防ぐために難読化する。

テスト方法

テストのために課金をし続けていては、手数料をとられて大損する。 テスト方法としては 2 種類の方法が用意されている。

  • Developer Console と連携して、販売アイテムを購入する方法 (課金はされない)
  • Developer Console と連携せずに、テスト用アイテムを購入する方法

開発の早期では、Developer Console と連携しない方法でテストする。 テスト用アイテムとして下記の Product ID (SKU) が用意されている (Google Play 請求サービスをテストする  |  Android デベロッパー  |  Android Developers)。これら Product ID をソースコード中で使用して各種の処理をテストする。

  • android.test.purchased : 購入成功の扱いとなる
  • android.test.canceled : 購入キャンセルの扱いとなる
  • android.test.refunded : 払い戻しの扱いとなる
  • android.test.item_unavailable : 購入不可の扱いとなる

テスト用アイテムの Product ID を用いて購入処理のテストを行った後に、Developer Console に販売アイテムを登録してテストをする。 ソースコード中で使用されている Product ID をテスト用アイテムの Product ID から Developer Console で登録した Product ID に変更する。

Developer Console の「設定」メニューから「アカウントの詳細」を開くと「テスト用のアクセス権がある Gmail アカウント」項目がある。 この項目に指定された Gmail アカウントでアイテムの購入を行うと「これはテスト用の注文です。課金は発生しません。」のようにメッセージが表示され、 購入しても課金されない。

未着手のことがら

下記のことがらについては未着手なので、おいおい調べる。

  • アイテムの消費
  • 定期購読
  • 払い戻し

SDカードにファイルを作成して書き込む

概要

Android で SD カードにデータを書き込む方法です。Android 6.0 以降では、権限設定に変更があります。

確認環境

参考情報

公式の情報。Android 6.0 (API level 23) 以降での権限要求の方法が説明されている。

対処方法

AndroidManifest.xmluses-permission を追加することは Android 5.1 以前までと同じ。インストール時、ユーザーに権限を要求する。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Android 6.0 以降では、実行時にも権限を要求する。 現在の権限は、設定アプリから確認できる。 SOV32 の場合は、設定アプリを起動したのち アプリ > アプリの設定(歯車アイコン) > アプリの権限 > ストレージ と辿れば確認できる。

これら設定のON/OFFは checkSelfPermission メソッドを呼ぶことで PERMISSION_GRANTED/PERMISSION_DENIED として得られる。 つまり、checkSelfPermission メソッドは、 アプリに権限が与えられている時に PERMISSION_GRANTED を返し、 与えられていない時に PERMISSION_DENIED を返す。 アプリに権限が与えられていない場合には、requestPermissions メソッドにより権限を要求する。 requestPermissions メソッドを実行するとシステムダイアログが表示され、許可/拒否の選択をユーザーに要求する。

ユーザーが許可/拒否を選択すると onRequestPermissionsResult メソッドが呼ばれる。SD カードのディレクトリパスは getExternalStorageDirectory メソッドにより得られる。

補足

Android 6.0 (API level 23) 以降では、システム権限が 2 つに分類される。

  • Normal 権限 : ユーザーのプライバシーに直接的なリスクとならない。
  • Dangerous 権限 : ユーザーの機密情報への読み書きを許すことになる。ユーザーが許可を明示する必要がある。

バイスAndroid Version とアプリの Target SDK Version によって、権限要求の振る舞いが異なる。

  • Android Version ≦ 5.1 もしくは Target SDK Version ≦ 22 ならば、アプリ実行時に権限は自動的に与えられる。
  • Android Version ≧ 6.0 かつ Target SDK Version ≧ 23 ならば、アプリ実行時に Dangerous 権限を要求し、ユーザーは許可/拒否を選択する。

AndroidManifest.xml にて要求する権限を指定することは同じである。

また、本投稿のサンプルコードでは省略しているが、権限を要求する理由をユーザーに伝える事が推奨される。 ユーザーとしては、説明なく許可/拒否の選択を求められても判断がつかない。 しかし、頻繁に確認を求められるとユーザーは煩わしく感じ、アプリの使用をやめてしまう恐れがある。 このような場合のために shouldShowRequestPermissionRationale メソッドが用意されている。 shouldShowRequestPermissionRationale が true を返した場合のみ、権限を要求する理由をユーザーに提示するように実装すれば良い。 ユーザーへの提示は何らかの形で自ら実装する必要がある。

メモリリークを解決する

概要

Android で発生しているメモリリーク (Memory Leak) の解決方法です。RecyclerView を使用していて発生するメモリリークを解決した例です。

確認環境

参考情報

SQUARE が公開している LeakCanary を使って解析する方法を紹介している。

LeakCanary のサイト。

Android Studio の Memory Monitor の解説。

解析

まずは、LeakCanary を試す。

実機でアプリを起動して、そのまま終了させてみる。すると Dumping memory, app will freeze. なんてメッセージが出た。 メモリをダンプ中だからアプリがフリーズするよ、ってことかな。 Leaks というアプリがインストールされているので、起動してみると何もない。 Leaks アプリを終了して、再び表示させてみると今度はリーク情報がある。 表示されるまでには時間の経過が必要な様子。

Android Studio の Memory Monitor でも確認してみる。

Initiate GC

を実行した後に、Dump Java Heap

を実行し、次に Analyzer Tasks

を実行することで、Leak Activity は確認できる。

Reference Tree には選択したインスタンス参照元(どこから参照されているか?)が表示されている。 Depth 0 まで辿っていくと、GC root が下記アイコンで示されている。

下記のようなオブジェクトが GC root になる。

  • references on the stack
  • Java Native Interface (JNI) native objects and memory
  • static variables and functions
  • threads and objects that can be referenced
  • classes loaded by the bootstrap loader
  • finalizers and unfinalized objects
  • busy monitor objects

Inspect your app's memory usage with Memory Profiler

対処方法

ライブラリのバグらしい。

ログイン - Google アカウント

LeakCanary にも丁寧に上記 URL がメッセージに表示され、解決方法として下記の URL が提示されている。

"Fix" for InputMethodManager leaking the last focused view: https://code.google.com/p/android/issues/detail?id=171190 · GitHub

これを元にして試してみたところ、メモリリークはなくなった。下記は Kotlin で書き換えたコード。

ViewPagerで右スクロールを禁止して、左スクロールだけにする

概要

ViewPager を使用して複数の Fragment を横スクロールで切り替える時、右スクロールは禁止して、左スクロール(戻る)のみを有効にする方法です。

確認環境

  • Compile SDK Version 22
  • Build Tools Version 22.0.1
  • 実行環境 Sony SOV32 Android 6.0, API 23

参考情報

onTouchEvent メソッドと onInterceptTouchEvent メソッドをオーバーライドすることで実現している。

At the end the solution was in the adapter. I changed the count of the PagerAdapter and this way blocks the user from passing the max page:

PagerAdapter の getCount メソッドで返す値を変えることにより実現している。

対処方法

両方とも試してみた。

  • onTouchEvent, onInterceptTouchEvent メソッドをオーバーライドする方法

スワイプでは、右に全くスクロールできない。(左スクロールはスワイプ、右スクロールはボタンで行っている。)

  • PagerAdapter の getCount メソッドで返す値を変える方法

スワイプで右にスクロールしようとすると、スクロールできるかに見えるが跳ね返る。(左スクロールはスワイプ、右スクロールはボタンで行っている。)

補足

onTouchEvent, onInterceptTouchEvent の違いがわからないので、ソースコードを読んでみた。 要約すると次のようになる。

  • onInterceptTouchEvent が true を返したら自身の dispatchTouchEvent を行い、false を返したら子 View の dispatchTouchEvent を行う。
  • ACTION_MOVE の時は、ACTION_DOWN の時にイベントを handle した子 View の dispatchTouchEvent を行う。
    • この際、onInterceptTouchEvent が true ならば、ACTION_CANCEL として子 View の dispatchTouchEvent を行う。

以下、要点のみ疑似コードで記載。

ViewGroup::dispatchTouchEvent は下記のように振る舞いっていると思われる。

ACTION_DOWN の時:
    intercepted = onInterceptTouchEvent
    intercepted が false の時:
        全ての childView に対して: ...(A)
            handled = childView.dispatchTouchEvent
            handled が true の時:
                TouchTarget の先頭に childView を追加して、(A) 終了
    TouchTarget がない時:
        handled = View::dispatchTouchEvent
    handled を返して、終了

ACTION_MOVE の時:
    TouchTarget がない時:
        handled = View::dispatchTouchEvent
    TouchTarget がある時:
        intercepted = onInterceptTouchEvent
        intercepted が true の時:
            event.action を ACTION_CANCEL として、全ての TouchTarget(childView) に対して:
                handled = childView.dispatchTouchEvent
                childView を TouchTarget から取り除く
        intercepted が false の時:
            全ての TouchTarget(childView) に対して:
                handled = childView.dispatchTouchEvent
    いずれかの handled が true ならば true を返して、終了

ACTION_HOVER_MOVE の時:
    TouchTarget がある時:
        intercepted = onInterceptTouchEvent
    TouchTarget がない時:
        intercepted = false
    intercepted が false の時:
        全ての childView に対して: ...(A)
            TouchView に childView が含まれているなら、(A) 終了
            handled = childView.dispatchTouchEvent
            handled が true の時:
                TouchTarget の先頭に childView を追加して、(A) 終了
    TouchTarget がない時:
        handled = View::dispatchTouchEvent
    TouchTarget がある時:
        intercepted が true の時:
            event.action を ACTION_CANCEL として、全ての TouchTarget(childView) に対して:
                handled = childView.dispatchTouchEvent
                childView を TouchTarget から取り除く
        intercepted が false の時:
            全ての TouchTarget(childView) に対して:
                handled = childView.dispatchTouchEvent
    いずれかの handled が true ならば true を返して、終了

View::dispatchTouchEvent は下記のように振る舞いっていると思われる。

OnTouchListener が登録されている時:
    handled = OnTouchListener::onTouch
handled が false で、TouchDelegate が登録されている時:
    handled = TouchDelegate::onTouchEvent
handled が false の時:
    handled = イベントを処理する
handled を返して、終了

DialogFragment を Back で戻ると InputEventReceiver で警告が出る

概要

DialogFragment を使用した 時に、Back ボタンで DialogFragment を閉じると W/InputEventReceiver: Attempted to finish an input event but the input event receiver has already been disposed. と表示される件の対処方法です。(2016.7.8現在、未解決)

確認環境

  • Compile SDK Version 22
  • Build Tools Version 22.0.1
  • 実行環境 Sony SOV32 Android 6.0, API 23

参考情報

I have this error message in the following scenario: Activity creates a Dialog. The Dialog is assigned a KeyListener. Back Button is pressed, and the Activity+Dialog are closed. Apparently, the Dialog is closed before the "input event" is finished. – Pawel Dec 24 '14 at 16:42

Dialog に KeyListener を登録して、Back ボタンを押下して Activity と Dialog を閉じる。InputEvent が終了する前に Dialog が閉じられると発生している様子。

I have same problem. I have PopupWindow with button (R.id.imageView1). I have callback on that button. I deregister callback when popup is disimissed (popupWindow.setOnDismissListener).

PopupWindow でも同様の問題が発生する様子。DismissListener にて登録されている Listener を削除することによって回避できる様子。

mReceiverPtr == 0 の時 finishInputEvent メソッドで警告をログに出力している。dispose メソッドで mReceiverPtr = 0 としている。

対処方法

Back ボタンの InputEvent が終了する前に DialogFragment が dismiss されるために警告が発生されていると推測される。

どうしたものか…。こういうものと諦めるのが良いか…。