2012年10月30日火曜日

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

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



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

個人的に気に入ったのは


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

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

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


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

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


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


2012年10月29日月曜日

How to create looping ViewPager

ループできる ViewPager です。ちゃんと作ってないので以下の制限があります。
  • ページ数が 3の倍数じゃないといけない(時間がなくて対応してない)
  • 連続ページ切り替え(スワイプ後のスクロールが終わる前にさらにスワイプ)に対応してない
  • わかんないけどなんかあるかも
ポイントは isViewFromObject() を駆使することと、ViewPager を extends して onLayout で位置を戻すことです。 public class MainActivity extends Activity { private ViewPager mViewPager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mPagerAdapter = new MyPagerAdapter(this); mViewPager = (ViewPager) findViewById(R.id.viewpager); mViewPager.setOffscreenPageLimit(2); mViewPager.setAdapter(mPagerAdapter); mViewPager.setOnPageChangeListener(mPagerAdapter); mViewPager.setCurrentItem(1, false); } private MyPagerAdapter mPagerAdapter; private class MyPagerAdapter extends PagerAdapter implements ViewPager.OnPageChangeListener { /** * from 0 to 2 */ int focusedPosition = 0; /** * from 0 to PAGE_NUM -1 */ int contentPosition = 0; /** * number of pages (3x) */ int PAGE_NUM = 9; Context mContext; LayoutInflater mInflater; public MyPagerAdapter(Context context) { mContext = context; mInflater = LayoutInflater.from(context); } @Override public int getCount() { return 3; } SparseArray<View> mViews = new SparseArray<View>(); @Override public Object instantiateItem(ViewGroup container, int position) { int newPosition = calcContentPosition(position); View view = createContent(newPosition); container.addView(view, 0); mViews.put(position, view); view.setTag(position); // return view; return Integer.valueOf(position); } @Override public boolean isViewFromObject(View view, Object object) { int index = (Integer) view.getTag(); int position = (Integer) object; int newPosition = (position + contentPosition) % 3; return newPosition == index; } /** * create content's view * * @param position * from 0 to PAGE_NUM - 1 * @return */ private View createContent(int position) { View view = mInflater.inflate(R.layout.list, null, false); updateContent(view, position); return view; } /** * update content's view * * @param view * @param position * from 0 to PAGE_NUM -1 */ private void updateContent(View view, int position) { ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, android.R.layout.simple_list_item_1, createList(position)); ListView listView = (ListView) view.findViewById(R.id.list); listView.setAdapter(adapter); listView.setScrollingCacheEnabled(false); } private List<String> createList(int index) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 50; i++) { list.add(index + " *************************** " + index + ":" + (i + 1)); } return list; } /** * get position of content * * @param position * from 0 to 2 * @return content position (from 0 to PAGE_NUM - 1) */ private int calcContentPosition(int position) { int offset = position - 1; int newPosition = contentPosition + offset; if (newPosition < 0) { newPosition = PAGE_NUM - 1; } else { newPosition = newPosition % PAGE_NUM; } return newPosition; } @Override public void destroyItem(ViewGroup container, int position, Object object) { int count = mViewPager.getChildCount(); int index = (Integer) object; for (int i = 0; i < count; i++) { View v = mViewPager.getChildAt(i); if (isViewFromObject(v, Integer.valueOf(index))) { container.removeView(v); break; } } } /** * update specified view's content * * @param index * 0 or 2 */ void updateContents(int index) { int count = mViewPager.getChildCount(); for (int i = 0; i < count; i++) { View v = mViewPager.getChildAt(i); if (isViewFromObject(v, Integer.valueOf(index))) { final int newPosition = calcContentPosition(index); updateContent(v, newPosition); break; } } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { focusedPosition = position; } @Override public void onPageScrollStateChanged(int state) { if (state == ViewPager.SCROLL_STATE_IDLE) { handlePageScrollStateChangedToIdle(); } } private void handlePageScrollStateChangedToIdle() { switch (focusedPosition) { case 0: // scroll to previous page if ((--contentPosition) < 0) { contentPosition = PAGE_NUM - 1; } updateContents(0); break; case 2: // scroll to next page contentPosition = (++contentPosition) % PAGE_NUM; updateContents(2); break; default: break; } } } } public class MyViewPager extends ViewPager { public MyViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if(getCurrentItem() != 1) { setCurrentItem(1, false); } } } res/layout/main.xml <?xml version="1.0" encoding="utf-8"?> <com.sample.viewpager.MyViewPager xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/viewpager" android:layout_width="match_parent" android:layout_height="match_parent" /> res/layout/list.xml <?xml version="1.0" encoding="utf-8"?> <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list" android:layout_width="match_parent" android:layout_height="match_parent" 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
12047 public void getLocationInWindow(int[] location) { 12048 if (location == null || location.length < 2) { 12049 throw new IllegalArgumentException("location must be an array of " 12050 + "two integers"); 12051 } 12052 12053 location[0] = mLeft; 12054 location[1] = mTop; 12055 if (mTransformationInfo != null) { 12056 location[0] += (int) (mTransformationInfo.mTranslationX + 0.5f); 12057 location[1] += (int) (mTransformationInfo.mTranslationY + 0.5f); 12058 } 12059 12060 ViewParent viewParent = mParent; 12061 while (viewParent instanceof View) { 12062 final View view = (View)viewParent; 12063 location[0] += view.mLeft - view.mScrollX; 12064 location[1] += view.mTop - view.mScrollY; 12065 if (view.mTransformationInfo != null) { 12066 location[0] += (int) (view.mTransformationInfo.mTranslationX + 0.5f); 12067 location[1] += (int) (view.mTransformationInfo.mTranslationY + 0.5f); 12068 } 12069 viewParent = view.mParent; 12070 } 12071 12072 if (viewParent instanceof ViewRootImpl) { 12073 // *cough* 12074 final ViewRootImpl vr = (ViewRootImpl)viewParent; 12075 location[1] -= vr.mCurScrollY; 12076 } 12077 } mTransformationInfo は変形(Rotate, Scale, Translation など)の情報を保持するためのフィールドです。translationX や translationY がセットされていればその分もカウントするということです。

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



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

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

v.setTranslationX(100); final int[] anchorPos = new int[2]; v.getLocationOnScreen(anchorPos); Log.d(TAG, "position : " + v.getLeft() + ", " + v.getTop()); 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
12030 public void getLocationOnScreen(int[] location) { 12031 getLocationInWindow(location); 12032 12033 final AttachInfo info = mAttachInfo; 12034 if (info != null) { 12035 location[0] += info.mWindowLeft; 12036 location[1] += info.mWindowTop; 12037 } 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
5825 public void getWindowVisibleDisplayFrame(Rect outRect) { 5826 if (mAttachInfo != null) { 5827 try { 5828 mAttachInfo.mSession.getDisplayFrame(mAttachInfo.mWindow, outRect); 5829 } catch (RemoteException e) { 5830 return; 5831 } 5832 // XXX This is really broken, and probably all needs to be done 5833 // in the window manager, and we need to know more about whether 5834 // we want the area behind or in front of the IME. 5835 final Rect insets = mAttachInfo.mVisibleInsets; 5836 outRect.left += insets.left; 5837 outRect.top += insets.top; 5838 outRect.right -= insets.right; 5839 outRect.bottom -= insets.bottom; 5840 return; 5841 } 5842 Display d = WindowManagerImpl.getDefault().getDefaultDisplay(); 5843 d.getRectSize(outRect); 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 とは異なるからです。

Toast toast = Toast.makeText(MainActivity.this, "Hello World", Toast.LENGTH_SHORT); toast.setGravity(Gravity.TOP, 0, 0); toast.show(); 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
105 private String mSharedPreferencesName; 141 PreferenceManager(Activity activity, int firstRequestCode) { 142 mActivity = activity; 143 mNextRequestCode = firstRequestCode; 144 145 init(activity); 146 } 160 private void init(Context context) { 161 mContext = context; 162 163 setSharedPreferencesName(getDefaultSharedPreferencesName(context)); 164 } 302 public String getSharedPreferencesName() { 303 return mSharedPreferencesName; 304 } 313 public void setSharedPreferencesName(String sharedPreferencesName) { 314 mSharedPreferencesName = sharedPreferencesName; 315 mSharedPreferences = null; 316 } 365 public static SharedPreferences getDefaultSharedPreferences(Context context) { 366 return context.getSharedPreferences(getDefaultSharedPreferencesName(context), 367 getDefaultSharedPreferencesMode()); 368 } 370 private static String getDefaultSharedPreferencesName(Context context) { 371 return context.getPackageName() + "_preferences"; 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
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2006 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceListItemSmall" android:gravity="center_vertical" android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" android:paddingRight="?android:attr/listPreferredItemPaddingRight" android:minHeight="?android:attr/listPreferredItemHeightSmall" />

protected void showListPopup(View anchor) { ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, new String[] { "test", "test2test2test2test2test2test2test2test2test2", "test3", "test4" }); ListPopupWindow popup = new ListPopupWindow(this); popup.setAdapter(adapter); popup.setAnchorView(anchor); popup.show(); }

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

android.R.layout.simple_dropdown_item_1line
<?xml version="1.0" encoding="utf-8"?> <!-- /* //device/apps/common/assets/res/any/layout/simple_spinner_item.xml ** ** Copyright 2008, The Android Open Source Project ** ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** ** http://www.apache.org/licenses/LICENSE-2.0 ** ** Unless required by applicable law or agreed to in writing, software ** distributed under the License is distributed on an "AS IS" BASIS, ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** See the License for the specific language governing permissions and ** limitations under the License. */ --> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" style="?android:attr/dropDownItemStyle" android:textAppearance="?android:attr/textAppearanceLargePopupMenu" android:singleLine="true" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeight" 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
1110 private static class DropDownListView extends ListView { ... 1159 /** 1160 * <p>Avoids jarring scrolling effect by ensuring that list elements 1161 * made of a text view fit on a single line.</p> 1162 * 1163 * @param position the item index in the list to get a view for 1164 * @return the view for the specified item 1165 */ 1166 @Override 1167 View obtainView(int position, boolean[] isScrap) { 1168 View view = super.obtainView(position, isScrap); 1169 1170 if (view instanceof TextView) { 1171 ((TextView) view).setHorizontallyScrolling(true); 1172 } 1173 1174 return view; 1175 }

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

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

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

R.layout.row
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeightSmall" android:orientation="vertical" android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" android:paddingRight="?android:attr/listPreferredItemPaddingRight" > <TextView android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceListItemSmall" /> </LinearLayout>

protected void showListPopup(View anchor) { ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.row, android.R.id.text1, new String[] { "test", "test2test2test2test2test2test2test2test2test2", "test3", "test4" }); ListPopupWindow popup = new ListPopupWindow(this); popup.setAdapter(adapter); popup.setAnchorView(anchor); popup.show(); }







2012年10月12日金曜日

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

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

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

EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } @Override public void onDestroyActionMode(ActionMode mode) { // TODO Auto-generated method stub } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } });

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
350 private Callback mCustomSelectionActionModeCallback; ... 10075 public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) { 10076 mCustomSelectionActionModeCallback = actionModeCallback; 10077 } 10078 10079 /** 10080 * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null. 10081 * 10082 * @return The current custom selection callback. 10083 */ 10084 public ActionMode.Callback getCustomSelectionActionModeCallback() { 10085 return mCustomSelectionActionModeCallback; 10086 }

1. ActionMode を起動しない

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

EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { ... @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { return false; } }); 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
10182 private class SelectionActionModeCallback implements ActionMode.Callback { 10183 10184 @Override 10185 public boolean onCreateActionMode(ActionMode mode, Menu menu) { ... 10237 10238 if (mCustomSelectionActionModeCallback != null) { 10239 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) { 10240 // The custom mode can choose to cancel the action mode 10241 return false; 10242 } 10243 } ... 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 9041 // Selection context mode 9042 private static final int ID_SELECT_ALL = android.R.id.selectAll; 9043 private static final int ID_CUT = android.R.id.cut; 9044 private static final int ID_COPY = android.R.id.copy; 9045 private static final int ID_PASTE = android.R.id.paste; 例えば、Cut と Paste 機能を削除したい場合は removeItem() を使います。 EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { ... @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { menu.removeItem(android.R.id.cut); menu.removeItem(android.R.id.paste); return true; } });

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

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

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

EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { ... @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { int id = item.getItemId(); switch(id) { case android.R.id.selectAll: // 独自の処理 return true; } return false; } });

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

public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText editText = (EditText) findViewById(R.id.editText1); editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return true; } @Override public void onDestroyActionMode(ActionMode mode) { } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { menu.removeItem(android.R.id.paste); menu.removeItem(android.R.id.cut); menu.removeItem(android.R.id.copy); MenuItem item = menu.add(Menu.NONE, R.id.replace, Menu.NONE, "Replace"); item.setIcon(R.drawable.ic_replace); return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { CharSequence text = editText.getText(); int min = 0; int max = text.length(); if (editText.isFocused()) { final int selStart = editText.getSelectionStart(); final int selEnd = editText.getSelectionEnd(); min = Math.max(0, Math.min(selStart, selEnd)); max = Math.max(0, Math.max(selStart, selEnd)); } int id = item.getItemId(); switch (id) { case R.id.replace: CharSequence sub = text.subSequence(min, max); editText.setText(text.subSequence(0, min) + "シャバドゥビタッチ" + convertKanaFull2Half(sub) + text.subSequence(max, text.length())); return true; } return false; } }); } private static final char[] FULL_WIDTH_KANA = { 'ァ', 'ア', 'ィ', 'イ', 'ゥ', 'ウ', 'ェ', 'エ', 'ォ', 'オ', 'カ', 'ガ', 'キ', 'ギ', 'ク', 'グ', 'ケ', 'ゲ', 'コ', 'ゴ', 'サ', 'ザ', 'シ', 'ジ', 'ス', 'ズ', 'セ', 'ゼ', 'ソ', 'ゾ', 'タ', 'ダ', 'チ', 'ヂ', 'ッ', 'ツ', 'ヅ', 'テ', 'デ', 'ト', 'ド', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'バ', 'パ', 'ヒ', 'ビ', 'ピ', 'フ', 'ブ', 'プ', 'ヘ', 'ベ', 'ペ', 'ホ', 'ボ', 'ポ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ャ', 'ヤ', 'ュ', 'ユ', 'ョ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ヮ', 'ワ', 'ヰ', 'ヱ', 'ヲ', 'ン', 'ヴ', 'ヵ', 'ヶ'}; private static final String[] HALF_WIDTH_KANA = { "ァ", "ア", "ィ", "イ", "ゥ", "ウ", "ェ", "エ", "ォ", "オ", "カ", "ガ", "キ", "ギ", "ク", "グ", "ケ", "ゲ", "コ", "ゴ", "サ", "ザ", "シ", "ジ", "ス", "ズ", "セ", "ゼ", "ソ", "ゾ", "タ", "ダ", "チ", "ヂ", "ッ", "ツ", "ヅ", "テ", "デ", "ト", "ド", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "バ", "パ", "ヒ", "ビ", "ピ", "フ", "ブ", "プ", "ヘ", "ベ", "ペ", "ホ", "ボ", "ポ", "マ", "ミ", "ム", "メ", "モ", "ャ", "ヤ", "ュ", "ユ", "ョ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ワ", "イ", "エ", "ヲ", "ン", "ヴ", "カ", "ケ"}; private static final char FULL_WIDTH_FIRST = FULL_WIDTH_KANA[0]; private static final char FULL_WIDTH_LAST = FULL_WIDTH_KANA[FULL_WIDTH_KANA.length - 1]; public static String convertKanaFull2Half(char c) { if (c >= FULL_WIDTH_FIRST && c <= FULL_WIDTH_LAST) { return HALF_WIDTH_KANA[c - FULL_WIDTH_FIRST]; } else if(c == 'ー') { return "-"; } else { return String.valueOf(c); } } public static String convertKanaFull2Half(CharSequence cs) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < cs.length(); i++) { sb.append(convertKanaFull2Half(cs.charAt(i))); } return sb.toString(); } }





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





2012年10月11日木曜日

Android TextView で文字列を選択する

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

android:textIsSelectable 属性

もしくは

setTextIsSelectable() メソッド

を使います。

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



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



2012年10月10日水曜日

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

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

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

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

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


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