画像のローディングとかにはAsyncTaskをextendsしたクラスを使って非同期的に処理を行うのが一般的です。
で、そこにprogressBarを入れたかったんだがやってる人全くいないのな。
概念的にはこんな感じ。
あとProgressBarのコンストラクタには何故かContextを渡さないといけないので、インナークラスにしないと色々都合が悪いみたい。
色々ググってみたけどどこにもそれっぽいのが無かった。
http://d.hatena.ne.jp/Kazzz/20101027/p1
http://lablog.lanche.jp/archives/220
http://techbooster.jpn.org/andriod/ui/659/
結局プログレスバーについてはActivity内で処理するしかないの?
ところで関係ないんだけど忍者ブログの日記編集画面がバージョンアップされました。
一見良くなったような気がするけど、なんかカーソルが言うことを聞かなくなったりしてユーザビリティはダウン。
Google検索とかもそうだけど最近改悪が流行りなんですかね。
で、そこにprogressBarを入れたかったんだがやってる人全くいないのな。
概念的にはこんな感じ。
public class getImageTask extends AsyncTask<String, Void, Bitmap> { // コンストラクタ private ImageView imageView; public getImageTask(ImageView imageView) { this.imageView = imageView; } // 事前処理 @Override public void onPreExecute() { this.imageView.setImageBitmap(new ProgressBar()); } // メイン処理 @Override protected Bitmap doInBackground(String... urls) { image = HttpClient.getImage(urls[0]); } // 後で実行する処理 @Override protected void onPostExecute(Bitmap result) { this.imageView.setImageBitmap(result); } }ところがそもそもProgressBarはImageViewに入れられず、予めレイアウト上にプログレスバー用のビューを別途作っておかねばならないらしい。
あとProgressBarのコンストラクタには何故かContextを渡さないといけないので、インナークラスにしないと色々都合が悪いみたい。
色々ググってみたけどどこにもそれっぽいのが無かった。
http://d.hatena.ne.jp/Kazzz/20101027/p1
http://lablog.lanche.jp/archives/220
http://techbooster.jpn.org/andriod/ui/659/
ImageDownloadTask task = new getImageTask((ImageView) findViewById(R.id.hoge)) task.execute('http://example.com/hoge.png');みたいなかんじでどこからでも自由に使える画像ローダにしたかったんだが。
結局プログレスバーについてはActivity内で処理するしかないの?
ところで関係ないんだけど忍者ブログの日記編集画面がバージョンアップされました。
一見良くなったような気がするけど、なんかカーソルが言うことを聞かなくなったりしてユーザビリティはダウン。
Google検索とかもそうだけど最近改悪が流行りなんですかね。
PR
クリックイベントを設定するのはOnClickListenerをimplementsして(findViewById(R.id.hoge)).setOnClickListener(this);ってするのが王道ですが、たかが<a href>ってしたいだけだってなのに毎回こんな書き方するのも面倒です。
ということでもう少しだけ楽に、XML側でクリックイベントを設定することが出来ます。
ところがこのandroid:onClick、Viewで実装されているらしいのですが、ImageViewやButtonなどでは動くくせに何故かTextViewでは動作しません。
いざとなればテキストをLinearLayoutなどで囲ってそこにandroid:onClickを設定する方法などもありますが、ちょっとどうなのよそれって感じです。
でも実は簡単な解決法があって、
なんでデフォルトで効かないようになっているのかはよくわかりません。
ということでもう少しだけ楽に、XML側でクリックイベントを設定することが出来ます。
<ImageView android:id="@+id/hoge" android:src="@drawable/hoge" android:onClick="onClickHoge"></ImageView>とandroid:onClickを設定すると、そこをクリックしたときにonClickHoge(View)メソッドが呼ばれます。
ところがこのandroid:onClick、Viewで実装されているらしいのですが、ImageViewやButtonなどでは動くくせに何故かTextViewでは動作しません。
いざとなればテキストをLinearLayoutなどで囲ってそこにandroid:onClickを設定する方法などもありますが、ちょっとどうなのよそれって感じです。
でも実は簡単な解決法があって、
<TextView android:id="@+id/hoge" android:src="@drawable/hoge" android:clickable="true" android:onClick="onClickHoge"></TextView>とandroid:clickable="true"を入れるだけです。
なんでデフォルトで効かないようになっているのかはよくわかりません。
最初から入っているApiDemosアプリケーションにサンプルがあるので見てみましょう。
起動後、Text→Linkifyを見るとtext1からtext4まで4種類の方法でリンクが作成されています。
順番に見てみます。
まずtext1、レイアウトで自動設定。
/res/layout/link.xml
/res/values/strings.xml
android:autoLinkを指定すると、文字列中にURLっぽい文字列、電話番号っぽい文字列などがあれば自動的にリンクを張ってくれます。
もっとも簡単な方法と言えますが、<a href="http://www.google.com/">リンク</a>みたいなことはできません。
2番目がTextView.setMovementMethod()を利用する方法。
/ApiDemo/com.example.android.apis.test/Link.java
/res/layout/link.xml
/res/values/strings.xml
"
文字列として<a>タグを記述すると、その部分をブラウザへのリンクに変換してくれます。
TextView.setMovementMethod()は何をやってるかというと、リンクをクリックしたときにブラウザアクティビティを起動するというリスナーを自動追加しています。
昔OnItemClickListenerで実装していたようなことを一行で勝手にやってくれます。
ただandroid:autoLinkではそこらへんまで自動でやってくれたんだから、こっちでもやってくれればよかったのに。
なんか違うことをしたいときだけ上書きとかそんなかんじで。
3番目、アクティビティから文字列追加。
/ApiDemo/com.example.android.apis.test/Link.java
/res/layout/link.xml
単に2番目ではandroid:textで設定していたテキストをTextView.setText()で設定するようにしただけなのですが、この場合は何故かHtml.fromHtml()という謎メソッドを経由しなければなりません。
単純にt3.setText()だけだと、タグ部分もそのままテキスト表示されてしまいます。
android:textとTextView.setText()の動作は微妙に違うようです。
最後の手段は、テキストの任意箇所にSpannableString.setSpan()でスタイルをぺたぺた貼り付けていくという苦行な方法です。
/ApiDemo/com.example.android.apis.test/Link.java
/res/layout/link.xml
SpannableStringに文字列を突っ込み、1~6文字目をボールドに、14~17文字目を電話帳へのリンクにしています。
便利なのはSpannableString.setSpan()は入れ子も気にしないでいいということです。
1~14文字目をボールドに、6~17文字目を電話帳へのリンクにしたりなんてことも問題なく行うことができます。
ちなみにURLSpanはマルチバイト対応なので、日本語が入っていても気にせずカウント可能です。
#Typeface.BOLDは日本語に変化がなかったんだがこれはフォントの問題?根本的に非対応?
起動後、Text→Linkifyを見るとtext1からtext4まで4種類の方法でリンクが作成されています。
順番に見てみます。
まずtext1、レイアウトで自動設定。
/res/layout/link.xml
<TextView android:id="@+id/text1" android:layout_width="match_parent" android:layout_height="match_parent" android:autoLink="all" android:text="@string/link_text_auto" />
/res/values/strings.xml
<string name="link_text_auto"><b>text1:</b> This is some text. In this text are some things that are actionable. For instance, you can click on http://www.google.com and it will launch the web browser. You can click on google.com too. And, if you click on (415) 555-1212 it should dial the phone. </string>
android:autoLinkを指定すると、文字列中にURLっぽい文字列、電話番号っぽい文字列などがあれば自動的にリンクを張ってくれます。
もっとも簡単な方法と言えますが、<a href="http://www.google.com/">リンク</a>みたいなことはできません。
2番目がTextView.setMovementMethod()を利用する方法。
/ApiDemo/com.example.android.apis.test/Link.java
TextView text2 = (TextView) findViewById(R.id.autoLinkSample); text2.setMovementMethod(LinkMovementMethod.getInstance());
/res/layout/link.xml
<TextView android:id="@+id/text2" android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/link_text_manual" />
/res/values/strings.xml
<string name="link_text_manual"><b>text2:</b> This is some other text, with a <a href="http://www.google.com">link</a> specified via an <a> tag. Use a \"tel:\" URL to <a href="tel:4155551212">dial a phone number</a>. </string>
"
文字列として<a>タグを記述すると、その部分をブラウザへのリンクに変換してくれます。
TextView.setMovementMethod()は何をやってるかというと、リンクをクリックしたときにブラウザアクティビティを起動するというリスナーを自動追加しています。
昔OnItemClickListenerで実装していたようなことを一行で勝手にやってくれます。
ただandroid:autoLinkではそこらへんまで自動でやってくれたんだから、こっちでもやってくれればよかったのに。
なんか違うことをしたいときだけ上書きとかそんなかんじで。
3番目、アクティビティから文字列追加。
/ApiDemo/com.example.android.apis.test/Link.java
TextView t3 = (TextView) findViewById(R.id.text3); t3.setText( Html.fromHtml( "<b>text3:</b> Text with a " + "<a href=\"http://www.google.com\">link</a> " + "created in the Java source code using HTML.")); t3.setMovementMethod(LinkMovementMethod.getInstance());
/res/layout/link.xml
<TextView android:id="@+id/text3" android:layout_width="match_parent" android:layout_height="match_parent" />
単に2番目ではandroid:textで設定していたテキストをTextView.setText()で設定するようにしただけなのですが、この場合は何故かHtml.fromHtml()という謎メソッドを経由しなければなりません。
単純にt3.setText()だけだと、タグ部分もそのままテキスト表示されてしまいます。
android:textとTextView.setText()の動作は微妙に違うようです。
最後の手段は、テキストの任意箇所にSpannableString.setSpan()でスタイルをぺたぺた貼り付けていくという苦行な方法です。
/ApiDemo/com.example.android.apis.test/Link.java
SpannableString ss = new SpannableString( "text4: Click here to dial the phone."); ss.setSpan(new StyleSpan(Typeface.BOLD), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ss.setSpan(new URLSpan("tel:4155551212"), 13, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); TextView t4 = (TextView) findViewById(R.id.text4); t4.setText(ss); t4.setMovementMethod(LinkMovementMethod.getInstance());
/res/layout/link.xml
<TextView android:id="@+id/text4" android:layout_width="match_parent" android:layout_height="match_parent" />
SpannableStringに文字列を突っ込み、1~6文字目をボールドに、14~17文字目を電話帳へのリンクにしています。
便利なのはSpannableString.setSpan()は入れ子も気にしないでいいということです。
1~14文字目をボールドに、6~17文字目を電話帳へのリンクにしたりなんてことも問題なく行うことができます。
ちなみにURLSpanはマルチバイト対応なので、日本語が入っていても気にせずカウント可能です。
#Typeface.BOLDは日本語に変化がなかったんだがこれはフォントの問題?根本的に非対応?
インテントの使い方は、new Intent()でインスタンスを作成し、setAction()なりputExtra()なりで渡したいデータや情報を設定し、最後にstartActivity(intent)ってするだけです。
Webアプリで言うところの<form>タグにわりと近いですが、最大の特徴として<form action>の行き先をきっちりと指定しなくてよい、というものがあります。
Intent.setAction(Intent.ACTION_DIAL)とすると、Android側で勝手に、AndroidManifest.xmlでIntent.ACTION_DIALに結びつけられているアプリケーションを探し出してきてくれます。
Windowsの拡張子関連付けのようなもので、拡張子が"ACTION_DIAL"のアプリを起動してくれという指定をするだけで、具体的にどのアプリを起動するかまでは記入しません。
最初はデフォルトのダイアラが起動すると思われますが、別のダイアラアプリをインストールしていればそちらを起動するようにもできるということです。
勿論宛先をきっちり指定したインテントも可能で、その場合はコンストラクタなりIntent.setClassName()なりで宛先パスを具体的に指定します。
さて、Intent.putExtra()でインテントに色々なデータを詰め込むことができるのですが、対応しているのがプリミティブ型とかParcelableとかCharSequenceとか、まあ要するにテキストデータを突っ込むのが前提になっています。
また、画像を扱うBitmapあたりはParcelableを実装しているのでそのまま突っ込めます。
が、適当なファイルなんかをそのまま突っ込みてーとか思ってもそのままではできません。
ではどうすればいいかというと、バイナリをストリーム化してバイト列にして投入、という面倒な手続きを取らないといけません。
送信側
いや、なんか手段はあるとは思うんだけどやり方がわかりません。
Webアプリで言うところの<form>タグにわりと近いですが、最大の特徴として<form action>の行き先をきっちりと指定しなくてよい、というものがあります。
Intent.setAction(Intent.ACTION_DIAL)とすると、Android側で勝手に、AndroidManifest.xmlでIntent.ACTION_DIALに結びつけられているアプリケーションを探し出してきてくれます。
Windowsの拡張子関連付けのようなもので、拡張子が"ACTION_DIAL"のアプリを起動してくれという指定をするだけで、具体的にどのアプリを起動するかまでは記入しません。
最初はデフォルトのダイアラが起動すると思われますが、別のダイアラアプリをインストールしていればそちらを起動するようにもできるということです。
勿論宛先をきっちり指定したインテントも可能で、その場合はコンストラクタなりIntent.setClassName()なりで宛先パスを具体的に指定します。
さて、Intent.putExtra()でインテントに色々なデータを詰め込むことができるのですが、対応しているのがプリミティブ型とかParcelableとかCharSequenceとか、まあ要するにテキストデータを突っ込むのが前提になっています。
intent.putExtra("title", "タイトル"); intent.putExtra("value", 1);などは問題なく行えます。
また、画像を扱うBitmapあたりはParcelableを実装しているのでそのまま突っ込めます。
が、適当なファイルなんかをそのまま突っ込みてーとか思ってもそのままではできません。
ではどうすればいいかというと、バイナリをストリーム化してバイト列にして投入、という面倒な手続きを取らないといけません。
送信側
//ファイルをストリーム化してインテントに突っ込む InputStream input = getResources().openRawResource(R.raw.hoge); try { byte[] buffer = new byte[8192]; int actual_read = 0; while ((actual_read = input.read(buffer)) != -1) { intent.putExtra("hogehoge", buffer); } } catch (Exception e) { e.printStackTrace(); }受信側
//インテントからストリームを取得 byte[] buffer = intent.getByteArrayExtra("hogehoge"); ByteArrayInputStream input = new ByteArrayInputStream(buffer);なんかもっとこうどうにかならないものですかね。
いや、なんか手段はあるとは思うんだけどやり方がわかりません。
http://codezine.jp/article/detail/4878?p=2
前回の続き。
マップアクティビティの呼び出し側はインテントで住所を突っ込んでいますが、呼ばれる側では見ていませんでした。
引き渡された住所をチェックして、マップの初期位置としてセットするにはMapView.getController().setCenter()などを使用しますが、この引数は経緯度で設定する必要があったりします。
そこで住所→経緯度への変換用にGeocorderというものが用意されています。
さっそく使ってみましょう。
/src/com.google.android.maps/MapActivity.java
public void onCreate(Bundle savedInstanceState) { /* ここに前回までの内容 */ //Geocoder Geocoder geocoder = new Geocoder( this, Locale.getDefault()); //インテント String strAddress = getIntent().getStringExtra( BizCard.COLUMN_ADDRESS); //Geocoderで住所からAddressへ変換 try { //最初の一件を取得 List<Address> addressList = geocoder.getFromLocationName(strAddress, 1); Address address = null; address = addressList.get( 0); //目的地を設定 if( address != null){ setDist(address); } } catch (Exception e) { Toast toast = Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG); toast.show(); } }
/res/values/strings.xml
<string name="select_target">対象を選択してください</string> <string name="cannot_get_address">住所の取得に失敗しました</string>サンプルではいきなりいろいろやってますが、とりあえず最低限の、一件だけ取得して目的地をセットするという簡単な内容にしてみました。
ところが、検索文字列に何入れてもGeocoder.getFromLocationName()で100%Exceptionが発動するんだがなんだこれ。
getMessage()すると"Service not Available"って言われた。なんだこれ?
map.xmlのAPIキーをちがう値にしてみると地図の取得自体できないからAPIキーが間違っているということもなさそうだし。
http://groups.google.co.jp/group/android-developers/browse_thread/thread/b02c29d746471358/81f09bbc825ffb26?lnk=raot
> I have seen the same issue when I updated from 2.1 and API Level 7
> (where it works) to 2.2 API Level 8 (where it does not).
> Seems to be in the level 8 package. I also noticed quite a bit of
> slowdown in the map view scrolling after updating to 2.2.
日本語での同様な症状がまったく見つからない。
GoogleAPI4を入れ、サンプルとまったく同じ環境にして実行してみたがそれでも駄目でした。
本当にただ単にGoogleMapAPIが非対応になってただけだったりしたらどうしよう。
先に進めないのでBizCardアプリケーションは一旦中断。
残念。
Androidの記事
http://codezine.jp/article/detail/4878
今回は残る地図の実装です。
現在はマップを選択すると、現在値などの指定をしていないのでデフォルトの日本全土マップが表示されます。
とりあえずMapView.onCreate()を実装。
/src/com.google.android.maps/MapActivity.java
public class MapActivity extends com.google.android.maps.MapActivity implements OnCheckedChangeListener { //インスタンス変数 private MapView map = null; private RadioButton normalMapRadio = null; private RadioGroup radioGroup = null; private ToggleButton currentLocationToggle = null; private MapController controller = null; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.map); // UI部品の取得 map = (MapView) findViewById(R.id.map); radioGroup = (RadioGroup)findViewById( R.id.mapRadioGroup); normalMapRadio = (RadioButton)findViewById(R.id.normalMapRadio); currentLocationToggle = (ToggleButton)findViewById( R.id.currentLocationToggle); // デフォルトで「地図」ボタンを選択しておく normalMapRadio.setChecked( true); // ラジオボタンのクリックリスナー radioGroup.setOnCheckedChangeListener( this); // マップコントローラを取得 controller = map.getController(); // マップビューの各使用フラグをセット map.setBuiltInZoomControls( true); map.setSatellite( false); map.setClickable( true); map.setEnabled( true); } }GoogleMapを使用するにはいつものActivityではなくMapActivityを継承する必要があります。
このMapActivityは単にActivityを継承して地図を利用するのに必要な機能を付け加えただけなので、概ね同じように使用することができます。
ひとつだけabstract MapActivity.isRouteDisplayed()が宣言されているのでこちらの実装を行う必要があります。
単にルート表示を行うか否かのフラグみたいなので今回はとりあえずreturn false;とかやっておけば問題なさげです。
radioGroupは"@+id/mapRadioGroup"のラジオボタングループ、normalMapRadioはその中の「地図」ボタンです。
RadioGroup.setChecked()で選択状態のデフォルト値を指定します。
setOnCheckedChangeListener()でラジオボタンにクリックリスナーを定義します。
クリックが検知されるとonCheckedChanged()が発動します。
で、これはOnCheckedChangeListenerインターフェイスで宣言されているのでimplementsして実装する必要があります。
MapViewの各メソッドでフラグを投入し、動作を指定します。
setBuiltInZoomControls()でズームの可否、setSatellite()で衛星写真表示にするか否か、setClickable()でクリックやドラッグの可否、というかんじみたいです。
setClickable()とsetEnabled()の違いがわからなかった。
アクティビティからマップAPIの拡大縮小といった具体的操作を行うには、MapView.getController()でMapControllerを取得します。
今回はとりあえず取得しただけで操作は無し。
ということでisRouteDisplayed()とonCheckedChanged()を実装します。
//ラジオボタンの選択変更時にマップを切り替える public void onCheckedChanged(RadioGroup group, int checkedId) { // 「地図」がクリックされていたら衛星写真をオフ if( checkedId == R.id.normalMapRadio){ map.setSatellite( false); //「衛星写真」がクリックされていたら衛星写真をオン }else if( checkedId == R.id.satelliteMapRadio){ map.setSatellite( true); } } //ルート表示を行うか否かのフラグ protected boolean isRouteDisplayed() { return false; }onCheckedChanged()は第一引数にラジオボタングループ、第二引数にラジオボタンIDが入ってきます。
第一引数は複数のラジオボタングループがあったりする場合に使うのだと思いますが、今回はひとつしかないのでIDだけで十分です。
その後はクリックされていたボタンによって、衛星写真をオンオフしているだけです。
isRouteDisplayed()は単にfalseを返します。
以上でラジオボタンが実装されたマップビューが完成しました。
Androidの記事
http://codezine.jp/article/detail/4842?p=4
前回までで「一覧」「参照」「登録」各画面の実装が一通り終了しました。
サンプルでは早々に次に進んでしまいますが、実はこの実装にはひとつ問題点があります。
「名刺一覧画面→新規→削除→OK」
と遷移するとエラーになります。
BizCardDao.delete()の実装はこのようになっています。
ということでBizCardDaoを変更。
本来は削除する前にbizCard.getRowid().length()とかでチェックするのが正道なのだと思いますがまあいいや。
以上で新規登録→削除を行っても落ちないようになりました。
返り値がvoidなんで成功したんだか失敗したんだかわからないところがアレですが。
さて、これで大丈夫かと思いきやさらにさらに問題があります。
「名刺一覧画面→詳細→編集→削除→OK→保存」
今回は何処でエラーになっているかというと、ShowActivity.onActivityResult()です。
/src/com.example.bizcard.db/BizCardDao.java
BizCard.load()の返り値は本来BizCard型ですが、取得に失敗した場合はnullが返るようになります。
ということで呼び出し側のShowActivity.onActivityResult()では返り値がnullだった場合さっさと名刺一覧画面に戻るようにしました。
以上で、「名刺一覧画面→詳細→編集→削除→OK→保存」とした場合、保存されたうえで名刺一覧画面に戻るようになりました。
これでめでたしめでたし、かと思えば実はまだ問題があったりして。
「名刺一覧画面→詳細→編集→削除→OK→戻るボタン」で、削除したはずの名刺情報が閲覧できます。
これはもまたShowActivity.onActivityResult()が原因です。
最初の一行でif( resultCode == RESULT_OK)とやっていますが、RegistActivity.onOptionsItemSelected()のように明示的に成功ステータスを返さないとここがtrueになりません。
戻るボタンが押された場合はRESULT_CANCELEDが返ってくるのでここのコードが実行されず、インテント呼び出し前の画面がそのまま表示されるという結果になります。
そこからさらに編集→保存などとしてみると「保存されました」とか表示するくせに一覧画面に戻ってさらに保存されていないというおかしな結果に。
解決策として、ここでは単純に返り値がどのようなステータスでもBizCardをリロードするようにします。
何故って、他に削除→戻るボタンと単純に戻るボタンを押しただけの状態を区別する方法が見つからないんじゃ。
/src/com.example.bizcard/ShowActivity.java
これで名刺一覧画面→詳細→編集→削除→OK→戻るボタンで、名刺一覧画面に戻るようになりました。
以上でようやくめでたしめでたしかと思えばまだまだ問題があったり。
「名刺一覧画面→新規登録→保存」、「名刺一覧画面→詳細→編集→削除→OK→保存」だと保存されたあと一覧画面に戻りますが、「名刺一覧画面→詳細→編集→保存」だと名刺詳細画面に移動します。
こんなふうに操作によってちがう挙動になるというのはよくないので、どの遷移を通っても名刺詳細画面に戻るべきでしょう。
問題は「名刺一覧画面→新規登録→保存」でShowActivity.bizCardにどうやって作成したBizCardオブジェクトを保存するかわかんないという点なわけですが。
前回までで「一覧」「参照」「登録」各画面の実装が一通り終了しました。
サンプルでは早々に次に進んでしまいますが、実はこの実装にはひとつ問題点があります。
「名刺一覧画面→新規→削除→OK」
と遷移するとエラーになります。
BizCardDao.delete()の実装はこのようになっています。
public void delete(BizCard bizCard) { SQLiteDatabase db = helper.getWritableDatabase(); try { db.delete( BizCard.TABLE_NAME, BizCard.COLUMN_ID + "=?", new String[]{ String.valueOf( bizCard.getRowid())}); } finally { db.close(); } }呼び出し側はこうです
BizCardDao dao = new BizCardDao( RegistActivity.this); dao.delete( bizCard);このbizCard、新規登録だとIDが無いので、BizCardDao.deleteで例外が発生するのです。
ということでBizCardDaoを変更。
public void delete(BizCard bizCard) { SQLiteDatabase db = helper.getWritableDatabase(); try { db.delete( BizCard.TABLE_NAME, BizCard.COLUMN_ID + "=?", new String[]{ String.valueOf( bizCard.getRowid())}); } catch(Exception e){ } finally { db.close(); } }空のcatchをつけ加えただけです。
本来は削除する前にbizCard.getRowid().length()とかでチェックするのが正道なのだと思いますがまあいいや。
以上で新規登録→削除を行っても落ちないようになりました。
返り値がvoidなんで成功したんだか失敗したんだかわからないところがアレですが。
さて、これで大丈夫かと思いきやさらにさらに問題があります。
「名刺一覧画面→詳細→編集→削除→OK→保存」
今回は何処でエラーになっているかというと、ShowActivity.onActivityResult()です。
protected void onActivityResult(int requestCode, int resultCode, Intent data) { if( resultCode == RESULT_OK){ // 表示を更新する BizCardDao dao = new BizCardDao( this); bizCard = dao.load( bizCard.getRowid()); updateView(); } }呼び出されるBizCardDao.load()
public BizCard load(Long rowId) { SQLiteDatabase db = helper.getReadableDatabase(); BizCard bizCard = null; try { Cursor cursor = db.query( BizCard.TABLE_NAME, null, BizCard.COLUMN_ID + "=?", new String[]{ String.valueOf( rowId)}, null, null, null); cursor.moveToFirst(); bizCard = getBizCard( cursor); } finally { db.close(); } return bizCard; }編集画面でbizCardを削除してしまっているので、bizCard.getRowid()に失敗してBizCardDao.load()中で例外が発生します。
/src/com.example.bizcard.db/BizCardDao.java
public BizCard load(Long rowId) { SQLiteDatabase db = helper.getReadableDatabase(); BizCard bizCard = null; try { Cursor cursor = db.query( BizCard.TABLE_NAME, null, BizCard.COLUMN_ID + "=?", new String[]{ String.valueOf( rowId)}, null, null, null); cursor.moveToFirst(); bizCard = getBizCard( cursor); } catch(Exception e) { } finally { db.close(); } return bizCard; }/src/com.example.bizcard/ShowActivity.java
protected void onActivityResult(int requestCode, int resultCode, Intent data) { if( resultCode == RESULT_OK){ // 表示を更新する BizCardDao dao = new BizCardDao( this); bizCard = dao.load( bizCard.getRowid()); if(bizCard == null ){ setResult( RESULT_CANCELED); finish(); return ; } updateView(); } }まずBizCard.load()のほうは例外を単に空のcatchで括ります。
BizCard.load()の返り値は本来BizCard型ですが、取得に失敗した場合はnullが返るようになります。
ということで呼び出し側のShowActivity.onActivityResult()では返り値がnullだった場合さっさと名刺一覧画面に戻るようにしました。
以上で、「名刺一覧画面→詳細→編集→削除→OK→保存」とした場合、保存されたうえで名刺一覧画面に戻るようになりました。
これでめでたしめでたし、かと思えば実はまだ問題があったりして。
「名刺一覧画面→詳細→編集→削除→OK→戻るボタン」で、削除したはずの名刺情報が閲覧できます。
これはもまたShowActivity.onActivityResult()が原因です。
最初の一行でif( resultCode == RESULT_OK)とやっていますが、RegistActivity.onOptionsItemSelected()のように明示的に成功ステータスを返さないとここがtrueになりません。
戻るボタンが押された場合はRESULT_CANCELEDが返ってくるのでここのコードが実行されず、インテント呼び出し前の画面がそのまま表示されるという結果になります。
そこからさらに編集→保存などとしてみると「保存されました」とか表示するくせに一覧画面に戻ってさらに保存されていないというおかしな結果に。
解決策として、ここでは単純に返り値がどのようなステータスでもBizCardをリロードするようにします。
何故って、他に削除→戻るボタンと単純に戻るボタンを押しただけの状態を区別する方法が見つからないんじゃ。
/src/com.example.bizcard/ShowActivity.java
protected void onActivityResult(int requestCode, int resultCode, Intent data) { // 表示を更新する BizCardDao dao = new BizCardDao( this); bizCard = dao.load( bizCard.getRowid()); //取得できなければ一覧に戻る if(bizCard == null ){ setResult( RESULT_CANCELED); finish(); return ; } updateView(); }単にifを削除しただけです。
これで名刺一覧画面→詳細→編集→削除→OK→戻るボタンで、名刺一覧画面に戻るようになりました。
以上でようやくめでたしめでたしかと思えばまだまだ問題があったり。
「名刺一覧画面→新規登録→保存」、「名刺一覧画面→詳細→編集→削除→OK→保存」だと保存されたあと一覧画面に戻りますが、「名刺一覧画面→詳細→編集→保存」だと名刺詳細画面に移動します。
こんなふうに操作によってちがう挙動になるというのはよくないので、どの遷移を通っても名刺詳細画面に戻るべきでしょう。
問題は「名刺一覧画面→新規登録→保存」でShowActivity.bizCardにどうやって作成したBizCardオブジェクトを保存するかわかんないという点なわけですが。
前回の続き。
さて、住所のマップボタンにリスナーを追加したのでOnClickListenerをimplementsしてonClickを実装する必要があります。
/src/com.example.bizcard/RegistActivity.java
以上で「名刺一覧画面から新規登録」「名刺詳細画面から編集」をクリックしたときに正しく表示されるようになりました。
次に必要なのは保存のロジックとテンプレートです。
閲覧画面とちがって編集前提なので最下段に保存ボタンとかでいい気がしますが、サンプルがメニューボタンから操作を行うようになっているので倣います。
個人的にこのswitch内に長大ロジックって作りが大嫌いなんだがみんな平気なの?
まずonCreateOptionsMenu()でメニューを押したらregist.xmlが表示されるようにします。
次にonOptionsItemSelected()でメニューボタンが押されたときの処理を記述します。
最初にあるのはR.id.menu_newですが、現在メニューボタンにR.id.menu_newはないのでここに来ることはありません。
次にR.id.menu_save、保存ボタンを押したときの処理です。
必須なのが名前だけなので、length()で長さが0だった場合はエラーメッセージを表示します。
このToastは一定時間だけダイアログを表示するという便利なクラスです。
お手軽に実績のポコンみたいなものを実装できます。
第三引数で表示時間を指定することができ、Toast.LENGTH_SHORTは短時間(2、3秒)、Toast.LENGTH_LONGは長時間(4、5秒)ってあんまり変わらねえ。
何故か任意の時間を渡すことはできません。
その後は単にBizCardオブジェクトに入力値を突っ込んでsave()メソッドを呼んでるだけです。
編集の場合一番最初の一覧画面で取得したBizCardオブジェクトをずっと引き回しているので、IDが入っていてUPDATEになり、新規作成の場合は直前にnewで作成されているのでIDがなくINSERTになります。
めでたし。
Activity.finish()すると現在のアクティビティを終了します。
Activity.setResult()で値を入れると呼び出し元のアクティビティに値を返すことができます。
でだ、前ShowActivity.onActivityResult()でRESULT_OKだったら表示を更新、というメソッドを実装したはずなんだが更新されない。何故?
ifを外して常時更新にしても駄目だったので根本的に呼ばれてすらいないみたいなんだが。
いろいろ触ってみたところ、startActivityForResult()の第二引数を正の値にしたら正しく動作するようになりました。
なにそれ。
サンプルでは間違いなく-1って書いてあるのに。
削除ボタンを押したときはR.id.menu_deleteの動作になります。
まずAlertDialogでアラートのポップアップを作成します。
AlertDialog.Builder.setIcon()とAlertDialog.setIcon()が一体どう違うのか私にはわからない。
ダイアログ内にsetNegativeButton()でキャンセルボタン、setPositiveButton()でOKボタンを実装します。
setPositiveButtonのリスナーはこの場かぎりの無名クラスで実装されています。
中身はデータベースから現在のBizCardを削除し、テキストを削除し、インスタンス内のBizCardオブジェクトも削除し、削除した旨のメッセージを表示する、となっています。
以上で、マップ関係以外の名刺管理アプリケーションの機能が完成しまし・・・実はひとつ問題があります。
Androidの記事
さて、住所のマップボタンにリスナーを追加したのでOnClickListenerをimplementsしてonClickを実装する必要があります。
/src/com.example.bizcard/RegistActivity.java
//mapButtonをクリックした public void onClick(View v) { //住所を取得してインテントに詰めてMapActivityを起動 String address = addressText.getText().toString(); Intent mapIntent = new Intent( RegistActivity.this, MapActivity.class); mapIntent.putExtra( BizCard.COLUMN_ADDRESS, address); startActivity( mapIntent); };やってることはコメントに書いてあるとおりで、ShowActivityの住所リンクをクリックしたときと全く同じです。
以上で「名刺一覧画面から新規登録」「名刺詳細画面から編集」をクリックしたときに正しく表示されるようになりました。
次に必要なのは保存のロジックとテンプレートです。
閲覧画面とちがって編集前提なので最下段に保存ボタンとかでいい気がしますが、サンプルがメニューボタンから操作を行うようになっているので倣います。
//テキストの削除を行うサブルーチン private void clear(){ bizCard = null; personNameText.setText( null); companyNameText.setText( null); organizationNameText.setText( null); positionNameText.setText( null); zipCodeText.setText( null); addressText.setText( null); tel1Text.setText( null); tel2Text.setText( null); mailText.setText( null); } //メニューボタン @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.regist, menu); return true; } //メニューボタンをクリックした @Override public boolean onOptionsItemSelected(MenuItem item) { //押したメニューボタンを取得 int itemId = item.getItemId(); switch (itemId) { // 新規("@+id/menu_new") case R.id.menu_new: clear(); break; // 保存("@+id/menu_save") case R.id.menu_save: //新規作成の場合BizCardオブジェクトができてないので作成 if( bizCard == null){ bizCard = new BizCard(); } // 入力チェック 氏名が入力されているかのみ String personName = personNameText.getText().toString(); if( personName.length() == 0){ Toast toast = Toast.makeText(this, R.string.error_required, Toast.LENGTH_SHORT); toast.show(); return false; } // BizCardオブジェクトに入力値を反映する bizCard.setPersonName( personName); bizCard.setCompanyName( companyNameText.getText().toString()); bizCard.setOrganizationName( organizationNameText.getText().toString()); bizCard.setPositionName( positionNameText.getText().toString()); bizCard.setZipCode( zipCodeText.getText().toString()); bizCard.setAddress( addressText.getText().toString()); bizCard.setTel1( tel1Text.getText().toString()); bizCard.setTel2( tel2Text.getText().toString()); bizCard.setMail( mailText.getText().toString()); // 保存 BizCardDao dao = new BizCardDao( this); bizCard = dao.save( bizCard); // メッセージ表示 Toast toast = Toast.makeText(this, R.string.saved, Toast.LENGTH_SHORT); toast.show(); // 保存時に終了し、前のアクティビティへ戻る setResult( RESULT_OK); finish(); break; // 削除("@+id/menu_delete") case R.id.menu_delete: // 確認ダイアログの表示 AlertDialog.Builder builder = new AlertDialog.Builder( this); // アイコン設定 builder.setIcon(android.R.drawable.ic_dialog_alert); // タイトル設定 builder.setTitle( R.string.confirm_delete); // OKボタン設定 builder.setPositiveButton( android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // 削除処理 BizCardDao dao = new BizCardDao( RegistActivity.this); dao.delete( bizCard); // 画面の更新 clear(); // メッセージ表示 Toast toast = Toast.makeText(RegistActivity.this, R.string.deleted, Toast.LENGTH_SHORT); toast.show(); } }); // キャンセルボタン設定 builder.setNegativeButton( android.R.string.cancel, null); // ダイアログの表示 builder.show(); break; } return true; }さあ一気に長くなりました。
個人的にこのswitch内に長大ロジックって作りが大嫌いなんだがみんな平気なの?
まずonCreateOptionsMenu()でメニューを押したらregist.xmlが表示されるようにします。
次にonOptionsItemSelected()でメニューボタンが押されたときの処理を記述します。
最初にあるのはR.id.menu_newですが、現在メニューボタンにR.id.menu_newはないのでここに来ることはありません。
次にR.id.menu_save、保存ボタンを押したときの処理です。
必須なのが名前だけなので、length()で長さが0だった場合はエラーメッセージを表示します。
このToastは一定時間だけダイアログを表示するという便利なクラスです。
お手軽に実績のポコンみたいなものを実装できます。
第三引数で表示時間を指定することができ、Toast.LENGTH_SHORTは短時間(2、3秒)、Toast.LENGTH_LONGは長時間(4、5秒)ってあんまり変わらねえ。
何故か任意の時間を渡すことはできません。
その後は単にBizCardオブジェクトに入力値を突っ込んでsave()メソッドを呼んでるだけです。
編集の場合一番最初の一覧画面で取得したBizCardオブジェクトをずっと引き回しているので、IDが入っていてUPDATEになり、新規作成の場合は直前にnewで作成されているのでIDがなくINSERTになります。
めでたし。
Activity.finish()すると現在のアクティビティを終了します。
Activity.setResult()で値を入れると呼び出し元のアクティビティに値を返すことができます。
でだ、前ShowActivity.onActivityResult()でRESULT_OKだったら表示を更新、というメソッドを実装したはずなんだが更新されない。何故?
ifを外して常時更新にしても駄目だったので根本的に呼ばれてすらいないみたいなんだが。
いろいろ触ってみたところ、startActivityForResult()の第二引数を正の値にしたら正しく動作するようになりました。
なにそれ。
サンプルでは間違いなく-1って書いてあるのに。
削除ボタンを押したときはR.id.menu_deleteの動作になります。
まずAlertDialogでアラートのポップアップを作成します。
AlertDialog.Builder.setIcon()とAlertDialog.setIcon()が一体どう違うのか私にはわからない。
ダイアログ内にsetNegativeButton()でキャンセルボタン、setPositiveButton()でOKボタンを実装します。
setPositiveButtonのリスナーはこの場かぎりの無名クラスで実装されています。
中身はデータベースから現在のBizCardを削除し、テキストを削除し、インスタンス内のBizCardオブジェクトも削除し、削除した旨のメッセージを表示する、となっています。
以上で、マップ関係以外の名刺管理アプリケーションの機能が完成しまし・・・実はひとつ問題があります。
Androidの記事
http://codezine.jp/article/detail/4842?p=4
前回は名刺詳細画面から詳細編集画面への遷移を作成しました。
今回は詳細編集画面にインテントが送られてきたらその値をデフォルト表示するようにします。
実は名刺一覧画面からやってくる新規登録画面と、名刺詳細画面からやってくる詳細編集画面は同じです。
入力は単にインテントがあれば初期値にして、インテントがなければ空っぽのままにします。
問題は保存で、更新と新規登録を見極めなければなりません。
が、実は前BizCardDaoを作成したときに、bizCard.rowidがあれば更新、無ければ新規登録というふうにしていました。
つまりインテントからrowidを持ち回しておけば問題無いということです。
ということでRegistActivityを実装。
まずは新規/保存/削除を選択させるメニューを作成。
/res/menu/regist.xml
ただ、これは保存/削除メニューを一気に作ってるんだけど、この作りだと絶対に保存/削除は並べて表示するしかなくなります。
regist.xml以外に/res/menu/delete.xmlを作って削除メニューはそっちに持っていった方が自由度があるのではないだろうかと思った。
まあ普通保存と削除は並んでるだろうからまあいいか。
先に「保存しました」とかの文言を作成しておく。
/res/values/strings.xml
インテントがあればそこから値を取得し、なければ単に新規登録するようにRegistActivity.onCreate()を書き換えます。
/src/com.example.bizcard/RegistActivity.java
部品の割り当て毎回書いてるけど面倒だな・・・
なんかView.setTexts(R.layout.regist, bizCard)とか無いのか?
元記事ではIntent intent = getIntent();と一旦インテントをオブジェクト化してから突っ込んでいますが、使い回す必要がなければメソッドチェーンした方がインポートの必要もないし手っ取り早いです。
てか、前回のShowActivity.javaではそう書いてるんですがね。
どっちの書き方でも同じなので問題無いんですが、ひとつのアプリ内では書き方は揃えておいた方がいいと思う。
それ以外は単に/res/layout/regist.xmlから部品を特定し、インテントがあればその値を突っ込んでいるだけです。
幸いIntent.getSerializableExtra()は取得できなくても例外を吐いたりしないようです。
無事インテントが引き継がれるようになりました。
Androidの記事
前回は名刺詳細画面から詳細編集画面への遷移を作成しました。
今回は詳細編集画面にインテントが送られてきたらその値をデフォルト表示するようにします。
実は名刺一覧画面からやってくる新規登録画面と、名刺詳細画面からやってくる詳細編集画面は同じです。
入力は単にインテントがあれば初期値にして、インテントがなければ空っぽのままにします。
問題は保存で、更新と新規登録を見極めなければなりません。
が、実は前BizCardDaoを作成したときに、bizCard.rowidがあれば更新、無ければ新規登録というふうにしていました。
つまりインテントからrowidを持ち回しておけば問題無いということです。
ということでRegistActivityを実装。
まずは新規/保存/削除を選択させるメニューを作成。
/res/menu/regist.xml
<?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 保存メニュー --> <item android:id="@+id/menu_save" android:title="@string/menu_save" android:icon="@android:drawable/ic_menu_save" /> <!-- 削除メニュー --> <item android:id="@+id/menu_delete" android:title="@string/menu_delete" android:icon="@android:drawable/ic_menu_delete" /> </menu>
ただ、これは保存/削除メニューを一気に作ってるんだけど、この作りだと絶対に保存/削除は並べて表示するしかなくなります。
regist.xml以外に/res/menu/delete.xmlを作って削除メニューはそっちに持っていった方が自由度があるのではないだろうかと思った。
まあ普通保存と削除は並んでるだろうからまあいいか。
先に「保存しました」とかの文言を作成しておく。
/res/values/strings.xml
<string name="saved">保存しました</string> <string name="confirm_delete">本当に削除しますか?</string> <string name="deleted">削除しました</string> <string name="error_required">必須項目を入力してください</string>
インテントがあればそこから値を取得し、なければ単に新規登録するようにRegistActivity.onCreate()を書き換えます。
/src/com.example.bizcard/RegistActivity.java
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.regist); // 部品の取得 personNameText = (EditText)findViewById( R.id.personNameText); companyNameText = (EditText)findViewById( R.id.companyNameText); organizationNameText = (EditText)findViewById( R.id.organizationNameText); positionNameText = (EditText)findViewById( R.id.positionNameText); zipCodeText = (EditText)findViewById( R.id.zipCodeText); addressText = (EditText)findViewById( R.id.addressText); mapButton = (ImageButton)findViewById( R.id.mapButton); tel1Text = (EditText)findViewById( R.id.tel1Text); tel2Text = (EditText)findViewById( R.id.tel2Text); mailText = (EditText)findViewById( R.id.mailText); // ボタンにマップ表示用のリスナー追加 mapButton.setOnClickListener( this); // インテントを取得 bizCard = (BizCard)getIntent().getSerializableExtra( BizCard.TABLE_NAME); // インテントが見つかれば値を設定する if( bizCard != null){ personNameText.setText( bizCard.getPersonName()); companyNameText.setText( bizCard.getCompanyName()); organizationNameText.setText( bizCard.getOrganizationName()); positionNameText.setText( bizCard.getPositionName()); zipCodeText.setText( bizCard.getZipCode()); addressText.setText( bizCard.getAddress()); tel1Text.setText( bizCard.getTel1()); tel2Text.setText( bizCard.getTel2()); mailText.setText( bizCard.getMail()); } }
部品の割り当て毎回書いてるけど面倒だな・・・
なんかView.setTexts(R.layout.regist, bizCard)とか無いのか?
元記事ではIntent intent = getIntent();と一旦インテントをオブジェクト化してから突っ込んでいますが、使い回す必要がなければメソッドチェーンした方がインポートの必要もないし手っ取り早いです。
てか、前回のShowActivity.javaではそう書いてるんですがね。
どっちの書き方でも同じなので問題無いんですが、ひとつのアプリ内では書き方は揃えておいた方がいいと思う。
それ以外は単に/res/layout/regist.xmlから部品を特定し、インテントがあればその値を突っ込んでいるだけです。
幸いIntent.getSerializableExtra()は取得できなくても例外を吐いたりしないようです。
無事インテントが引き継がれるようになりました。
Androidの記事
http://codezine.jp/article/detail/4842?p=3
前回の続き。
前、名刺一覧画面にメニューを実装しましたが、今回は名刺詳細画面にメニューを実装し、詳細編集画面への遷移を作成してみます。
基本は同じですが、今回はアクティビティ呼び出し部分がstartActivity()ではなくstartActivityForResult()になっているところが違います。
/res/menu/show.xml
/src/com.example.bizcard/ShowActivity.java
オプションメニューは他のメソッド類とは独立しているので単に追加するだけです。
onCreateOptionsMenu()にて作成した/res/menu/show.xmlをセット。
onOptionsItemSelected()でIDをチェックし、クリックされたのが"@+id/menu_edit"であればRegistActivityを実行。
ここで実行時にstartActivityForResult()を使用すると、移動先から戻ってきた際にonActivityResult()が実行されます。
onActivityResult()では単に返却値=RESULT_OKであればリロードを行っているだけです。
RESULT_OKという定数がいきなり登場しますが、Activity.RESULT_OKで定義されています。
他にRESULT_CANCELEDとRESULT_FIRST_USERが予め用意されているので、返り値としてこれを使用するのがわかりやすいでしょう。
で、RESULT_FIRST_USERってどういう意味だ?
startActivityForResult()の第二引数-1がわからないんですが何これ。
単に使わないから適当な値入れてるだけ?
Androidの記事
前回の続き。
前、名刺一覧画面にメニューを実装しましたが、今回は名刺詳細画面にメニューを実装し、詳細編集画面への遷移を作成してみます。
基本は同じですが、今回はアクティビティ呼び出し部分がstartActivity()ではなくstartActivityForResult()になっているところが違います。
/res/menu/show.xml
<?xml version="1.0" encoding="utf-8" ?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 編集メニュー --> <item android:id="@+id/menu_edit" android:title="@string/menu_edit" android:icon="@android:drawable/ic_menu_edit" /> </menu>
/src/com.example.bizcard/ShowActivity.java
/** * オプションメニューの生成 */ @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.show, menu); return true; } /** * オプションメニューをクリックした */ @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); switch (itemId) { // 更新 case R.id.menu_edit: // 現在表示しているBizCardを詰めてRegistActivityを実行 Intent registIntent = new Intent( this, RegistActivity.class); registIntent.putExtra( BizCard.TABLE_NAME, bizCard); startActivityForResult( registIntent, -1); } return true; }; /** * startActivityForResult()で飛んでった先から戻ってきた */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if( resultCode == RESULT_OK){ // 表示を更新する BizCardDao dao = new BizCardDao( this); bizCard = dao.load( bizCard.getRowid()); updateView(); } }
オプションメニューは他のメソッド類とは独立しているので単に追加するだけです。
onCreateOptionsMenu()にて作成した/res/menu/show.xmlをセット。
onOptionsItemSelected()でIDをチェックし、クリックされたのが"@+id/menu_edit"であればRegistActivityを実行。
ここで実行時にstartActivityForResult()を使用すると、移動先から戻ってきた際にonActivityResult()が実行されます。
onActivityResult()では単に返却値=RESULT_OKであればリロードを行っているだけです。
RESULT_OKという定数がいきなり登場しますが、Activity.RESULT_OKで定義されています。
他にRESULT_CANCELEDとRESULT_FIRST_USERが予め用意されているので、返り値としてこれを使用するのがわかりやすいでしょう。
で、RESULT_FIRST_USERってどういう意味だ?
startActivityForResult()の第二引数-1がわからないんですが何これ。
単に使わないから適当な値入れてるだけ?
Androidの記事