2012年10月9日火曜日

Android SeekBar のトラックとつまみの位置を合わせる

SeekBar でつまみやトラック部分をオリジナルの画像にしたら、バーの進み具合がつまみの中心に合わない、という状況になったことがあると思います。

実は style の設定で合うようにすることができます。

以下では、つまみ部分に星型の画像を、トラック部分に 9patch の画像を用意しています。

つまみ
プログレス
トラックベース


res/values/style.xml で次のように定義し <resources> <style name="AppTheme" parent="android:Theme.Light"> <item name="android:seekBarStyle">@style/MySeekBar</item> </style> <style name="MySeekBar" parent="@android:style/Widget.SeekBar"> <item name="android:indeterminateOnly">false</item> <item name="android:progressDrawable">@drawable/seekbar</item> <item name="android:indeterminateDrawable">@drawable/seekbar</item> <item name="android:thumb">@drawable/seekbar_thumb</item> </style> </resources> Android 2.3.3 をターゲットとしてビルドし、実行すると次のようになります。

確かにバーの進み具体がつまみの中心になっていません。

左端

少し進んだところ

右端

どうしてこうなるかを理解するには、実際のプログラムでどう描画されるのかを知る必要があります。

1. 初期値を知る

上記では parent="@android:style/Widget.SeekBar" しているので、このスタイルで定義されている値をみてみましょう。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/values/styles.xml#397 397 <style name="Widget.SeekBar"> 398 <item name="android:indeterminateOnly">false</item> 399 <item name="android:progressDrawable">@android:drawable/progress_horizontal</item> 400 <item name="android:indeterminateDrawable">@android:drawable/progress_horizontal</item> 401 <item name="android:minHeight">20dip</item> 402 <item name="android:maxHeight">20dip</item> 403 <item name="android:thumb">@android:drawable/seek_thumb</item> 404 <item name="android:thumbOffset">8dip</item> 405 <item name="android:focusable">true</item> 406 </style> レイアウトに関連する値として minHeight と maxHeight が 20dip、thumbOffset が 8dip に設定されています。


2. コードの初期値を知る

parent="@android:style/Widget.SeekBar" を外してみましょう。そうすると、バーの進み具合がつまみの中心になります。


このときは minHeight, maxHeight, thumbOffset は設定されていないわけですから、コード内で設定されていないときの初期値が割り当てられています。

SeekBar の親クラスの AbsSeekBar でその処理が実装されています。

tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AbsSeekBar.java 65 public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) { 66 super(context, attrs, defStyle); 67 68 TypedArray a = context.obtainStyledAttributes(attrs, 69 com.android.internal.R.styleable.SeekBar, defStyle, 0); 70 Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); 71 setThumb(thumb); // will guess mThumbOffset if thumb != null... 72 // ...but allow layout to override this 73 int thumbOffset = a.getDimensionPixelOffset( 74 com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset()); 75 setThumbOffset(thumbOffset); 76 a.recycle(); 77 78 a = context.obtainStyledAttributes(attrs, 79 com.android.internal.R.styleable.Theme, 0, 0); 80 mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f); 81 a.recycle(); 82 83 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 84 } 94 public void setThumb(Drawable thumb) { 95 boolean needUpdate; 96 // This way, calling setThumb again with the same bitmap will result in 97 // it recalcuating mThumbOffset (if for example it the bounds of the 98 // drawable changed) 99 if (mThumb != null && thumb != mThumb) { 100 mThumb.setCallback(null); 101 needUpdate = true; 102 } else { 103 needUpdate = false; 104 } 105 if (thumb != null) { 106 thumb.setCallback(this); 107 108 // Assuming the thumb drawable is symmetric, set the thumb offset 109 // such that the thumb will hang halfway off either edge of the 110 // progress bar. 111 mThumbOffset = thumb.getIntrinsicWidth() / 2; 112 113 // If we're updating get the new states 114 if (needUpdate && 115 (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth() 116 || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) { 117 requestLayout(); 118 } 119 } 120 mThumb = thumb; 121 invalidate(); 122 if (needUpdate) { 123 updateThumbPos(getWidth(), getHeight()); 124 if (thumb.isStateful()) { 125 // Note that if the states are different this won't work. 126 // For now, let's consider that an app bug. 127 int[] state = getDrawableState(); 128 thumb.setState(state); 129 } 130 } 131 } 132 133 /** 134 * @see #setThumbOffset(int) 135 */ 136 public int getThumbOffset() { 137 return mThumbOffset; 138 } 一言でいうと、thumbOffset が明示的に設定されていない場合、つまみ画像の横幅の半分が thumbOffset になります。111行目の処理です。

つまり、parent="@android:style/Widget.SeekBar" を入れていると、thumbOffset が 8dip なので、横幅が 16dip ではない画像をつまみとして使うとずれてしまうという事です。


3. padding をセットする

このままだと端にいったときにつまみが切れてしまいます。実は Holo テーマの SeekBar はその辺りの設定が正しくされています。



Holo テーマでの SeekBar の設定をみてみましょう。 1739 <style name="Widget.Holo.SeekBar"> 1740 <item name="android:indeterminateOnly">false</item> 1741 <item name="android:progressDrawable">@android:drawable/scrubber_progress_horizontal_holo_dark</item> 1742 <item name="android:indeterminateDrawable">@android:drawable/scrubber_progress_horizontal_holo_dark</item> 1743 <item name="android:minHeight">13dip</item> 1744 <item name="android:maxHeight">13dip</item> 1745 <item name="android:thumb">@android:drawable/scrubber_control_selector_holo</item> 1746 <item name="android:thumbOffset">16dip</item> 1747 <item name="android:focusable">true</item> 1748 <item name="android:paddingLeft">16dip</item> 1749 <item name="android:paddingRight">16dip</item> 1750 </style> minHeight, maxHeight, thumbOffset は値が少し変わっています。そのほか、paddingLeft と paddingRight が新しく追加されています。
この設定によって、つまみが内側に収まるようにしているのです。

この padding に設定する値はつまみ画像の横幅の半分にします。

例えば、この星の画像は横幅が 68px で、xhdpi 用としているので、dip に直すと 68 / 2 = 34dip です。 横幅の半分を padding にするので 17dip です。

よって <style name="MySeekBar"> <item name="android:indeterminateOnly">false</item> <item name="android:progressDrawable">@drawable/seekbar</item> <item name="android:indeterminateDrawable">@drawable/seekbar</item> <item name="android:thumb">@drawable/seekbar_thumb</item> <item name="android:paddingLeft">17dip</item> <item name="android:paddingRight">17dip</item> </style> とすれば OK です。


こうするとわかりますが、つまみの画像の横幅は 32dip (xhdpi 用だと 64px)がいいんでしょうね。





1 件のコメント: