読者です 読者をやめる 読者になる 読者になる

フローティングアプリを作るためのはじめの一歩

概要

Android で他アプリを実行中でも前面に表示されるアプリ(フローティングアプリ、Xperia のスモールアプリのようなもの)を作るため、 他アプリを実行中にビューを表示できることを確認します。 さらに、フローティングアプリの移動とタップが可能であること、他アプリの操作が行えることを確認します。

確認環境

参考情報

解説

Android の画面表示には層 (Layer) がある。 blog.nagopy.com: Androidユーザー向け 「何となくわかる表示レイヤー講座 ~オーバーレイ表示の仕組み~」 を参考に前面の層から順に並べると下記のようになる。 TYPE_* は後述する WIndowManager.LayoutParams (以後 LayoutParams と呼ぶ) のコンストラクタで指定する定数である。

  1. システムオーバーレイ (TYPE_SYSTEM_OVERLAY)
  2. ステータスバー
  3. キーガード(ロック画面)
  4. システムアラート (TYPE_SYSTEM_ALERT)
  5. トースト (TYPE_TOAST)
  6. 着信画面 (TYPE_PHONE)
  7. 通常のアプリ (TYPE_APPLICATION)

通常のアプリよりも前面の層にビューを表示すれば、他アプリを実行中でも表示されるようになる。 前面の層にビューを表示するためには WindowManager の addView メソッドを使う。 addView メソッドには、表示する View オブジェクトと LayoutParams オブジェクトを渡す。 LayoutParams で TYPE_* を指定することで表示する層を指定できる。 TYPE_* は上記以外にも存在する (WindowManager.LayoutParams | Android Developers) が、 全ての TYPE_* を指定できるわけではない。

上位の層にビューを配置すれば如何なる場合にも表示されるようになるが、 TYPE_SYSTEM_OVERLAY ではタッチイベントを捕捉できない。 TYPE_SYSTEM_ALERT であればタッチイベントを捕捉でき、トーストや着信よりも前面に表示される。 トーストの表示を邪魔したくなければ TYPE_PHONE にすればよい。 LayoutParams で MATCH_PARENT を指定するとタッチイベントが通常のアプリに渡らなくなるので WRAP_CONTENT にする。

フローティングアプリの表示位置は LayoutParams で管理する。 LayoutParams の x, y プロパティを設定した上で、WindowManager の updateViewLayout を呼ぶことで表示位置が変更される。

フローティングアプリは、他アプリが実行中に表示される必要があるのでアクティビティではなくサービスとして実装する。 サービスが他アプリの影響で出来る限り終了しないようにしたければ、startForeground と Service.START_STICKY を忘れずに。 サービスが自動的に再起動する際には、intent が null で onStartCommand が呼ばれるので注意が必要である。 詳しくは 落ちないサービスでアプリの起動を監視する - NOSIX 参照。

Android 6.0 からはセキュリティが厳しくなり、ユーザーが他のアプリの上に表示を許可しなければならない。 Settings.canDrawOverlays メソッドにより許可が与えられていることを確認し、許可されていなければ startActivityForResult で許可設定の画面を表示する。 従来どおり android.permission.SYSTEM_ALERT_WINDOW の権限も必要である。

Kotlin 解説

Kotlin に詳しくない方のために、少しだけ Kotlin の補足。

拡張関数と拡張プロパティ

Extensions.kt で使われている

fun クラス名.関数名()

が拡張関数の定義です。 fun Activity.hasOverlayPermission() とすることで Activity クラスに hasOverlayPermission メソッドがあるかのように Activity クラスを使えます。 MainActivity は Activity クラスを継承しており、MainActivity クラスは hasOverlayPermission メソッドを持っているかのように振る舞います。

FloatingButton.kt では

val クラス名.プロパティ名
var クラス名.プロパティ名

として拡張プロパティを定義しています。 拡張関数と同様に振る舞います。 var WindowManager.LayoutParams.position とすることで WindowManager.LayoutParams クラスが position プロパティを持つように振る舞います。

initial = params.position - e.position の params は WindowManager.LayoutParams のインスタンスです。 params.position として参照することで拡張プロパティで定義した position プロパティの get() が呼び出され、 Position(x.toFloat(), y.toFloat()) が実行されます。 x, y は WindowManager.LayoutParams のプロパティです。

params.position = it + e.position としてプロパティに代入した場合には、position プロパティの set(value) が呼び出されます。

プロパティは Java で言うところのフィールド、セッター、ゲッターをひとつにまとめたものです。 今回の拡張プロパティではセッターとゲッターのみを定義し、フィールドは定義していません。 フィールドを定義する場合には、set(value)field 変数を使います。 FloatingButton.kt の visible プロパティではフィールドを定義しています。

run 関数

下記のコードは同じことを行っています。

// Kotlin
button?.run {
    visible = false
}
// Java
if (button != null) {
    button.setVisible(false);
}

?. は null ではないときのみメソッドを実行します。

run 関数はスコープ関数と呼ばれる関数の一種で、拡張関数として定義されています。 Kotlin ではメソッドではなく関数 (function) と呼ぶようです。 Java のメソッドは Kotlin ではメンバー関数 (member function) と呼ぶことがありますが、 本記事ではクラスに属している関数 (member function) をメソッドと呼んでいます。

apply 関数

下記のコードは同じことを行っています。

// Kotlin
button = FloatingButton(windowManager, this).apply {
    visible = true
}
// Java
button = new FloatingButton(windowManager, this);
button.setVisible(true);

apply 関数も run 関数と同様にスコープ関数と呼ばれる関数の一種で、拡張関数として定義されています。

run 関数との異なる点は関数の戻り値です。 run 関数の戻り値はブロック内で最後に評価された値です。 先の例では、visible = false の結果が Unit 型 (Java で言うところの Void のような型) なので、run 関数は Unit オブジェクトを返します。 apply 関数の戻り値はレシーバーオブジェクトです。 例では、FloatingButton オブジェクトに対して apply 関数を呼び出しているため、レシーバーオブジェクトは FloatingButton オブジェクトです。 つまり、apply 関数の戻り値は FloatingButton オブジェクトになります。