2012年10月30日火曜日

数学が好きか嫌いかは解けるようになってから言え

総裁の本読みましたー。なぜか朝会社にきたらあったので。



普通に面白かったです。
ブログのまとめなので、募金の話とか懐かしかったです。
あんまり眠くならないので睡眠導入剤代わりにはならないと思います。
通勤時に電車で読むのがいいのではないでしょうか。

個人的に気に入ったのは


"僕が高校のときに読んだ数学の参考書にこう書いてありました。

「好きだからできるようになるのではない。できるようになったから好きになるのだ。」

数学の本にそう書かれると、「好きか嫌いかは、解けるようになってから言えよバーカ」という意味に取れなくもないです。"


というところです。「できるかできないかは、やってみてから言えよバーカ」というのは案ずるより産むが易しだっけ。
ちょっと違うかも。
まぁいいか。

食わず嫌いはいろいろもったいないよね。


読んだ直後にこれ書いてるので効能はわかりませんが、こないだ行った伊豆大島の温泉(塩泉)でお肌つるつるになりました。


2012年10月29日月曜日

How to create looping ViewPager

ループできる ViewPager です。ちゃんと作ってないので以下の制限があります。
  • ページ数が 3の倍数じゃないといけない(時間がなくて対応してない)
  • 連続ページ切り替え(スワイプ後のスクロールが終わる前にさらにスワイプ)に対応してない
  • わかんないけどなんかあるかも
ポイントは isViewFromObject() を駆使することと、ViewPager を extends して onLayout で位置を戻すことです。
  1. public class MainActivity extends Activity {  
  2.   
  3.     private ViewPager mViewPager;  
  4.   
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);  
  9.   
  10.         mPagerAdapter = new MyPagerAdapter(this);  
  11.         mViewPager = (ViewPager) findViewById(R.id.viewpager);  
  12.         mViewPager.setOffscreenPageLimit(2);  
  13.         mViewPager.setAdapter(mPagerAdapter);  
  14.         mViewPager.setOnPageChangeListener(mPagerAdapter);  
  15.         mViewPager.setCurrentItem(1false);  
  16.     }  
  17.   
  18.     private MyPagerAdapter mPagerAdapter;  
  19.   
  20.     private class MyPagerAdapter extends PagerAdapter implements ViewPager.OnPageChangeListener {  
  21.   
  22.         /** 
  23.          * from 0 to 2 
  24.          */  
  25.         int focusedPosition = 0;  
  26.   
  27.         /** 
  28.          * from 0 to PAGE_NUM -1 
  29.          */  
  30.         int contentPosition = 0;  
  31.   
  32.         /** 
  33.          * number of pages (3x) 
  34.          */  
  35.         int PAGE_NUM = 9;  
  36.   
  37.         Context mContext;  
  38.         LayoutInflater mInflater;  
  39.   
  40.         public MyPagerAdapter(Context context) {  
  41.             mContext = context;  
  42.             mInflater = LayoutInflater.from(context);  
  43.         }  
  44.   
  45.         @Override  
  46.         public int getCount() {  
  47.             return 3;  
  48.         }  
  49.   
  50.         SparseArray<View> mViews = new SparseArray<View>();  
  51.   
  52.         @Override  
  53.         public Object instantiateItem(ViewGroup container, int position) {  
  54.             int newPosition = calcContentPosition(position);  
  55.             View view = createContent(newPosition);  
  56.             container.addView(view, 0);  
  57.             mViews.put(position, view);  
  58.             view.setTag(position);  
  59.             // return view;  
  60.             return Integer.valueOf(position);  
  61.         }  
  62.   
  63.         @Override  
  64.         public boolean isViewFromObject(View view, Object object) {  
  65.             int index = (Integer) view.getTag();  
  66.             int position = (Integer) object;  
  67.             int newPosition = (position + contentPosition) % 3;  
  68.             return newPosition == index;  
  69.         }  
  70.   
  71.         /** 
  72.          * create content's view 
  73.          *  
  74.          * @param position 
  75.          *            from 0 to PAGE_NUM - 1 
  76.          * @return 
  77.          */  
  78.         private View createContent(int position) {  
  79.             View view = mInflater.inflate(R.layout.list, nullfalse);  
  80.             updateContent(view, position);  
  81.             return view;  
  82.         }  
  83.   
  84.         /** 
  85.          * update content's view 
  86.          *  
  87.          * @param view 
  88.          * @param position 
  89.          *            from 0 to PAGE_NUM -1 
  90.          */  
  91.         private void updateContent(View view, int position) {  
  92.             ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, android.R.layout.simple_list_item_1,  
  93.                     createList(position));  
  94.             ListView listView = (ListView) view.findViewById(R.id.list);  
  95.             listView.setAdapter(adapter);  
  96.             listView.setScrollingCacheEnabled(false);  
  97.         }  
  98.   
  99.         private List<String> createList(int index) {  
  100.             List<String> list = new ArrayList<String>();  
  101.             for (int i = 0; i < 50; i++) {  
  102.                 list.add(index + " *************************** " + index + ":" + (i + 1));  
  103.             }  
  104.             return list;  
  105.         }  
  106.   
  107.         /** 
  108.          * get position of content 
  109.          *  
  110.          * @param position 
  111.          *            from 0 to 2 
  112.          * @return content position (from 0 to PAGE_NUM - 1) 
  113.          */  
  114.         private int calcContentPosition(int position) {  
  115.             int offset = position - 1;  
  116.             int newPosition = contentPosition + offset;  
  117.             if (newPosition < 0) {  
  118.                 newPosition = PAGE_NUM - 1;  
  119.             } else {  
  120.                 newPosition = newPosition % PAGE_NUM;  
  121.             }  
  122.             return newPosition;  
  123.         }  
  124.   
  125.         @Override  
  126.         public void destroyItem(ViewGroup container, int position, Object object) {  
  127.             int count = mViewPager.getChildCount();  
  128.             int index = (Integer) object;  
  129.             for (int i = 0; i < count; i++) {  
  130.                 View v = mViewPager.getChildAt(i);  
  131.                 if (isViewFromObject(v, Integer.valueOf(index))) {  
  132.                     container.removeView(v);  
  133.                     break;  
  134.                 }  
  135.             }  
  136.         }  
  137.   
  138.         /** 
  139.          * update specified view's content 
  140.          *  
  141.          * @param index 
  142.          *            0 or 2 
  143.          */  
  144.         void updateContents(int index) {  
  145.             int count = mViewPager.getChildCount();  
  146.             for (int i = 0; i < count; i++) {  
  147.                 View v = mViewPager.getChildAt(i);  
  148.                 if (isViewFromObject(v, Integer.valueOf(index))) {  
  149.                     final int newPosition = calcContentPosition(index);  
  150.                     updateContent(v, newPosition);  
  151.                     break;  
  152.                 }  
  153.             }  
  154.         }  
  155.   
  156.         @Override  
  157.         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {  
  158.         }  
  159.   
  160.         @Override  
  161.         public void onPageSelected(int position) {  
  162.             focusedPosition = position;  
  163.         }  
  164.   
  165.         @Override  
  166.         public void onPageScrollStateChanged(int state) {  
  167.             if (state == ViewPager.SCROLL_STATE_IDLE) {  
  168.                 handlePageScrollStateChangedToIdle();  
  169.             }  
  170.         }  
  171.   
  172.         private void handlePageScrollStateChangedToIdle() {  
  173.             switch (focusedPosition) {  
  174.                 case 0:  
  175.                     // scroll to previous page  
  176.                     if ((--contentPosition) < 0) {  
  177.                         contentPosition = PAGE_NUM - 1;  
  178.                     }  
  179.                     updateContents(0);  
  180.                     break;  
  181.   
  182.                 case 2:  
  183.                     // scroll to next page  
  184.                     contentPosition = (++contentPosition) % PAGE_NUM;  
  185.                     updateContents(2);  
  186.                     break;  
  187.                 default:  
  188.                     break;  
  189.             }  
  190.         }  
  191.     }  
  192. }  
  1. public class MyViewPager extends ViewPager {  
  2.   
  3.     public MyViewPager(Context context, AttributeSet attrs) {  
  4.         super(context, attrs);  
  5.     }  
  6.       
  7.     @Override  
  8.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  9.         super.onLayout(changed, l, t, r, b);  
  10.           
  11.         if(getCurrentItem() != 1) {  
  12.             setCurrentItem(1false);  
  13.         }  
  14.     }  
  15. }  
res/layout/main.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <com.sample.viewpager.MyViewPager   
  3.     xmlns:android="http://schemas.android.com/apk/res/android"  
  4.     android:id="@+id/viewpager"  
  5.     android:layout_width="match_parent"  
  6.     android:layout_height="match_parent" />  
res/layout/list.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <ListView   
  3.     xmlns:android="http://schemas.android.com/apk/res/android"  
  4.     android:id="@+id/list"  
  5.     android:layout_width="match_parent"  
  6.     android:layout_height="match_parent"  
  7.     android:cacheColorHint="#00000000" />  

2012年10月23日火曜日

Android View の位置を取得する

View クラスのメソッドで位置やサイズを取得するメソッドがいくつかあるので紹介します。

getLocationInWindow(int[] location)

ウィンドウ上でのこの View の位置を計算します。引数は長さが2以上の int 配列で、Index 0 に x 座標、Index 1 に y 座標の値が入ります。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/View.java#12047
  1. 12047     public void getLocationInWindow(int[] location) {  
  2. 12048         if (location == null || location.length < 2) {  
  3. 12049             throw new IllegalArgumentException("location must be an array of "  
  4. 12050                     + "two integers");  
  5. 12051         }  
  6. 12052   
  7. 12053         location[0] = mLeft;  
  8. 12054         location[1] = mTop;  
  9. 12055         if (mTransformationInfo != null) {  
  10. 12056             location[0] += (int) (mTransformationInfo.mTranslationX + 0.5f);  
  11. 12057             location[1] += (int) (mTransformationInfo.mTranslationY + 0.5f);  
  12. 12058         }  
  13. 12059   
  14. 12060         ViewParent viewParent = mParent;  
  15. 12061         while (viewParent instanceof View) {  
  16. 12062             final View view = (View)viewParent;  
  17. 12063             location[0] += view.mLeft - view.mScrollX;  
  18. 12064             location[1] += view.mTop - view.mScrollY;  
  19. 12065             if (view.mTransformationInfo != null) {  
  20. 12066                 location[0] += (int) (view.mTransformationInfo.mTranslationX + 0.5f);  
  21. 12067                 location[1] += (int) (view.mTransformationInfo.mTranslationY + 0.5f);  
  22. 12068             }  
  23. 12069             viewParent = view.mParent;  
  24. 12070         }  
  25. 12071   
  26. 12072         if (viewParent instanceof ViewRootImpl) {  
  27. 12073             // *cough*  
  28. 12074             final ViewRootImpl vr = (ViewRootImpl)viewParent;  
  29. 12075             location[1] -= vr.mCurScrollY;  
  30. 12076         }  
  31. 12077     }  
mTransformationInfo は変形(Rotate, Scale, Translation など)の情報を保持するためのフィールドです。translationX や translationY がセットされていればその分もカウントするということです。

例えば、次のような画面のボタンに対して呼び出すと



  1. final int[] anchorPos = new int[2];  
  2. v.getLocationOnScreen(anchorPos);  
  3.   
  4. Log.d(TAG, "position : " + v.getLeft() + ", " + v.getTop());  
  5. Log.d(TAG, "window location : " + anchorPos[0] + ", " + anchorPos[1] );  
position : 0, 0
window location : 0, 146

となりますが、translationX をセットすると

  1. v.setTranslationX(100);  
  2. final int[] anchorPos = new int[2];  
  3. v.getLocationOnScreen(anchorPos);  
  4.   
  5. Log.d(TAG, "position : " + v.getLeft() + ", " + v.getTop());  
  6. Log.d(TAG, "window location : " + anchorPos[0] + ", " + anchorPos[1] );  
position : 0, 0
window location : 100, 146

となります。translationX は getLeft() には影響しません。

mParent をたどって、View の親 ViewGroup の左上の位置とスクロール位置と Translation を考慮するので、例えば次のように ScrollView の中にいれたとすると



最初の状態では
window location : 0, 746

ですが、スクロールするとその分だけ y の位置が変わります。
window location : 0, 488






getLocationOnScreen(int[] location)

スクリーン上でのこの View の位置を計算します。引数は長さが2以上の int 配列で、Index 0 に x 座標、Index 1 に y 座標の値が入ります。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/View.java#12030
  1. 12030     public void getLocationOnScreen(int[] location) {  
  2. 12031         getLocationInWindow(location);  
  3. 12032   
  4. 12033         final AttachInfo info = mAttachInfo;  
  5. 12034         if (info != null) {  
  6. 12035             location[0] += info.mWindowLeft;  
  7. 12036             location[1] += info.mWindowTop;  
  8. 12037         }  
  9. 12038     }  
mAttachInfo が null なら getLoactionOnScreen() の戻り値は getLocationInWindow() と同じになるということです。
getLocationInWindow() と getLocationOnScreen() が異なる例として、ダイアログがあります。
例えばこの Activity のテーマを Theme.Holo.Light.Dialog にしてみます。



そうすると、次のように値が異なります。

window location : 16, 148
screen location : 56, 635

getLocationInWindow() はダイアログからの相対位置、getLocationOnScreen() は画面上の位置になります。
もちろん Dialog クラスを使ったダイアログ上の View でも同じです。




getWindowVisibleDisplayFrame(Rect outRect)

このメソッドは上の2つとはちょっと違って、このビューが配置されているウィンドウのサイズを取得します。
実質的にはこのコンテンツ(View)が配置できかつユーザーから見える領域になります。
引数で渡した Rect の top, bottom, left, right に値が格納されます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/view/View.java#5825
  1. 5825     public void getWindowVisibleDisplayFrame(Rect outRect) {  
  2. 5826         if (mAttachInfo != null) {  
  3. 5827             try {  
  4. 5828                 mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect);  
  5. 5829             } catch (RemoteException e) {  
  6. 5830                 return;  
  7. 5831             }  
  8. 5832             // XXX This is really broken, and probably all needs to be done  
  9. 5833             // in the window manager, and we need to know more about whether  
  10. 5834             // we want the area behind or in front of the IME.  
  11. 5835             final Rect insets = mAttachInfo.mVisibleInsets;  
  12. 5836             outRect.left += insets.left;  
  13. 5837             outRect.top += insets.top;  
  14. 5838             outRect.right -= insets.right;  
  15. 5839             outRect.bottom -= insets.bottom;  
  16. 5840             return;  
  17. 5841         }  
  18. 5842         Display d = WindowManagerImpl.getDefault().getDefaultDisplay();  
  19. 5843         d.getRectSize(outRect);  
  20. 5844     }  
例えば、次のような画面のボタンに対して呼び出すと



dispalyFrame : 0, 50, 720, 1184
(left, top, right, bottom)

となります。ステータスバーやナビゲーションバーは入らない(View を配置できない)ということです。

一方、次のように Toast にセットしたビューに対して取得すると

dispalyFrame : 0, 0, 720, 1184
(left, top, right, bottom)

となり、ステータスバーの領域も含まれます。
これは Toast を表示する Window のレイヤーが Activity の setContentView() で表示される View とは異なるからです。

  1. Toast toast = Toast.makeText(MainActivity.this"Hello World", Toast.LENGTH_SHORT);  
  2. toast.setGravity(Gravity.TOP, 00);  
  3. toast.show();  
  4. toast.getView().getWindowVisibleDisplayFrame(displayFrame);  


また、IME 部分は含まれません。そのため、IME を表示した状態と表示していない状態では値がかわります。

表示していない状態
dispalyFrame : 0, 50, 720, 1184
(left, top, right, bottom)



表示している状態
dispalyFrame : 0, 50, 720, 604
(left, top, right, bottom)



ちなみに DisplayMetrics の値は
widthPixels : 720
heightPixels : 1184
でステータスバーは入りますが、ナビゲーションバーは入りません(ナビゲーションバーの高さは 48dip * 2 = 96px)。


getGlobalVisibleRect(Rect r)

この View の可視領域を global (root) の座標で返します。 getLocationInWindow() と同じように親の ViewGroup の位置、スクロール、Translation が考慮されます。


getDrawingRect(Rect outRect)

この View の可視描画領域を返します。 親の ViewGroup の位置、スクロール、Translation は考慮されません。




2012年10月22日月曜日

Android getDefaultSharedPreferences() の SharedPreferences の名前を取得する

結論

「PreferenceManager の getSharedPreferenceName() 呼べば OK」

PreferenceManager のコンストラクタ

init()

getDefaultSharedPreferencesName() で取得した名前を setSharedPreferencesName() で mSharedPreferencesName にセット

getDefaultSharedPreferences() では getDefaultSharedPreferencesName() で取得した名前の SharedPreferences を返しているので、

getSharedPreferencesName() を呼んで mSharedPreferencesName を取得すればいいということになる。
ただし、setSharedPreferencesName() を呼んで名前を変えてしまうと、getDefaultSharedPreferencesName() と違う文字列が帰ってくることになるので注意が必要。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/preference/PreferenceManager.java
  1. 105     private String mSharedPreferencesName;  
  2.   
  3. 141     PreferenceManager(Activity activity, int firstRequestCode) {  
  4. 142         mActivity = activity;  
  5. 143         mNextRequestCode = firstRequestCode;  
  6. 144   
  7. 145         init(activity);  
  8. 146     }  
  9.   
  10. 160     private void init(Context context) {  
  11. 161         mContext = context;  
  12. 162   
  13. 163         setSharedPreferencesName(getDefaultSharedPreferencesName(context));  
  14. 164     }  
  15.   
  16. 302     public String getSharedPreferencesName() {  
  17. 303         return mSharedPreferencesName;  
  18. 304     }  
  19.   
  20. 313     public void setSharedPreferencesName(String sharedPreferencesName) {  
  21. 314         mSharedPreferencesName = sharedPreferencesName;  
  22. 315         mSharedPreferences = null;  
  23. 316     }  
  24.   
  25. 365     public static SharedPreferences getDefaultSharedPreferences(Context context) {  
  26. 366         return context.getSharedPreferences(getDefaultSharedPreferencesName(context),  
  27. 367                 getDefaultSharedPreferencesMode());  
  28. 368     }  
  29.   
  30. 370     private static String getDefaultSharedPreferencesName(Context context) {  
  31. 371         return context.getPackageName() + "_preferences";  
  32. 372     }  


ちなみに PreferenceManager のインスタンスを取得するには、2.x なら PreferenceActivity, 3.0 以降なら PreferenceFragment の getPreferenceManager() から取得できます。

PreferenceManager のコンストラクタが package private だから support package で PreferenceFragment が提供されないのかなと思いました。

2012年10月18日木曜日

Android ListPopupWindow で折り返し表示させる

ListPopupWindow の1行のレイアウトに android.R.layout.simple_list_item_1 を指定すると、TextView に表示する文字列が1行の幅より長い場合に折り返さずに切れてしまいます。



android.R.layout.simple_list_item_1
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <!-- Copyright (C) 2006 The Android Open Source Project  
  3.   
  4.      Licensed under the Apache License, Version 2.0 (the "License");  
  5.      you may not use this file except in compliance with the License.  
  6.      You may obtain a copy of the License at  
  7.     
  8.           http://www.apache.org/licenses/LICENSE-2.0  
  9.     
  10.      Unless required by applicable law or agreed to in writing, software  
  11.      distributed under the License is distributed on an "AS IS" BASIS,  
  12.      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
  13.      See the License for the specific language governing permissions and  
  14.      limitations under the License.  
  15. -->  
  16.   
  17. <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
  18.     android:id="@android:id/text1"  
  19.     android:layout_width="match_parent"  
  20.     android:layout_height="wrap_content"  
  21.     android:textAppearance="?android:attr/textAppearanceListItemSmall"  
  22.     android:gravity="center_vertical"  
  23.     android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"  
  24.     android:paddingRight="?android:attr/listPreferredItemPaddingRight"  
  25.     android:minHeight="?android:attr/listPreferredItemHeightSmall"  
  26. />  


  1. protected void showListPopup(View anchor) {  
  2.   
  3.     ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,   
  4.             android.R.layout.simple_list_item_1,   
  5.             android.R.id.text1,  
  6.             new String[] {   
  7.                "test",   
  8.                "test2test2test2test2test2test2test2test2test2",   
  9.                "test3",   
  10.                "test4" });  
  11.   
  12.     ListPopupWindow popup = new ListPopupWindow(this);  
  13.     popup.setAdapter(adapter);  
  14.     popup.setAnchorView(anchor);  
  15.     popup.show();  
  16. }  


android.R.layout.simple_dropdown_item_1line.xml のように singleLine や ellipsize を指定しているならわかりますが、そうではないのにどうしてなのか調べてみました。

android.R.layout.simple_dropdown_item_1line
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <!--  
  3. /* //device/apps/common/assets/res/any/layout/simple_spinner_item.xml  
  4. **  
  5. ** Copyright 2008, The Android Open Source Project  
  6. **  
  7. ** Licensed under the Apache License, Version 2.0 (the "License");   
  8. ** you may not use this file except in compliance with the License.   
  9. ** You may obtain a copy of the License at   
  10. **  
  11. **     http://www.apache.org/licenses/LICENSE-2.0   
  12. **  
  13. ** Unless required by applicable law or agreed to in writing, software   
  14. ** distributed under the License is distributed on an "AS IS" BASIS,   
  15. ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   
  16. ** See the License for the specific language governing permissions and   
  17. ** limitations under the License.  
  18. */  
  19. -->  
  20. <TextView xmlns:android="http://schemas.android.com/apk/res/android"   
  21.     android:id="@android:id/text1"  
  22.     style="?android:attr/dropDownItemStyle"  
  23.     android:textAppearance="?android:attr/textAppearanceLargePopupMenu"  
  24.     android:singleLine="true"  
  25.     android:layout_width="match_parent"  
  26.     android:layout_height="?android:attr/listPreferredItemHeight"  
  27.     android:ellipsize="marquee" />  


ListPopupWindow では、内部で持っている PopupWindow に ListView ではなく、ListView を継承した DropDownListView をセットしています。
この DropDownListView で次のように obtainView() を Override して、そこで1行の View が TextView だったら setHorizontallyScrolling(true) をセットしています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/ListPopupWindow.java#1110
  1. 1110     private static class DropDownListView extends ListView {  
  2.   
  3. 1159         /** 
  4. 1160          * <p>Avoids jarring scrolling effect by ensuring that list elements 
  5. 1161          * made of a text view fit on a single line.</p> 
  6. 1162          * 
  7. 1163          * @param position the item index in the list to get a view for 
  8. 1164          * @return the view for the specified item 
  9. 1165          */  
  10. 1166         @Override  
  11. 1167         View obtainView(int position, boolean[] isScrap) {  
  12. 1168             View view = super.obtainView(position, isScrap);  
  13. 1169   
  14. 1170             if (view instanceof TextView) {  
  15. 1171                 ((TextView) view).setHorizontallyScrolling(true);  
  16. 1172             }  
  17. 1173   
  18. 1174             return view;  
  19. 1175         }  


コメントに書いてありますが、「TextView を1行に固定する事でスクロール時にガタガタ動いてしまう状態を避ける」ためだそうです。

setHorizontallyScrolilng() は TextView のメソッドです。TextView の setSingleLine() 内で呼ばれる applySingleLine() から呼ばれていることからもわかるように、文字を1行にするかどうかに関わるメソッドです。

1行の View が TextView のときだけ setHorizontallyScrolilng() を呼ぶようになっているので、次のようになんらかの ViewGroup に入れてしまえば複数行で表示されます。

R.layout.row
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="wrap_content"  
  4.     android:gravity="center_vertical"  
  5.     android:minHeight="?android:attr/listPreferredItemHeightSmall"  
  6.     android:orientation="vertical"  
  7.     android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"  
  8.     android:paddingRight="?android:attr/listPreferredItemPaddingRight" >  
  9.   
  10.     <TextView  
  11.         android:id="@android:id/text1"  
  12.         android:layout_width="match_parent"  
  13.         android:layout_height="wrap_content"  
  14.         android:textAppearance="?android:attr/textAppearanceListItemSmall" />  
  15.   
  16. </LinearLayout>  


  1. protected void showListPopup(View anchor) {  
  2.   
  3.     ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,   
  4.             R.layout.row,   
  5.             android.R.id.text1,  
  6.             new String[] {   
  7.                "test",   
  8.                "test2test2test2test2test2test2test2test2test2",   
  9.                "test3",   
  10.                "test4" });  
  11.   
  12.     ListPopupWindow popup = new ListPopupWindow(this);  
  13.     popup.setAdapter(adapter);  
  14.     popup.setAnchorView(anchor);  
  15.     popup.show();  
  16. }  








2012年10月12日金曜日

Android TextView (EditText) の文字選択処理をカスタマイズする

EditText (もしくは TextView で android:textIsSelectable="true" を指定した場合)に文字列をロングタップして起動する ActionMode をカスタマイズすることができます。

TextView の setCustomSelectionActionModeCallback() で ActionMode.Callback を指定することで、既存のメニューを削除したり、新しいメニューを追加したりすることができます。

  1. EditText editText = (EditText) findViewById(R.id.editText1);  
  2. editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {  
  3.       
  4.     @Override  
  5.     public boolean onPrepareActionMode(ActionMode mode, Menu menu) {  
  6.         // TODO Auto-generated method stub  
  7.         return false;  
  8.     }  
  9.       
  10.     @Override  
  11.     public void onDestroyActionMode(ActionMode mode) {  
  12.         // TODO Auto-generated method stub  
  13.           
  14.     }  
  15.       
  16.     @Override  
  17.     public boolean onCreateActionMode(ActionMode mode, Menu menu) {  
  18.         // TODO Auto-generated method stub  
  19.         return false;  
  20.     }  
  21.       
  22.     @Override  
  23.     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {  
  24.         // TODO Auto-generated method stub  
  25.         return false;  
  26.     }  
  27. });  


setCustomSelectionActionModeCallback() で渡した ActionMode.Callback は TextView の mCustomSelectionActionModeCallback で保持されます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#10075
  1.   350     private Callback mCustomSelectionActionModeCallback;  
  2.   
  3. 10075     public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {  
  4. 10076         mCustomSelectionActionModeCallback = actionModeCallback;  
  5. 10077     }  
  6. 10078   
  7. 10079     /** 
  8. 10080      * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null. 
  9. 10081      * 
  10. 10082      * @return The current custom selection callback. 
  11. 10083      */  
  12. 10084     public ActionMode.Callback getCustomSelectionActionModeCallback() {  
  13. 10085         return mCustomSelectionActionModeCallback;  
  14. 10086     }  


1. ActionMode を起動しない

onCreateActionMode() で false を返すと、ロングタップしても ActionMode が起動しなくなります。

  1. EditText editText = (EditText) findViewById(R.id.editText1);  
  2. editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {  
  3.   
  4.     ...              
  5.       
  6.     @Override  
  7.     public boolean onCreateActionMode(ActionMode mode, Menu menu) {  
  8.         return false;  
  9.     }  
  10. });  
TextView の ActionMode である SelectionActionModeCallback の onCreateActionMode() の中で mCustomSelectionActionModeCallback の onCreateActionMode() を呼び出し、その戻り値が false の場合は false を返すようになっているからです。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#10238
  1.    10182     private class SelectionActionModeCallback implements ActionMode.Callback {  
  2.    10183   
  3.    10184         @Override  
  4.    10185         public boolean onCreateActionMode(ActionMode mode, Menu menu) {  
  5. ...  
  6.    10237   
  7.    10238             if (mCustomSelectionActionModeCallback != null) {  
  8.    10239                 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {  
  9.    10240                     // The custom mode can choose to cancel the action mode  
  10.    10241                     return false;  
  11.    10242                 }  
  12.    10243             }  
  13. ...  
  14.    10251         }  


2. 既存のメニュー項目を削除する

デフォルトのメニュー項目のそれぞれの ID は

SelectAll : android.R.id.selectAll
Cut : android.R.id.cut
Copy : android.R.id.copy;
Paste : android.R.id.paste

です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#9042
  1. 9041     // Selection context mode  
  2. 9042     private static final int ID_SELECT_ALL = android.R.id.selectAll;  
  3. 9043     private static final int ID_CUT = android.R.id.cut;  
  4. 9044     private static final int ID_COPY = android.R.id.copy;  
  5. 9045     private static final int ID_PASTE = android.R.id.paste;  
例えば、Cut と Paste 機能を削除したい場合は removeItem() を使います。
  1. EditText editText = (EditText) findViewById(R.id.editText1);  
  2. editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {  
  3.   
  4.     ...  
  5.            
  6.     @Override  
  7.     public boolean onCreateActionMode(ActionMode mode, Menu menu) {  
  8.           
  9.         menu.removeItem(android.R.id.cut);  
  10.         menu.removeItem(android.R.id.paste);  
  11.           
  12.         return true;  
  13.     }  
  14. });  


3. メニューの機能を置き換える

メニューの項目はそのままで、タップされたときの処理を置き換えるには onActionItemClicked() で true を返します。もともとの処理も行ってほしい場合は false を返します。

例えば、MenuItem の id が android.R.id.selectAll のときに true を返すようにすると、全選択をタップしても何も起こらなくなります。

  1. EditText editText = (EditText) findViewById(R.id.editText1);  
  2. editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {  
  3.   
  4.     ...  
  5.   
  6.     @Override  
  7.     public boolean onActionItemClicked(ActionMode mode, MenuItem item) {  
  8.         int id = item.getItemId();  
  9.         switch(id) {  
  10.             case android.R.id.selectAll:  
  11.                 // 独自の処理  
  12.                 return true;  
  13.         }  
  14.         return false;  
  15.     }  
  16. });  


4. 独自のメニュー項目を追加する

メニュー項目を追加するには onCreateActionMode で Menu.add() を使います。
残念ながら既存のメニュー項目の Order が 0 になっているため、任意の位置に追加することはできないようで、最後の位置に追加されます。さらに、Overflow menu に入ると、展開したときに EditText からフォーカスが外れて ActionMode が終了するという残念なことになります。

もう一つ残念なのが、メニュー項目をタップされたときに ActionMode を終了するための stopSelectionActionMode() というメソッドが private なため外部から呼べません(せめて protected にしてほしい)。
ただし、setText() し直すと選択が解除されるので ActionMode を終了することができます。


選択した文字が全角カナだったら半角カナにして先頭に "シャバドゥビタッチ" *1 をつけるようにしてみました。
(アイコンはがんばってトレースしました。)

R.id.replace は XML で定義しました。追加するメニューの ID は適当な数字ではなく、XML で定義しておくのがいいと思います。More Resource Type - ID

  1. public class MainActivity extends Activity {  
  2.   
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.   
  8.         final EditText editText = (EditText) findViewById(R.id.editText1);  
  9.         editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {  
  10.   
  11.             @Override  
  12.             public boolean onPrepareActionMode(ActionMode mode, Menu menu) {  
  13.                 return true;  
  14.             }  
  15.   
  16.             @Override  
  17.             public void onDestroyActionMode(ActionMode mode) {  
  18.             }  
  19.   
  20.             @Override  
  21.             public boolean onCreateActionMode(ActionMode mode, Menu menu) {  
  22.                 menu.removeItem(android.R.id.paste);  
  23.                 menu.removeItem(android.R.id.cut);  
  24.                 menu.removeItem(android.R.id.copy);  
  25.   
  26.                 MenuItem item = menu.add(Menu.NONE, R.id.replace, Menu.NONE, "Replace");  
  27.                 item.setIcon(R.drawable.ic_replace);  
  28.   
  29.                 return true;  
  30.             }  
  31.   
  32.             @Override  
  33.             public boolean onActionItemClicked(ActionMode mode, MenuItem item) {  
  34.   
  35.                 CharSequence text = editText.getText();  
  36.   
  37.                 int min = 0;  
  38.                 int max = text.length();  
  39.   
  40.                 if (editText.isFocused()) {  
  41.                     final int selStart = editText.getSelectionStart();  
  42.                     final int selEnd = editText.getSelectionEnd();  
  43.   
  44.                     min = Math.max(0, Math.min(selStart, selEnd));  
  45.                     max = Math.max(0, Math.max(selStart, selEnd));  
  46.                 }  
  47.   
  48.                 int id = item.getItemId();  
  49.                 switch (id) {  
  50.                     case R.id.replace:  
  51.                         CharSequence sub = text.subSequence(min, max);  
  52.                         editText.setText(text.subSequence(0, min) + "シャバドゥビタッチ" + convertKanaFull2Half(sub)  
  53.                                 + text.subSequence(max, text.length()));  
  54.                         return true;  
  55.                 }  
  56.                 return false;  
  57.             }  
  58.         });  
  59.     }  
  60.   
  61.     private static final char[] FULL_WIDTH_KANA = { 'ァ''ア''ィ''イ''ゥ''ウ''ェ''エ''ォ''オ''カ''ガ''キ',  
  62.             'ギ''ク''グ''ケ''ゲ''コ''ゴ''サ''ザ''シ''ジ''ス''ズ''セ''ゼ''ソ''ゾ''タ''ダ''チ''ヂ',  
  63.             'ッ''ツ''ヅ''テ''デ''ト''ド''ナ''ニ''ヌ''ネ''ノ''ハ''バ''パ''ヒ''ビ''ピ''フ''ブ''プ',  
  64.             'ヘ''ベ''ペ''ホ''ボ''ポ''マ''ミ''ム''メ''モ''ャ''ヤ''ュ''ユ''ョ''ヨ''ラ''リ''ル''レ',  
  65.             'ロ''ヮ''ワ''ヰ''ヱ''ヲ''ン''ヴ''ヵ''ヶ'};  
  66.   
  67.     private static final String[] HALF_WIDTH_KANA = { "ァ""ア""ィ""イ""ゥ""ウ""ェ""エ""ォ""オ""カ""ガ""キ",  
  68.             "ギ""ク""グ""ケ""ゲ""コ""ゴ""サ""ザ""シ""ジ""ス""ズ""セ""ゼ""ソ""ゾ""タ""ダ",  
  69.             "チ""ヂ""ッ""ツ""ヅ""テ""デ""ト""ド""ナ""ニ""ヌ""ネ""ノ""ハ""バ""パ""ヒ""ビ""ピ",  
  70.             "フ""ブ""プ""ヘ""ベ""ペ""ホ""ボ""ポ""マ""ミ""ム""メ""モ""ャ""ヤ""ュ""ユ""ョ""ヨ",  
  71.             "ラ""リ""ル""レ""ロ""ワ""ワ""イ""エ""ヲ""ン""ヴ""カ""ケ"};  
  72.   
  73.     private static final char FULL_WIDTH_FIRST = FULL_WIDTH_KANA[0];  
  74.     private static final char FULL_WIDTH_LAST = FULL_WIDTH_KANA[FULL_WIDTH_KANA.length - 1];  
  75.   
  76.     public static String convertKanaFull2Half(char c) {  
  77.         if (c >= FULL_WIDTH_FIRST && c <= FULL_WIDTH_LAST) {  
  78.             return HALF_WIDTH_KANA[c - FULL_WIDTH_FIRST];  
  79.         } else if(c == 'ー') {  
  80.             return "-";  
  81.         } else {  
  82.             return String.valueOf(c);  
  83.         }  
  84.     }  
  85.   
  86.     public static String convertKanaFull2Half(CharSequence cs) {  
  87.         StringBuffer sb = new StringBuffer();  
  88.         for (int i = 0; i < cs.length(); i++) {  
  89.             sb.append(convertKanaFull2Half(cs.charAt(i)));  
  90.         }  
  91.         return sb.toString();  
  92.     }  
  93. }  






*1 仮面ライダーウィザードでぐぐってください。





2012年10月11日木曜日

Android TextView で文字列を選択する

Android 3.0 (API Level 11) から文字列選択の API がちょっとかわって、TextView でも(EditText でなくても)文字列選択を簡単に実装できるようになりました。

android:textIsSelectable 属性

もしくは

setTextIsSelectable() メソッド

を使います。

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <TextView  
  7.         android:id="@+id/editText1"  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="This is an Android Phone!"  
  11.         android:textIsSelectable="true" />  
  12.   
  13. </RelativeLayout>  
編集できないので、ActionMode には「全体を選択」と「コピー」しか出てきません。



setTextIsSelectable() の中をみるとこんな感じです。
  1. 4650     public void setTextIsSelectable(boolean selectable) {  
  2. 4651         if (mTextIsSelectable == selectable) return;  
  3. 4652   
  4. 4653         mTextIsSelectable = selectable;  
  5. 4654   
  6. 4655         setFocusableInTouchMode(selectable);  
  7. 4656         setFocusable(selectable);  
  8. 4657         setClickable(selectable);  
  9. 4658         setLongClickable(selectable);  
  10. 4659   
  11. 4660         // mInputType is already EditorInfo.TYPE_NULL and mInput is null;  
  12. 4661   
  13. 4662         setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);  
  14. 4663         setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);  
  15. 4664   
  16. 4665         // Called by setText above, but safer in case of future code changes  
  17. 4666         prepareCursorControllers();  
  18. 4667     }  
Android 3.0 から mTextIsSelectable というフィールドが新しく追加されています。
mTextIsSelectable はデフォルトでは false になっています。もちろん EditText でも false ですが、こちらは別の条件でテキスト選択が開始されます(具体的には editable かどうか)。
結構 2.3 のときと TextView のコード変わってて、なかなか面白いです。



2012年10月10日水曜日

Android コードからテーマの属性値を取得する

テーマに設定されている attribute は ? を使って参照することができます。

例えば、
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:background="?android:attr/selectableItemBackground"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="wrap_content" />  
とすることで、実際の画像リソースIDではなく、テーマの属性値を指定して、その属性値に設定されているリソースを利用することができます。
  1. <style name="Theme">  
  2.     <item name="selectableItemBackground">@android:drawable/item_background</item>  
  3.     ...  
  4. </style>  


このように XML から参照するのは簡単なのですが、コードからアクセスする方法はほとんど書かれていません。

具体的には、Theme クラスの resolveAttribute() メソッドを使います。
  1. TypedValue outValue = new TypedValue();  
  2. context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);  
  3. setBackgroundResource(outValue.resourceId);  
インタラクションさせるカスタム View を作ったときに、コードから View を生成した場合でも、リストの行のようにタップしたときにデフォルトの色変化をさせたい場合に便利です。


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)がいいんでしょうね。