GlassPaneを作る(その2)

GlassPaneの要件

配下(子供)のGUIを覆う透明又は半透明な領域(パネル)として描画 ・・(済)
表示されている間は一部の入力以外は受け付けない(ブロックする)
自身の上に予め登録されたGUIを描画でき、そのGUIだけは入力を受け付ける

昨日は半透明な領域の描画を実装したので、残る部分に関しても実装していく。(実装しながら書いている)

重要な機能として表示されている間は配下のビューが操作できないように一切の操作をブロック(阻止)する必要がある。
GlassPaneのスーパクラス、ひいては全てのレイアウトのスーパクラスであるViewGroup抽象クラスにはdispatch〜(イベント名)という名前のメソッドが定義されており、Androidのビュー上で発生したイベントはこのメソッドを辿ることで伝搬していく。

    • Android SDK内部のキーイベントの伝搬順

この階層はAndroid SDKに添付されてきた "Hierarchy Viwer"で見ると解りやすい。

(一部階層を省略しているが)このビュー階層の上から下にイベントは伝搬される。
なのでdispatch〜メソッドをオーバライドして、内部でイベントの伝搬を止めることで入力をブロックする。

    /* キーボード */
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return true;
        //return super.dispatchKeyEvent(event);
    }
    /* トラックボール */
    @Override
    public boolean dispatchTrackballEvent(MotionEvent event) {
        return true;
        //return super.dispatchTrackballEvent(event);
    }
    /* タッチ */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        return true;
        //return super.dispatchTouchEvent(event);
    }

戻り値にtrueを戻すことでイベントは「消費された」と見なされ、以降の子孫階層にイベントは伝搬しない。

この状態でテストしてみたが、GlassPaneの背後(実際には子供)のビューがキーボード、タッチ(有ればトラックボール)に反応しなくなることが確認できる。(エミュレータにはトラックボールは無いが、F6を押下するとトラックボールモード(左上にボールが表示される)に切り替わる(DELキーを押下している間だけトラックボールのモードにすることも可能)

さて、入力をブロックできたのは良いが困ったことが見つかった。

1. システムにとって必ず動作が必要なキー操作までブロックされてしまう
2. ブロック状態から抜ける手段が無い

1.に関して、Homeキーは元々アプリケーション側では無効にできないので良いが、その他の重要なBackキーやMenuキー、ハードウェアを直接制御するキー(フックup/downや音量up/down等)等、機器の最低限の操作を行うために一部の操作だけは有効にしておかなければならない。

これらの制御の方法としてはイベントのキーコードやイベント状態を直接判定すれば良いのだが、KeyEventクラスには便利な状態検査のためのメソッドがあるので、それを使うことでコードをシンプルにできる。

    • 一部のキー操作を有効にしたdispatchKeyEventメソッド
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        /*
        isSystem()では以下のキーコードが有効と見なされる (Android 2.2)、
            KEYCODE_MENU
            KEYCODE_SOFT_RIGHT
            KEYCODE_HOME
            KEYCODE_BACK
            KEYCODE_CALL
            KEYCODE_ENDCALL
            KEYCODE_VOLUME_UP
            KEYCODE_VOLUME_DOWN
            KEYCODE_MUTE
            KEYCODE_POWER
            KEYCODE_HEADSETHOOK
            KEYCODE_MEDIA_PLAY_PAUSE
            KEYCODE_MEDIA_STOP
            KEYCODE_MEDIA_NEXT
            KEYCODE_MEDIA_PREVIOUS
            KEYCODE_MEDIA_REWIND
            KEYCODE_MEDIA_FAST_FORWARD
            KEYCODE_CAMERA
            KEYCODE_FOCUS
            KEYCODE_SEARCH
        */
        if ( event.isSystem() ) {
            return super.dispatchKeyEvent(event);
        }
        return true;
        //return super.dispatchKeyEvent(event);
    }

2.上記の方法で他のアプリケーションに制御を移すことはできるようになったが、依然としてアプリケーション側からブロック/ブロック解除を制御することができない。これではコンポーネントとしては使えないので、GlassPaneの状態を制御するためのメソッドを追加することにする。

    • lock/unclockメソッドを追加
    //マルチスレッドは考慮していない
    protected boolean lock;

    public void lock() {
        this.lock = true;
        this.innerPaint.setAlpha(225);
        this.invalidate();
    }
    public void unlock() {
        this.lock = false;
        this.innerPaint.setAlpha(0);
        this.invalidate();
    }
    public boolean isLock() {
        return this.lock;
    }
    public void setLock(boolean lock) {
        if ( lock ) {
            this.lock();
        } else {
            this.unlock();
        }
    }

lockとunlockでは描画オブジェクトを切替えるのではなく描画オブジェクトのアルファ値を変更するだけで見た目を変えている。(アンロック時はアルファ値=0で透過つまり見えなくなる)このお陰でdispatchDrawは書き直す必要は無く、必要なのは状態変数lockで現在ロック中か否かを判定して、先ほど無条件でブロックしていた各イベントを通すようにすることだ。

    • dispatch〜メソッドを更に変更
    /* キーボード */
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if ( event.isSystem() ) {
            return super.dispatchKeyEvent(event);
        }
        if ( this.islock() ) {
            return true;
        }
        return super.dispatchKeyEvent(event);
    }
    /* トラックボール */
    @Override
    public boolean dispatchTrackballEvent(MotionEvent event) {
        if ( this.islock() ) {
            return true;
        }
        return super.dispatchTrackballEvent(event);
    }
    /* タッチ */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if ( this.islock() ) {
            return true;
        }
        return super.dispatchTouchEvent(event);
    }

これでlock中のみ入力をブロックするようにできる。

ではサンプルを使って実際に動かしてみよう。

ロック中はGlassPaneではイベントを拾えないので、面倒だが(どんだけ面倒くさがりだよ)オプションメニューで状態をトグルするようにしてみた。

    • GlassPaneTest.java (追加分のみ)
public class GlassPaneTest extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        final GlassPane gp = (GlassPane)this.findViewById(R.id.GlassPane01); 
        gp.lock(); //ロック開始
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        menu.add(Menu.NONE, Menu.FIRST + 1, Menu.NONE, "アンロック");
        return super.onCreateOptionsMenu(menu);
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if ( item.getItemId() == Menu.FIRST + 1 )  {
            final GlassPane gp = (GlassPane)this.findViewById(R.id.GlassPane01);
            gp.setLock(!gp.isLock());
            
            if ( gp.isLock() ) {
                item.setTitle("アンロック");
            } else {
                item.setTitle("ロック");
            }
        }
        return true;
    }
}

では実行してみよう。

    • 実行結果

ロック状態 (起動直後)


アンロック状態 (メニュー選択後)

このように、アプリケーション側でロック/アンロックを制御できるようになった。

ガラス区画上に任意のビューを配置できるようにする

さて、あと一つ残っているが、またもや長くなったので続きはまた今度。