seekBarをかいぞう

数値を選択させる時とかにseek barの使用を検討することは良くあると思う。seek barちうのは、progress bar の上にツマミが乗っかってて、そのツマミを弄くると数値が弄くれる具合の部品だ。ところがこの部品、標準のを使おうとすると結構ガッカリする。ちっちゃいbarだと押し辛いので、大きめのサイズを設定してやろうとすると…悲しい画面になる。

何故だかしらないが、ツマミの画像のサイズが追従してくれないので、非常にマヌケなことになっている。こいつをなんとかしたい。
といっても、まあ、ネットを調べれば散々既出の事項で、ツマミの画像を変更してやれることをすぐに判る。

…のだけど、ネットをみるとxmlを使って画面を作成するひとが多いみたいなんだな。つーか、javaのコードから設定するやり方が何処にも載ってねえ。。。…ってことで、全世界に2人は存在する、xml嫌い!コードで書く派のひとのために方法を書いておくよ!

答え!AbsSeekBar.setThumb(Drawable d)を使って画像を設定してあげればいいよ(seekBarはAbsSeekBarから派生している)!終わり。ついでに、下のprogress barの画像を変更したい場合はprogressBar.setProgressDrawable(Drawable d)を使えばOK。終わり。


ところがどうも調べてみると、Thumb(ツマミ)のサイズはView(seekBar)のサイズから決定されるのでは無く、画像のサイズから決定されるつくりになってるみたいなんだな。ちうことで、seek barの大きさを動的に変更したりすると、結局おんなじ問題がついてまわる宿命のようなんだな。。。なんてイケてねえ。。。

そんなわけで、じゃあViewの大きさから画像のサイズを返すようなDrawableを自作してやればいいんじゃね?という発想でやってみたら、それなりに動きましたのでご報告。

まあ、単純にandroid.graphics.drawable.Drawableを継承してクラスを作っただけなんだが…。Drawableはabstractなクラスなんで、幾つかオーバーライドしなくちゃならんメソッドが幾つかあるわけ。それが以下の4つ。

  public void setColorFilter(ColorFilter cf)
  public void setAlpha(int alpha)
  public int getOpacity()
  public void draw(Canvas canvas)

使用しないのであれば、こいつらは空実装しておけばよさそうなのだが。。。目を引くのがdraw(Canvas)。まあ、想像通り、seekBarのonDrawの中で呼ばれる(確か)ので、こいつでツマミの絵を出すようにすればいいわけだな。つうか、Canvasを受け取った時点で、もういくらでも好きに出来ますよ!ってことなんで、ゲーム製作脳的には燃えてくる展開だよな!

で、無事に実装が終わってアプリを実行してみると…ツマミが表示されない!ガッカリ!

というのは、実は前述のように、seekBarが画像のサイズを問い合わせて、そこからThumbのサイズを決定してるので、そこんところも修正してやらないといけない。その問い合わせで使用されるメソッドが以下の2つ。

  public int getIntrinsicWidth()
  public int getIntrinsicHeight()

まあ見たままなんだけど、getIntrinsicWidthとgetIntrinsicHeightがそれぞれViewのサイズから算出した幅と高さを返すようにオーバーライドしてやればいい。そうすっと、Thumbのサイズをテケトーに算出してくれて、getBounds()してやるとThumbの描画域をRectで返してくれる。それを使ってDraw(Canvas)してやれば、無事にThumbが表示されるようになる。やったー!

…それで終了でもいいのだが、標準のseekBarってフォーカスが当たると色が変わるんだよね。。。その辺も一応サポートしておこう。

フォーカスが当たってる、等々の情報はint[] getState()で取得できるので、そんなかに"state_focused"に相当するint値が入っているかを走査してやればよい。上のdraw(Canvas)にて、フォーカスのあたり具合で分岐してやればいいわけだね。

なお、Stateを使用する場合には以下のメソッドをオーバーライドしてやる必要がある。

  public boolean isStateful() { return true; }

上のクラスでreturn falseしてるままだと、受け取ったステータスを破棄しちゃうので、トゥルっておかないとだめ。

これで望む通りにThumbが表示される。ついでにprogressBar部分のDrawableも置き換える時の注意点を記載しておこう。

特に注意するところは無いのだが、メーターのたまり具合に応じて表示してやるところが問題となるか。メータの溜まり具合はgetLevel()で取得できて、これは10000を100%とするint値なので、それを適当に加工してdraw(Canvas)してあげてください。


てなあたりを踏まえてつくったぼくのseek barはこれだ!

んー、それなりに満足してるけど、もっとcoolにならんもんかな。。。

まあ、ソースも載っけてみますので、暇なひとは見てみて。

import android.content.Context;
import android.util.StateSet;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.widget.SeekBar;
import android.graphics.drawable.Drawable;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;

public class MySeekBar extends SeekBar{
  private int color = 0xffff0000;
  public void setColor(int color){ this.color = color; }
  
  private final SeekBar seekBar = this;
  
  public MySeekBar(Context c){
    super(c);
    
    this.setThumb(new Thumb());
    this.setProgressDrawable(new Progress());
  }

  private class Thumb extends Drawable{
    @Override
    public void setColorFilter(ColorFilter cf){};
    
    @Override
    public void setAlpha(int alpha){};
    
    @Override
    public int getOpacity(){return PixelFormat.OPAQUE;};

    @Override
    public boolean isStateful() { return true; }
    
    @Override
    public int getIntrinsicWidth(){ return getIntrinsicHeight()/3; }
    
    @Override
    public int getIntrinsicHeight(){ return seekBar.getHeight() - seekBar.getPaddingTop() - seekBar.getPaddingBottom(); };
    
    @Override
    public void draw(Canvas canvas){
      int[] STATE_FOCUSED = { android.R.attr.state_focused }; 

      Rect rect = new Rect();
      rect.set(getBounds());
      {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0xffffffff);
        canvas.drawRect( rect, paint);
        
        paint.setColor(color);
        int delta = 4;
        Rect colorRect = new Rect(rect.left+delta, rect.top+delta, rect.right-delta, rect.bottom-delta);
        
        canvas.drawRect( colorRect, paint);
        
        if( StateSet.stateSetMatches(STATE_FOCUSED, getState())){
          paint.setColor(0xffff4000);
        } else {
          paint.setColor(0xff000000);
        }
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(2);
        canvas.drawRect( rect, paint);
      }
    }
  }

  private class Progress extends Drawable{
    @Override
    public void setColorFilter(ColorFilter cf){};

    @Override
    public void setAlpha(int alpha){};
    
    @Override
    public int getOpacity(){return PixelFormat.OPAQUE;};

    @Override
    public void draw(Canvas canvas){
      Paint paint = new Paint();
      paint.setAntiAlias(true);

      Rect rect = new Rect();
      rect.set(getBounds());

      final int delta = 5;
      RectF baseRect = new RectF(rect.left+delta, rect.top+delta, rect.right+delta, rect.bottom+delta );
      RectF progressRect = new RectF(rect.left, rect.top, rect.width()*getLevel()/10000, rect.bottom );
      paint.setColor(0xffffffff);
      canvas.drawRoundRect(baseRect, 5, 5, paint);

      paint.setColor(0x80000000 + (0x00ffffff & color));
      canvas.drawRoundRect(progressRect, 5, 5, paint);
    }
  }
}