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

【Android】クリック可能なToastをつくる(実装編)

Android snippet

Toastを作りなおそう。

androidのソースのライセンスがApache License 2.0じゃなかったら詰んでましたね。

とりあえずソースをコピペする

継承してどうにかなるものでもありません。ToastのソースをコピペするなりDLするなりして引っ張ってきて適当なプロジェクトにコピーしましょう。

そのままではビルドは通りません。当たり前です。通らない理由はおおよそ以下の三つだと思います。

  • com.android.internal.Rが解決できない
  • INotificationManagerとITransientNotificationとServiceManagerが解決できない
  • targetSdkVersionが低すぎて見つからないAPIがある

一番最後のはどうにもならないので、もっと低いバージョンのToastを引っ張ってきましょう。低いバージョンだとWindowManagerのインスタンスをWindowManagerImplと言う具象クラスで取得しようとしていますが、普通に(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)してやればいいです。

(この手の内部クラスでImplと末尾についているものは大体バージョン間の後方互換性を吸収するためなのであまり気にする必要はない)

面倒なのはandroidのFramework内部で所持している諸々です。こいつらをどうにかしていきます。

com.android.internalのリソースを取得する

Resources#getSystem()で取得できるResourcesに対してgetIdentifier (String name, String defType, String defPackage)メソッドを呼び出すことで取得できます。(参考

そんなに量はないのでささっと書き直せると思います。ただ、リソースの取得は結構重いので、staticなメンバでキャッシュしておいたほうがいいかもしれません。

地味に気をつけなくてはならないのがTNのコンストラクタです。

TN() {
    // XXX This should be changed to use a Dialog, with a Theme.Toast
    // defined that sets up the layout params appropriately.
    final WindowManager.LayoutParams params = mParams;
    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    params.format = PixelFormat.TRANSLUCENT;
    params.windowAnimations = com.android.internal.R.style.Animation_Toast;
    params.type = WindowManager.LayoutParams.TYPE_TOAST;
    params.setTitle("Toast");
    params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}

ここでToastの表示するアニメーションを設定しているのですが、実際のnameはAnimation_ToastではなくAnimation.Toastです。ソースをgrepしてみるとわかる

なので、先ほど紹介した方法だとこんな感じになります。

int resId = Resources.getSystem().gerIdentifier("Animation.Toast", "style", "android");

INotificationManagerとITransientNotificationをとってくる

これらはAIDLから自動生成されるクラスです。

android.app内にいるみたいなのでINotificationManagerITransientNotificationもそのままとってきてしまいましょう。

ITransientNotificationは作りが死ぬほど単純なので問題ないんですが、INotificationManagerは更に他の内部インターフェース(INotificationListener)を参照しているのでそのままではビルドが通りません。

極端な話、enqueueToastとcancelToastがあればToastは表示出来ます。他のものは削除してしまいましょう。

コンパイルエラーがなくなりプロジェクトのgenフォルダに各インターフェースのjavaファイルが生成されればOKです。後はそれらをToastクラスでimportするだけです。

ServiceManager

これはコピペで持ってくるのはしんどいです。先人がリフレクションで無理矢理ガメてくるものを公開してくださっているので、ありがたく使わせていただきましょう。

Toast#getService()を以下のように書き換えます。

static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }

        sService =
            (INotificationManager) ServiceLocator.getServiceStub("notification", "android.app.INotificationManager$Stub");
        return sService;
    }

INotificationManager自体をsServiceでキャッシュしているので、リフレクション側のキャッシュはあんまり気にしなくてもいいと思います。

Toastにクリックイベントを渡せるようにする

さてさて仕上げです。前回「これじゃね?」と思ったWindowManager.LayoutParamsのパラメータを書き換えていきます。

TN(int windowAnimationsId) {
    // XXX This should be changed to use a Dialog, with a Theme.Toast
    // defined that sets up the layout params appropriately.
    final WindowManager.LayoutParams params = mParams;
    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    params.format = PixelFormat.TRANSLUCENT;
    params.windowAnimations = windowAnimationsId;
    //params.type = WindowManager.LayoutParams.TYPE_TOAST;
    params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
    params.setTitle("Toast");
    params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            //| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}

変更点は二つです。

  1. WindowManager.LayoutParams.FLAG_NOT_TOUCHABLEコメントアウトする
  2. typeをWindowManager.LayoutParams.TYPE_SYSTEM_ALERTに変更する

1はいいでしょう。タッチイベントを受け取ってもらえるようにすることでクリックイベントまで流れるようになります。FLAG_NOT_FOCUSABLEはあってもなくても変わりませんでした。

TYPE_SYSTEM_ALERTに変更した理由ですが、TYPE_TOASTにはどう頑張ってもタッチイベントを渡せません。

(実際にはバージョンによって渡せるか渡せないか変わるらしい。)

これはもう…どうしようもなさそうなので諦めます。

TYPE_SYSTEM_ALERTを使用するには権限が必要になります。AndroidManifestに以下のパーミッションを追加しておきましょう。

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

まとめ

後は前回記述したように、OnClickListnerをセットしたViewを表示するようにすればOKです。Viewの内部にOnClickListenerを持ったViewを持たせても普通にいけるはずです。

諸々をまとめたソースをGitHubにあげようと思ってはいるんですが、まだライセンス関連のあれこれとかjavaDocとか書ききれていないので後日にします。申し訳ないです。

[2014/06/08追記]

GitHubにあげました。

参考