GlassPaneを作る (その1)

以前の日記で言及したことがあるが、JFC/SwingはGlassPane(ガラス区画)と呼ばれるレイヤがJFrameに仕込まれており、これを利用してGUI上にブロッカと呼ばれる入力操作を弾く機能を追加できる。

GlassPaneとブロッカ

同じ機能がAndroidアプリケーションでも欲しくなったこともあり、同様に実現できないだろうかと考えてみた。※

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

取り敢えずこんな所だろうか。
順に一つずつ実装していこう。

  • 配下(子供)のGUIを覆う透明又は半透明な領域(パネル)

子供のViewを覆う必要があるため、継承元はViewGroupから派生したコンテナクラスのどれかを使うのが良いが、今回は単純なFrameLayoutから継承することにした。

public class GlassPane extends FrameLayout

Androidは描画する際の色にアルファ値を指定できるため、透明や半透明の背景のビューを作るのは簡単だ。単に背景を透過にしたい場合はsetBackGroundメソッドにより背景色を透過色にすれば良いのだが、他の要件も含めて実現するためには自らの描画を制御する必要があるため、今回は内部の矩形を自ら描画することにした。

public class GlassPane extends FrameLayout  {
    protected Paint innerPaint;
    public GlassPane(Context context) {
        super(context);
        this.init();
    }
    public GlassPane(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.init();
    }
    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); 
    }
    @Override
    protected void dispatchDraw(Canvas canvas) {
        RectF drawRect = new RectF(); 
        drawRect.set(0, 0, this.getMeasuredWidth(), this.getMeasuredHeight()); 
 
        canvas.drawRoundRect(drawRect, 5, 5, this.innerPaint); 

        super.dispatchDraw(canvas);
    }
}

重要なのはdispatchDrawメソッドだ。

通常、描画処理をオーバライドしたい場合はonDrawメソッドだが、それだと配下のビューの描画を制御できない。後述するが、配下のビューをどう描画するかが重要なのでこのメソッドをオーバライドする。
自らの配下にあるビューに描画をディスパッチしており、通常の場合は自らを含めて全ての子供のビューを辿りながら描画処理を伝搬していくが、この例ではオーバライドとして自ら用意した矩形を透過値225の灰色で描画している。

さて、では実際に見てみることにしよう。実験用の画面を用意してレイアウトのルートの直下にGlassPaneを指定した。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:orientation="vertical"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
>
 <org.test.GlassPane
  android:id="@+id/GlassPane01"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
 >
  <LinearLayout
   android:layout_height="wrap_content"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:id="@+id/LinearLayout01"
  >
   <TextView
    android:id="@+id/TextView01"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="TextView01"
   ></TextView>
   <EditText
    android:id="@+id/EditText01"
    android:layout_height="wrap_content"
    android:layout_width="fill_parent"
    android:layout_margin="10dp"
    android:text="EditText01"
   ></EditText>
   <Button
    android:id="@+id/Button01"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:layout_gravity="right"
    android:text="Button01"
   ></Button>
  </LinearLayout>
 </org.test.GlassPane>
</FrameLayout>
  • 実行結果


背景は透過のグレーで描画されているが、他の子供のビューは覆われずにトップレベルに描画されてしまう。これは異常ではなく、上位階層のビューからから下位のビューを順に描画していくと自然にこのように描画される。(レイアウトは何枚重ねても、そこに描画されているビューの操作には影響しない) しかし、これでは要件を満たしていない。

ビューのルートがFrameLayoutであり、配下のビューは全て重なることを考えれば、GlassPaneをLinearLayout01と同じ階層に配置することで全てを覆うことができるが、そうすると

    • GlassPaneの配置順に効果が依存してしまう。(配置順によっては覆っているように見えない)
    • 親のコンテナをFrameLayoutに限定する必要がある (LinearLayout等にするとView同士は重ならないように描画される)

と使いづらいものになってしまう。
そこでdispatchDrawメソッドを変更して、子供のビューの描画を先に行ってしまい、その後に矩形を描画することにした。

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        RectF drawRect = new RectF(); 
        drawRect.set(0, 0, this.getMeasuredWidth(), this.getMeasuredHeight()); 
 
        canvas.drawRoundRect(drawRect, 5, 5, this.innerPaint); 
    }

  • 実行結果


これで期待通り配下のビューは全てGlassPaneに覆われるように描画される。


長くなってきたので今回はこの辺で。
次回は

    • タップ、キーボードによる操作を無効(lock)にする
    • ガラス区画上に任意のビューを配置できるようにする

この辺りの実装を進めていく予定。

Androidにはダイアログ表示中に背後にブラー効果を施す処理が元々入っているが、これはWindowクラス周りで生成時に実行されており、簡単には手を出せそうになかったため、利用を諦めた。