2012年10月9日火曜日

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

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

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

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

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


res/values/style.xml で次のように定義し
  1. <resources>  
  2.   
  3.     <style name="AppTheme" parent="android:Theme.Light">  
  4.         <item name="android:seekBarStyle">@style/MySeekBar</item>  
  5.     </style>  
  6.   
  7.     <style name="MySeekBar" parent="@android:style/Widget.SeekBar">  
  8.         <item name="android:indeterminateOnly">false</item>  
  9.         <item name="android:progressDrawable">@drawable/seekbar</item>  
  10.         <item name="android:indeterminateDrawable">@drawable/seekbar</item>  
  11.         <item name="android:thumb">@drawable/seekbar_thumb</item>  
  12.     </style>  
  13.   
  14. </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
  1. 397     <style name="Widget.SeekBar">  
  2. 398         <item name="android:indeterminateOnly">false</item>  
  3. 399         <item name="android:progressDrawable">@android:drawable/progress_horizontal</item>  
  4. 400         <item name="android:indeterminateDrawable">@android:drawable/progress_horizontal</item>  
  5. 401         <item name="android:minHeight">20dip</item>  
  6. 402         <item name="android:maxHeight">20dip</item>  
  7. 403         <item name="android:thumb">@android:drawable/seek_thumb</item>  
  8. 404         <item name="android:thumbOffset">8dip</item>  
  9. 405         <item name="android:focusable">true</item>  
  10. 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
  1.  65     public AbsSeekBar(Context context, AttributeSet attrs, int defStyle) {  
  2.  66         super(context, attrs, defStyle);  
  3.  67   
  4.  68         TypedArray a = context.obtainStyledAttributes(attrs,  
  5.  69                 com.android.internal.R.styleable.SeekBar, defStyle, 0);  
  6.  70         Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb);  
  7.  71         setThumb(thumb); // will guess mThumbOffset if thumb != null...  
  8.  72         // ...but allow layout to override this  
  9.  73         int thumbOffset = a.getDimensionPixelOffset(  
  10.  74                 com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset());  
  11.  75         setThumbOffset(thumbOffset);  
  12.  76         a.recycle();  
  13.  77   
  14.  78         a = context.obtainStyledAttributes(attrs,  
  15.  79                 com.android.internal.R.styleable.Theme, 00);  
  16.  80         mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);  
  17.  81         a.recycle();  
  18.  82   
  19.  83         mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();  
  20.  84     }  
  21.   
  22.  94     public void setThumb(Drawable thumb) {  
  23.  95         boolean needUpdate;  
  24.  96         // This way, calling setThumb again with the same bitmap will result in  
  25.  97         // it recalcuating mThumbOffset (if for example it the bounds of the  
  26.  98         // drawable changed)  
  27.  99         if (mThumb != null && thumb != mThumb) {  
  28. 100             mThumb.setCallback(null);  
  29. 101             needUpdate = true;  
  30. 102         } else {  
  31. 103             needUpdate = false;  
  32. 104         }  
  33. 105         if (thumb != null) {  
  34. 106             thumb.setCallback(this);  
  35. 107   
  36. 108             // Assuming the thumb drawable is symmetric, set the thumb offset  
  37. 109             // such that the thumb will hang halfway off either edge of the  
  38. 110             // progress bar.  
  39. 111             mThumbOffset = thumb.getIntrinsicWidth() / 2;  
  40. 112   
  41. 113             // If we're updating get the new states  
  42. 114             if (needUpdate &&  
  43. 115                     (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()  
  44. 116                         || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {  
  45. 117                 requestLayout();  
  46. 118             }  
  47. 119         }  
  48. 120         mThumb = thumb;  
  49. 121         invalidate();  
  50. 122         if (needUpdate) {  
  51. 123             updateThumbPos(getWidth(), getHeight());  
  52. 124             if (thumb.isStateful()) {  
  53. 125                 // Note that if the states are different this won't work.  
  54. 126                 // For now, let's consider that an app bug.  
  55. 127                 int[] state = getDrawableState();  
  56. 128                 thumb.setState(state);  
  57. 129             }  
  58. 130         }  
  59. 131     }  
  60. 132   
  61. 133     /** 
  62. 134      * @see #setThumbOffset(int) 
  63. 135      */  
  64. 136     public int getThumbOffset() {  
  65. 137         return mThumbOffset;  
  66. 138     }  
一言でいうと、thumbOffset が明示的に設定されていない場合、つまみ画像の横幅の半分が thumbOffset になります。111行目の処理です。

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


3. padding をセットする

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



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

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

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

よって
  1. <style name="MySeekBar">  
  2.     <item name="android:indeterminateOnly">false</item>  
  3.     <item name="android:progressDrawable">@drawable/seekbar</item>  
  4.     <item name="android:indeterminateDrawable">@drawable/seekbar</item>  
  5.     <item name="android:thumb">@drawable/seekbar_thumb</item>  
  6.     <item name="android:paddingLeft">17dip</item>  
  7.     <item name="android:paddingRight">17dip</item>  
  8. </style>  
とすれば OK です。


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





0 件のコメント:

コメントを投稿