GlassPaneを作る(その3)

OracleAndroidを提訴したことでかなり萎えたが、めげずにいってみよう。

前回はGlassPaneの表示時(lock時)に入力をブロックするところまでを実装した。

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

  • 自身の上に予め登録されたGUIを描画でき、そのGUIだけは入力を受け付ける

GlassPaneはビューのレイヤであり、元々がViewGroup抽象クラスから派生したレイアウト具象クラスの派生クラスなので、他のビューを乗せることができる。ただし、単に乗せるだけではなく

    • ロックの対象となるGlassPaneの全ての子孫(Descendant)
    • ロックの対象とならないGlassPane上に配置されるビュー(FloatingView)

これらを分けて管理したい。なぜならば、子孫のビューをロックした際に行う処理があり(その中にはロックを解除する処理も含まれる)その処理を行うためには前回サンプルで書いたようにオプションメニューを使用する方法もあるが、Androidのような画面の小さな携帯端末UIでは、できるだけ見やすくタップする回数の少ないスクリーンを使って処理を行い、オプションメニューは次善の作とするのが通例だ。

ロックの対象となる子孫は通常の子孫として扱えば良いので、特に追加の実装は無い。前回は子孫のビューに伝搬するイベントをブロックする処理を入れたが、今回は後述するがロック時のフォーカス戦略に少し変更があるだけだ。

そして、ロックの対象とならない、GlassPane上に配置・描画されるビューは"FloatingView"として追加することとする。内部の処理ではフィールドcontainerに子孫のビューとして追加するが、追加できるのは一つだけ、それをGlassPaneの中央に配置するこことする。(ただし、追加するのはレイアウトでもよい)

    • GlassPane.java (変更箇所のみ)
public class GlassPane extends FrameLayout  {
    :
    protected LinearLayout container;
    :
    private void init() {
        this.setLayoutParams(new LayoutParams
                (LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
        this.innerPaint = new Paint(); 
        this.innerPaint.setARGB(225, 75, 75, 75); 
        this.innerPaint.setAntiAlias(true); 
        
        this.container = new LinearLayout(this.getContext());
        this.container.setBackgroundColor(Color.argb(0, 0, 0, 0));
        this.container.setLayoutParams(new LayoutParams
                (LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
        this.container.setGravity(Gravity.CENTER_HORIZONTAL|Gravity.CENTER_VERTICAL);
        
        this.addView(this.container);
    }
    public void setFloatingView(View view) {
        if ( this.container.getChildAt(0) != null ) {
            this.container.removeViewAt(0);
        }
        ViewParent parent = view.getParent();
        if ( parent != null ) {
            *1; 
 
        canvas.drawRoundRect(drawRect, 5, 5, this.innerPaint); 

        this.container.draw(canvas);

    }
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if ( event.isSystem() ) {
            return super.dispatchKeyEvent(event);
        }
        if ( this.lock ) {
            return this.container.dispatchKeyEvent(event);
        }
        return super.dispatchKeyEvent(event);
    }
    @Override
    public boolean dispatchTrackballEvent(MotionEvent event) {
        if ( this.lock ) {
            return this.container.dispatchTrackballEvent(event);
        }
        return super.dispatchTrackballEvent(event);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if ( this.lock ) {
            return this.container.dispatchTouchEvent(event);
        }
        return super.dispatchTouchEvent(event);
    }
}

新たに追加したフィールドcontainerにsetFloatingViewメソッドでGlassPane上に配置するビューを追加する。(今回は一つだけ可能とし、GlassPaneの中心に配置・描画する) また、dispatchDrawメソッドではcontainerは他の子孫と違い、通常通りに描画を実施している。

なお、前回ロック中は単純にtrueを返していたdispatch〜イベント〜メソッドは、

    • ロック中 containerのみイベントを有効にする
    • ロック解除中 通常通りに処理

と変更している。

では実際に動かしてみよう。

最も予想される使い方としてロック状態をトグルする処理を前回はオプションメニューで書いたが、今回は同様の処理をGlassPane上に配置したボタンで行うように書いてみた。

    • GlassPaneTest.java
public class GlassPaneTest extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(org.test.R.layout.main);

        final Button btn = (Button)this.findViewById(org.test.R .id.Button04);
        btn.setText("アンロック");
        
        final GlassPane gp = (GlassPane)this.findViewById(org.test.R.id.GlassPane01); 

        gp.setFloatingView(btn);
        btn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                gp.setLock(!gp.isLock());
                
                if ( gp.isLock() ) {
                    btn.setText("アンロック");
                } else {
                    btn.setText("ロック");
                }
                
            }
        });

        gp.lock();
    }
    • 実行結果

起動直後

アンロック後

想定していた通り、GlassPane上に配置されたボタンでロック/アンロックをトグルできるようになったが、ロック中でイベントをブロックされているはずのButton01やEditText01が、なぜかフォーカスを得ることが可能になってしまっている

既に書いた通り、ロック中でもボタンのイベントを処理するためにdispatch〜メソッドを書き換えているが、

    if ( this.lock ) {
        return this.container.dispatchKeyEvent(event);
    }

これだと期待通りボタンは動作するのだが、内部的には同一の親(GlassPane)に属している子孫のビューの間でフォーカスの移動が可能なため、矢印キー押下、トラックボール回転でブロックされているはずのビューにフォーカスが移ってしまうのだ。

container以外にイベントを伝搬させないために、戻り値に関わらずイベントを消費したことにしようともしたが、

    if ( this.lock ) {
        this.container.dispatchKeyEvent(event);
        return true;
    }

これだとロック中のフォーカスの移動は抑制されるものの、今度はFloatingビュー上でフォーカスの移動ができない不具合が発生するし、何より"格好悪い"コードなので止めた。

このような場合、根本的な部分から理解して最善の解決としたいところだ。

ちょっと調べてみることにしよう。

*1:ViewGroup)parent).removeView(view); } this.container.addView(view, 0); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); RectF drawRect = new RectF(); drawRect.set(0, 0, this.getMeasuredWidth(), this.getMeasuredHeight(