2014年11月18日火曜日

Android Enter キーでフォーカスを移動するのは android:nextFocusDown ? android:nextFocusForward ?

singleLine の EditText で、Enter キーが押されたときに次のフィールドにフォーカスが移動すると使いやすいですよね。
ある程度は勝手にフォーカスが移動するようになるのですが、細かい部分は自分で指定しないと思い通りに移動してくれません。

例えば、次のように EditText が LinearLayout で縦に並んでいる場合、edit_text1 で Enter キーを押すと、edit_text2 へ移動してくれます。 <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <EditText android:id="@+id/edit_text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> <EditText android:id="@+id/edit_text2" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> <EditText android:id="@+id/edit_text3" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> </LinearLayout>

一方、次のように、edit_text1 と edit_text2 が横に並び、その下に edit_text3 がある場合、edit_text1 で Enter キーを押すと edit_text3 に移動します。 <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android:id="@+id/edit_text1" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="text" /> <EditText android:id="@+id/edit_text2" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="text" /> </LinearLayout> <EditText android:id="@+id/edit_text3" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> </LinearLayout>

edit_text1 で Enter キーを押したときに edit_text2 に移動させるには、方法が2つあります。

1. android:nextFocusDown を指定する <EditText android:id="@+id/edit_text1" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="text" android:nextFocusDown="@+id/edit_text2" />

2. android:nextFocusForward と android:imeOptions を指定する <EditText android:id="@+id/edit_text1" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="text" android:nextFocusForward="@+id/edit_text2" android:imeOptions="actionNext" />

次へ、なんだから android:nextFocusForward だろうと思いきや、こちらは android:imeOptions(他にも android:imeActionLabel とか android:imeActionId などの指定でも可)を指定するという workaround が必要になります。バグじゃないかと思うんだけど。。。
ちなみに、Tab を押したときには android:nextFocusForward で指定された id の View に移動します。


解説

キモのメソッドは TextView.onCreateInputConnection() と TextView.onEditorAction() です。
4.x はそれぞれ微妙にコードが違うのですが、4.4.2 (API Level 19)のコードで説明します。

ちょっと長いですが、android:singleLine="true" や android:inputType="text" が指定されている EditText では、focusSearch(FOCUS_DOWN) で次に移動できる View を探します(ここでは FOCUS_DOWN なのよね...)。

移動できる View があって、IME_ACTION が明示的に指定されていない場合、IME_ACTION として EditorInfo.IME_ACTION_NEXT が指定されます。 @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (onCheckIsTextEditor() && isEnabled()) { mEditor.createInputMethodStateIfNeeded(); outAttrs.inputType = getInputType(); if (mEditor.mInputContentType != null) { outAttrs.imeOptions = mEditor.mInputContentType.imeOptions; outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions; outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel; outAttrs.actionId = mEditor.mInputContentType.imeActionId; outAttrs.extras = mEditor.mInputContentType.extras; } else { outAttrs.imeOptions = EditorInfo.IME_NULL; } if (focusSearch(FOCUS_DOWN) != null) { outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT; } if (focusSearch(FOCUS_UP) != null) { outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; } if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION) == EditorInfo.IME_ACTION_UNSPECIFIED) { if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) { // An action has not been set, but the enter key will move to // the next focus, so set the action to that. outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; } else { // An action has not been set, and there is no focus to move // to, so let's just supply a "done" action. outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE; } if (!shouldAdvanceFocusOnEnter()) { outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; } } if (isMultilineInputType(outAttrs.inputType)) { // Multi-line text editors should always show an enter key. outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION; } outAttrs.hintText = mHint; if (mText instanceof Editable) { InputConnection ic = new EditableInputConnection(this); outAttrs.initialSelStart = getSelectionStart(); outAttrs.initialSelEnd = getSelectionEnd(); outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType()); return ic; } } return null; }

次に onEditorAction() です。

ict という変数の null チェックをしているのがポイントです。この変数が null じゃない場合、まず EditorActionListener の処理をします。
EditorActionListener がセットされているかをチェックし、セットされている場合は EditorActionListener.onEditorAction() を呼び、戻り値が true ならそこで処理を終了します。

EditorActionListener がセットされていない、またはセットされていても戻り値が false の場合は、actionCode に応じた処理をします。
actionCode が IME_ACTION_NEXT の場合 focusSearch(FOCUS_FORWARD) で View を探して、対応する View があればそれに対して requestFocus() を呼びます(ここは FOCUS_FORWARD)。
IME_ACTION_DONE の場合はキーボードを閉じます。

ict が null のときや、上記に対応する View がない場合などは、その下の処理が行われます。つまり、KeyEvent.KEYCODE_ENTER の KeyEvent.ACTION_DOWN と KeyEvent.ACTION_UP を ViewRootImpl.dispatchKeyFromIme() で割り当てます。
これにより、onKeyDown() と onKeyUp() が呼ばれることになります。

ict が null かどうかによらず actionCode に対応した処理部分を呼ぶようにするべきなんじゃないかなと思いました。。。 public void onEditorAction(int actionCode) { final Editor.InputContentType ict = mEditor == null ? null : mEditor.mInputContentType; if (ict != null) { if (ict.onEditorActionListener != null) { if (ict.onEditorActionListener.onEditorAction(this, actionCode, null)) { return; } } // This is the handling for some default action. // Note that for backwards compatibility we don't do this // default handling if explicit ime options have not been given, // instead turning this into the normal enter key codes that an // app may be expecting. if (actionCode == EditorInfo.IME_ACTION_NEXT) { View v = focusSearch(FOCUS_FORWARD); if (v != null) { if (!v.requestFocus(FOCUS_FORWARD)) { throw new IllegalStateException("focus search returned a view " + "that wasn't able to take focus!"); } } return; } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) { View v = focusSearch(FOCUS_BACKWARD); if (v != null) { if (!v.requestFocus(FOCUS_BACKWARD)) { throw new IllegalStateException("focus search returned a view " + "that wasn't able to take focus!"); } } return; } else if (actionCode == EditorInfo.IME_ACTION_DONE) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && imm.isActive(this)) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } return; } } ViewRootImpl viewRootImpl = getViewRootImpl(); if (viewRootImpl != null) { long eventTime = SystemClock.uptimeMillis(); viewRootImpl.dispatchKeyFromIme( new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION)); viewRootImpl.dispatchKeyFromIme( new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION)); } }

onKeyUp() での KeyEvent.KEYCODE_ENTER の処理は onEditorAction() と同じような感じなのですが、focusSearch() & requestFocus() する対象が FOCUS_DOWN です(こっちは FOCUS_DOWN !) @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (!isEnabled()) { return super.onKeyUp(keyCode, event); } if (!KeyEvent.isModifierKey(keyCode)) { mPreventDefaultMovement = false; } switch (keyCode) { ... case KeyEvent.KEYCODE_ENTER: if (event.hasNoModifiers()) { if (mEditor != null && mEditor.mInputContentType != null && mEditor.mInputContentType.onEditorActionListener != null && mEditor.mInputContentType.enterDown) { mEditor.mInputContentType.enterDown = false; if (mEditor.mInputContentType.onEditorActionListener.onEditorAction( this, EditorInfo.IME_NULL, event)) { return true; } } if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0 || shouldAdvanceFocusOnEnter()) { /* * If there is a click listener, just call through to * super, which will invoke it. * * If there isn't a click listener, try to advance focus, * but still call through to super, which will reset the * pressed state and longpress state. (It will also * call performClick(), but that won't do anything in * this case.) */ if (!hasOnClickListeners()) { View v = focusSearch(FOCUS_DOWN); if (v != null) { if (!v.requestFocus(FOCUS_DOWN)) { throw new IllegalStateException( "focus search returned a view " + "that wasn't able to take focus!"); } /* * Return true because we handled the key; super * will return false because there was no click * listener. */ super.onKeyUp(keyCode, event); return true; } else if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0) { // No target for next focus, but make sure the IME // if this came from it. InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && imm.isActive(this)) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } } } } return super.onKeyUp(keyCode, event); } break; } ... }

つまり、ict が null になってしまうときは android:nextFocusDown に指定したものに、null にならないときは android:nextFocusForward に指定したものに移動します。
ややこし。。。
明示的に android:imeOptions="actionNext" を指定するのは、この ict が null にならないようにするためです。 (ict が null にならないようにする方法は他にもあって、android:imeActionLabel を指定するとかいくつかあります。)


2014年11月17日月曜日

Android CheckBox 画像の正しいカスタマイズ方法

各テーマでのチェックボックスのスタイルは android:checkboxStyle で指定されています。
以下は 4.0.3 のコードですが、この部分は 3.0 で Theme.Holo が追加されて以降特に変わっていません。

http://tools.oesf.biz/android-4.0.3_r1.0/xref/frameworks/base/core/res/res/values/themes.xml ... ... ...

Widget.Holo.CompoundButton.CheckBox と Widget.Holo.Light.CompoundButton.CheckBox を見ると、Widget.CompoundButton.CheckBox をそのまま継承しているだけです。

http://tools.oesf.biz/android-4.0.3_r1.0/xref/frameworks/base/core/res/res/values/styles.xml#1003 つまり、Theme であろうが Theme.Holo であろうが Widget.CompoundButton.CheckBox が使われるということです。


このスタイルですが、4.1 と 4.2 で微妙に変わります。

4.1まで

http://tools.oesf.biz/android-4.1.2_r1.0/xref/frameworks/base/core/res/res/values/styles.xml#345
4.2以降

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/res/res/values/styles.xml#345 4.1 までは Widget.CompoundButton.CheckBox で android:background が指定されていますが、4.2 以降ではなくなっています。


実は 4.2 で paddingLeft の使い方が変わりました。Rtl に対応するためだと思われます。

4.1 までは左端からテキストの間が paddingLeft でした。そのため、チェックボックス画像分の paddingLeft を指定するために、背景画像が使われていました。
一方、4.2 からはチェックボックス画像とテキストの間が paddingLeft になりました。



チェックボックスをカスタマイズする場合

この違いのため、チェックボックスをカスタマイズする場合は注意が必要です。

例えばチェックボックス画像を 32 x 32 dp で作成し、画像とテキストの間を 4dp あけたいとしたら、次のように values/dimens.xml と values-v17/dimens.xml を用意する必要があります。

values/dimens.xml 36dp values-v17/dimens.xml 4dp


チェックボックスの画像を変えるには、checkboxStyle に指定するスタイルで android:button に drawable を指定するか、テーマで android:listChoiceIndicatorMultiple に drawable を指定します。

values/styles.xml or




おまけ : CompoundButton.java の変更について

onDraw() については、Rtl のとき右端に描画されるようになっているだけで、paddingLeft の扱いは変わっていません。

4.1.2

http://tools.oesf.biz/android-4.1.2_r1.0/xref/frameworks/base/core/java/android/widget/CompoundButton.java#228 227 @Override 228 protected void onDraw(Canvas canvas) { 229 super.onDraw(canvas); 230 231 final Drawable buttonDrawable = mButtonDrawable; 232 if (buttonDrawable != null) { 233 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 234 final int height = buttonDrawable.getIntrinsicHeight(); 235 236 int y = 0; 237 238 switch (verticalGravity) { 239 case Gravity.BOTTOM: 240 y = getHeight() - height; 241 break; 242 case Gravity.CENTER_VERTICAL: 243 y = (getHeight() - height) / 2; 244 break; 245 } 246 247 buttonDrawable.setBounds(0, y, buttonDrawable.getIntrinsicWidth(), y + height); 248 buttonDrawable.draw(canvas); 249 } 250 }
4.2.0

http://tools.oesf.biz/android-4.2.0_r1.0/xref/frameworks/base/core/java/android/widget/CompoundButton.java#252 251 @Override 252 protected void onDraw(Canvas canvas) { 253 super.onDraw(canvas); 254 255 final Drawable buttonDrawable = mButtonDrawable; 256 if (buttonDrawable != null) { 257 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 258 final int drawableHeight = buttonDrawable.getIntrinsicHeight(); 259 final int drawableWidth = buttonDrawable.getIntrinsicWidth(); 260 261 int top = 0; 262 switch (verticalGravity) { 263 case Gravity.BOTTOM: 264 top = getHeight() - drawableHeight; 265 break; 266 case Gravity.CENTER_VERTICAL: 267 top = (getHeight() - drawableHeight) / 2; 268 break; 269 } 270 int bottom = top + drawableHeight; 271 int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; 272 int right = isLayoutRtl() ? getWidth() : drawableWidth; 273 274 buttonDrawable.setBounds(left, top, right, bottom); 275 buttonDrawable.draw(canvas); 276 } 277 } 4.2 で padding の扱いが変わった理由は、getCompoundPaddingLeft() と getCompoundPaddingRight() を Override して padding を上書きするようになったからです。 227 @Override 228 public int getCompoundPaddingLeft() { 229 int padding = super.getCompoundPaddingLeft(); 230 if (!isLayoutRtl()) { 231 final Drawable buttonDrawable = mButtonDrawable; 232 if (buttonDrawable != null) { 233 padding += buttonDrawable.getIntrinsicWidth(); 234 } 235 } 236 return padding; 237 } 238 239 @Override 240 public int getCompoundPaddingRight() { 241 int padding = super.getCompoundPaddingRight(); 242 if (isLayoutRtl()) { 243 final Drawable buttonDrawable = mButtonDrawable; 244 if (buttonDrawable != null) { 245 padding += buttonDrawable.getIntrinsicWidth(); 246 } 247 } 248 return padding; 249 } このメソッドは TextView の onMeasure() などから呼ばれています。


2014年11月3日月曜日

Android Studio でよく使うコマンド

command + o : クラス検索で開く
command + shif + o : ファイル名検索で開く
command + e : Recent Files
command + n : Generate...
option + F7 : find usage
option + command + l : 整形
option + command + v : 左辺を挿入
ctrl + option + o : optimize imports
shift + command + @ : 左のタブに移動
shift + command + [ : 右のタブに移動
command + l : Go to Line
command + shift + delete : 前回の編集位置に飛ぶ