複数あるEditTextの入力時のソフトキーボード制御
Androidにて同一のActivity内に複数のEditTextがあった場合、
ひとつのEditTextに入力をすると次のEditTextにフォーカスが移って連続して入力が始まる。
それを便利ととるかは人次第だけど、個人的にひとつ入力を終えた時点で
次の入力のためのソフトキーボードが立ち上がっていると画面の変化が見えなくてうっとおしい。
特にEditTextの入力によって動的な変化があった場合は尚更…
そこで最初はレイアウトのXML側でImeOptionsを設定して対処していた。
<EditText android:id="@+id/editText" android:layout_width="0dp" android:layout_height="wrap_content" android:inputText="number" android:imeOptions="actionDone" >
ただ、(特に数値)入力をした後に動的な変化がある場合、
EditTextが空なときなどに問題が発生することがある。
そのため、入力の完了時にEditTextが空の場合に指定の入力をセットするようにしたかった。
そこでキーリスナーを利用して対処しようとしたけど…
ImeOptionsにactionDoneを適用しているとOnKeyListenerのKeyEvent.KEYCODE_ENTERで拾ってもらえなくなる…
そこで泣く泣くImeOptionsを消して、他のEditTextをsetFocusableなどで誤魔化しつつ
EditText edit = (EditText)findViewById(R.id.editText); edit.setOnKeyListener(new OnKeyListener(){ @Override public boolean onKey(View v, int keyCode, KeyEvent event) { //エンターが押されたとき(入力確定時) if(event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER){ //EditTextの中身が空だった場合に0を入れる if(edit.getText.toString().equals("")){ edit.setText("0"); edit.setSelection(edit.getText().toString.length()); } } //入力中に(ハードキーの)バックキーが押されたとき if(keyCode == KeyEvent.KEYCODE_BACK){ //EditTextの中身が空だった場合に0を入れる if(edit.getText.toString().equals("")){ edit.setText("0"); edit.setSelection(edit.getText().toString.length()); } } return false; } });
とりえずこれでなんとか入力問題はすっきりしたけど、結局ソフトキーボードが連続して立ち上がる問題が振り戻しに…
キーリスナーやらimeOptionsやら弄り回したけど両問題を一括して解決できない。
やっぱりACTION_DONEが使いたい…!
それをOnEditActionListenerが解決してくれた。
<EditText android:id="@+id/editText" android:layout_width="0dp" android:layout_height="wrap_content" android:inputText="number" android:imeOptions="actionDone" >
imeOptionsをactionDoneに戻して
EditText edit = (EditText)findViewById(R.id.editText); //アクションリスナー edit.setOnEditActionListener(new OnEditActionListener(){ @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { //ActionDoneの場合 if(actionId == EditorInfo.IME_ACTION_DONE){ //EditTextの中身が空だった場合に0を入れる if(edit.getText.toString().equals("")){ edit.setText("0"); edit.setSelection(edit.getText().toString.length()); } } return false; }); //キーリスナー edit.setOnKeyListener(new OnKeyListener(){ @Override public boolean onKey(View v, int keyCode, KeyEvent event) { //入力中に(ハードキーの)バックキーが押されたとき if(keyCode == KeyEvent.KEYCODE_BACK){ //EditTextの中身が空だった場合に0を入れる if(edit.getText.toString().equals("")){ edit.setText("0"); edit.setSelection(edit.getText().toString.length()); } } return false; } });
アクションリスナーでActionDoneの場合の入力確定時操作を、キーリスナーでバックキー操作をフォローできるようになりました。
これで変にフォーカスで四苦八苦せずにソフトキーボードが鬱陶しくなく入力できるようになった。
にしてもはてなダイアリーのソース記載はインデントどうにかならないかな…
VLC media playerでのスキン変更を手動リセット
作業中に映像を流しておきたくてVLC media playerをサイズ縮小して使うことがある。
ただデフォルトスキンのままだとサイズを縮小しても横幅のサイズは変わらず左右に黒の余白が残る。
アスペクト比を弄れば黒の余白はなくなって見易いが作業中に横目で見るサイズにしては邪魔。
なのでスキン変更でどうにか対処しようとした。
BlackMac
http://d1ckies.blogspot.jp/2009/12/blackmac-vlc.html
ShiftieVLC
http://homelessbrian.deviantart.com/art/ShiftieVLC-98142034
このへんがちょうどよかった。
ただ、色々弄っている内にスキンとの相性やトラブルで起動しているにも関わらず、
プレイヤー自体が表示されず元に戻すこともできなくなった。
となると手動で書き換えてリセットするしかないので設定ファイルを探した。
C:\\Documents and Settings\\ユーザ名\\Application Data\\vlc内にある
vlcrcファイルをテキストツールで開くと自分の環境では1500行過ぎにスキン関連が記載されていた。
隠しファイル下を覗かなければいけないので、表示をONにしておく。
# Skin to use (文字列) skins2-last=C:\Program Files\***
あとは***部分をデフォルトスキンなり使用可能なスキンのパスすれば変更できる。
ファイル入出力時例外 Java.illegalArgumentException contains a path separator
ファイル入出力時にopenFileOutputもしくはopenFileInputを利用した場合、
これらはパスを指定できないので指定しようとすると上記エラーが発生。
おとなしくFileInputStreamもしくはFileOutputStreamを使ってやればいい。
FileOutputStream fos = null; BufferedWriter bw = null; try{ /*パス指定ができないため例外発生 fos = this.openFileOutput("/sdcard/xxx.txt", mode); */ fos = new FileOutputStream(new File("/sdcard/xxx.txt"); bw = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); 処理 }catch(Exception){ }finally{ try{ fos.close(); bw.close(); }catch(IOException ioe){ }
あと、SDカード下の入出力時はマニュフェストファイルに以下を追加
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
AndroidでJava7はまだ推奨されないのかな?
try(FileOutputStream fos = new FileOutputStream(new File("sdcard/sort_log.txt")); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"))){ }
コード量が違いすぎる…
あと、BasicFileAttributes使ってファイルの作成日時を楽に取得したい
Androidのソースのインストール&添付とNo repository found containingの対処
バックキーを押したときの動作はonKeyDownもしくはdispatchKeyEventを使用すればいいとのこと。
バックキーを押すと現在実行中のActivityを終了するので実行中のActivityがひとつだけなら
アプリ自体が終了(もしくは最小化)させると思う。
今回は、複数のActivityが実行中(といってもonStop()してるはず)でひとつ前のActivityへ。
そして、データをそのActivityへ渡したい…ので、その辺のソースを見たい。
Android SDKは宣言を開こうとすると「ソースが見つからないから添付してくれ」と言われる。
とりあえずで放置していたので、そろそろ諦め時かとソースを探す。
Android 4.x以降ならAndroid SDK ManagerでSourceをインストールできるけど、それ以前のは手間がかかる。
遠回りで色々大変そうで、どうしようかと思ったらAndroid Sourcesというeclipseプラグインがあるみたい。
情報が新しいものばかりだけど最近できたものなのだろうか?
とりあえず「ヘルプ」-「新規ソフトウェアのインストール」で
http://adt-addons.googlecode.com/svn/trunk/source/com.android.ide.eclipse.source.update/
を対象にインストール
しようとしたら、「No repository found containing」でインストールできない…
「ウィンドウ」-「設定」-「インストール/更新」-「使用可能なソフトウェア〜」
項目全部を選択して、「bookmark」としてエクスポートしてから除去。
念のためeclipseを再起動してから同設定まで戻って、今度はさっきのbookmarkをインポート。
これでもう一回「新規ソフトウェアのインストール」を行ったら問題なくインストールできた。
プロジェクトからビルドパスで対象に設定。
ListViewでAdapterをカスタマイズしてgetChildAtでのnull回避
AndroidのListViewはAdapterを介してコレクションを表示する。
ただ、ListViewは画面内に表示できる数の行を作成してAdapterに登録している情報を表示しているだけ。
Adapterがその行であるViewの情報を入れ替えてスクロールに対応している。
その使いまわしに気をつけた実装をしなければいけないのはAdapter#getView関連で調べると出てくる。
ユーザーから見てListViewは固定された透明な外箱の画面を通して、
その先にAdapterというスクロール可能な内箱が並べてあり、
各内箱の中に情報が入っているような感じのイメージで理解している。
前回の項目選択時の背景色変更に加えてボタン操作で項目の入れ替えをしようと画策。
そのため、項目と言うかListViewの子要素(View)を取得したいのだけど、
上記理由からListVew#getChildAtで取得しようにも初期表示の子要素は取得できるが、
スクロール後の子要素はnullになってしまい取得できない。
ようはButtonでのonClick(View view)の利用などListView外部からの動的変更のために
ListViewの子要素(View)を取得したい。
雑なお試し
public class sample extends Activity{ 省略 int position = -1; ArrayAdapter<String> adapter; ListView list; 省略 adapter = new ArrayAdapter<String>(this, R.layout.aaa, bbb); list = (ListView)this.findViewById(R.id.xxx); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickeListener(){ @Override public void onItemClick((AdapterView<?> adapterView, View view, int position, long id){ setPosition(position); } }); Button button = (Button)this.findViewById(R.id.zzz); button.setOnClickListener(new OnClickListener(){ @Override public void onClick(View view){ /*クリックした要素の前後2要素までを対象*/ int count = -2; /*getChildAtの場合*/ for(int i = 0;i < 5;i++){ System.out.println(count + ": getChildAt : " +list.getChildAt(position + count)); count++; } /*viewの場合*/ System.out.println(count + ": view : " + view); } }); 省略 public void setPosition(int position){ this.position = position; } public int getPosition(){ return position; } 省略 }
実行結果
初期表示のままクリック -2: getChildAt : android.widget.LinearLayout@405f6e68 -1: getChildAt : android.widget.LinearLayout@405f7830 0: getChildAt : android.widget.LinearLayout@405f81f8 1: getChildAt : android.widget.LinearLayout@405f8bc0 2: getChildAt : android.widget.LinearLayout@405f9588 0: view : android.widget.LinearLayout@405f81f8 スクロール後にクリック -2: getChildAt : null -1: getChildAt : null 0: getChildAt : null 1: getChildAt : null 2: getChildAt : null 0: view : android.widget.LinearLayout@405fa918
どうにか手間をかけずにこのままどうにかしようと思ったけど諦めた。
スクロールに対応した子要素の取得のために、カスタムアダプターを作成。
カスタムアダプター
public class CustomAdapter extends ArrayAdapter<SampleRow>{ 省略 Map<Integer, View> positionView = new HashMap<Integer, View>(); /*もしくは SparseArray<View> positionView = new SparseArray<View>();*/ @Override public View getView(int position, View convertView, ViewGroup parent) { View view = convertView; ViewHolder holder; SampleRow sr = (SampleRow)getItem(position); if(view == null){ view = inflater.inflate(R.layout.sample_row, parent, false); holder = new ViewHolder(); holder.nameView = (TextView)view.findViewById(R.id.sample_row_name); view.setTag(holder); }else{ holder = (ViewHolder)view.getTag(); } holder.nameView.setText(sr.getName()); /*キーの重複不可を利用してgetViewで生成されるViewを格納*/ positionView.put(position, view); return view; } public View getPositionView(int targetPosition){ View targetView = positionView.get(targetPosition); return targetView; } 省略 }
修正後
public class Sample extends Activity{ int position = -1; ArrayAdapter<String> adapter; ListView list; 省略 adapter = new Customadapter(this, 0, bbb); list = (ListView)this.findViewById(R.id.xxx); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickeListener(){ @Override public void onItemClick((AdapterView<?> adapterView, View view, int position, long id){ setPosition(position); } }); Button button = (Button)this.findViewById(R.id.zzz); button.setOnClickListener(new OnClickListener(){ @Override public void onClick(View view){ /*処理対象となるListView内の子要素を取得*/ View clicked = adapter.getPositionView(getPosition()); 処理 /*上記処理でgetViewが呼び出される場合、getViewの処理を先に終わらせてから対象となる部分を取得しなおす*/ list.post(new Runnable(){ public void run(){ /*処理後に改めて取得*/ View clicked = adapter.getPositionView(getPosition()); /*処理後に改めて選択*/ clicked.setSelected(true); } }); } }); 省略 public void setPosition(int position){ this.position = position; } public int getPosition(){ return position; } 省略 }
カスタムアダプターを生成してgetView中にconvertViewをMapに格納していく。
Mapはキーの重複時に上書きするので、スクロールでサイズが増えていくことも防げる。
とりあえず素人考えで実装はしてみた。
勿論きちんと期待通りに動く。
効率がいいかはわからない。
ListViewの項目の背景色を動的に変更したいんだけど…
ListViewにおけるタップ時の色効果変更は…
main.java
ListView list = (ListView)this.findViewById(R.id.listView); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.custom_row, R.id.row_text); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, int id){ } }
と、カスタマイズした(といっても今回はシンプルな)レイアウトをセットし、
custom_row.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@color/row_color" > <TextView android:id="@+id/row_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="#FFF" /> </LinearLayout>
その対象となるレイアウト内のbackgroundにselectorによる色選択用xmlを指定。
row_color.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_selected="false" android:state_pressed="false" android:drawable="@color/black" /> <item android:state_selected="true" android:drawable="@color/red" /> <item android:state_selected="false" android:state_pressed="true" android:drawable="@color/black" /> </selector>
その色選択用xmlをres内に作ったcolorディレクトリに作成。
value内にcolors.xmlを作成して動作に対するオリジナル色の指定もできるようにした。
これで、ListView内のアイテムをタップすると指定した色に変更などができるわけだけど…
今回はListViewの項目をタップしたとき、その背景色を変えたい。
また、同じ項目をもう一度タップしたら背景色を元に、
違う項目をタップしたら先ほどの項目の背景色を元に戻した上でタップした項目の背景色を変更したい。
ようは、シンプルにON/OFFのある単項目の選択状態を明確に作りたい。
タップしている間の色変更はpressed指定で簡単にできるけど、今回はタップ判定ではなく選択判定。
ただ、背景色変更と上記設定の同項目再タップ時の非選択状態への両立が上手くいかない…
選択している以上、selected指定でいけるだろうとonItemClick内でsetSelected()のtrueやfalseを指定しても、
一回trueになるとなにを押してもtrueのままだったり、一回だけtrueであとはfalseだったり…
(setSelected()を使わずにsetBackgroundResource()などで無理やり再現はできるけど、
色指定用にXMLを無駄に作ったりでスマートじゃないし、position指定によるviewの再利用でスクロールに問題があるし、
そもそもselectorのxmlの意味がない…)
失敗パターンのひとつ.java
ListView list = (ListView)this.findViewById(R.id.listView); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.custom_row, R.id.row_text); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, int id){ System.out.println(view.isSelected() +":clicked "+ view); if(view.isSelected() == true){ view.setSelected(false); 処理 System.out.println(view.isSelected() +":true_root "+ view); }else if(view.isSelected() == false){ view.setSelected(true); 処理 System.out.println(view.isSelected() +":false_root "+ view); } } }
同じ項目を連打した動きはこんな感じ
わかり辛いけど タップ後の選択状態 : 出力位置 対象view
false:clicked android.widget.LinearLayout@405a9ec0 true:false_root android.widget.LinearLayout@405a9ec0 false:clicked android.widget.LinearLayout@405a9ec0 true:false_root android.widget.LinearLayout@405a9ec0 false:clicked android.widget.LinearLayout@405a9ec0 true:false_root android.widget.LinearLayout@405a9ec0 false:clicked android.widget.LinearLayout@405a9ec0 true:false_root android.widget.LinearLayout@405a9ec0
押したタイミングでselectedがtrueになっているのに同じ項目をもう一度タップするとfalse_rootにはいっている
viewにsetSelectedしても次にタップしたタイミングまで保持されてないみたい…?
スクロールしなくてもItemClickしたらgetViewで新しく生成されなおしてるの?
いや、view自体は同一みたいだし…再利用だからだろうか?
もしそうだとしても今回のレベルでgetView()内を弄るのもぐちゃぐちゃするし…
そこらへん確認したい。
と、書いてて思いついたのが正常に動作した。
修正案.java
ListView list = (ListView)this.findViewById(R.id.listView); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.custom_row, R.id.row_text); list.setAdapter(adapter); list.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, int id){ if(view.equals(clicked) == true){ view.setSelected(false); clicked = null; 処理 }else if(view.equals(clicked) == false){ view.setSelected(true); clicked = view; 処理 } } }
selectedが保持できないならフラグとして使わずにselectorでの判定のみにして、同一viewかをフラグにしてしまえばいい。
無駄に悩ませられたわりにあっさりしたコードになって拍子抜け…
固執しすぎは視野を狭めるいい教訓になりました…
カスタムAlertDialog生成時の例外回避
アイコンなど機能上のデザインの関係でカスタマイズしたListViewをAlertDialogにsetView()した際に、
「java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.」
なるエラーが発生して詰まった。
AlertDialogやカスタマイズしたListViewと絡めて調べてみると、
エラー内容に書いてある通りにすでに子要素に対する親要素があるからremoveView()するべきと。
そうすると今度は「java.lang.UnsupportedOperationException: removeView(View) is not supported in AdapterView」に悩まされた。
さらに調べてnotifyDataSetChanged()とかasList使用による固定長Listの返し対策としてみたが、
いずれも自分に該当せず…
自分のはファイルダイアログとしてAlertDialogを利用。
選択した項目がディレクトリならば、さらに下層(該当ディレクトリ)の内容で再生成していた。
初回のAlertDialogは開くけどディレクトリを選択するとIllefgalStateExceptionが発生。
発生箇所は再生成時のshow()。(正直、最初は再生成のメソッド周りだと思ってた…)
で、引用元をやっと発見し、解決。
同一のダイアログを複数回表示する際にはcreate()してからshow()をし、以降はshow()のみとのこと。
非常に助かった…が、
これカスタムAlertDialogでなく普通のAlertDialog時にはなにも問題なく再表示できていたんだけど、
どうも引用元の感じだとカスタム関係なくっぽいんだけど…どうなんだろうか?
Qiita Androidで同一Dialogを2回表示する
http://qiita.com/ayakix/items/65cbabb540a20ae2ede4
Qiita登録するだけしといて使えてないけど、いつか入り込めるといいなー