TextView中の文字列をクリッカブルに

http://などのhtmlリンクは、TextViewのプロパティを弄れば自動的にリンク貼ってくれるけど、"@hoge"などのユーザID、"#hoge"などのハッシュタグなどもクリッカブルにしないといけない。

TextView中の特定箇所をクリッカブルにするには、CharacterStyleのサブクラスであるClickableSpanを使う。

// ※tvPost…TextView
// ※CSpan…ClickableSpanを継承したクラス

tvPost.setText("@hoge テスト #hoge");
Spannable spannable = (Spannable)tvPost.getText();
String text = tvPost.getText().toString();

// id検索(@hoge)
Pattern patternId = Pattern.compile("(@[^\\p{InHiragana}\\p{InKatakana}\\p{InCJKUnifiedIdeographs}\\s:;()@]+)");
Matcher matcherId = patternId.matcher(text);
			
while(matcherId.find()){
	spannable.setSpan(new CSpan(matcherId.group(1)), matcherId.start(1), matcherId.end(1), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
			
// ハッシュタグ検索(#hoge)
Pattern patternHashtag = Pattern.compile("(#[0-9a-zA-Z]+)");
Matcher matcherHashtag = patternHashtag.matcher(text);

while(matcherHashtag.find()){
	spannable.setSpan(new CSpan(matcherHashtag.group(1)), matcherHashtag.start(1), matcherHashtag.end(1), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
	}

tvPost.setMovementMethod(LinkMovementMethod.getInstance());

ClickableSpanを継承したクラス中のonClickに、IDとハッシュタグがクリックされた時の処理を書いておく。

// CSpan.java

import android.text.style.ClickableSpan;
import android.view.View;

public class CSpan extends ClickableSpan{
	public CSpan(String url) {
		super();
	}
	
	@Override
	public void onClick(View v){
		// TODO:IDがクリックされた場合の処理をここに書く
		// TODO:ハッシュタグがクリックされた場合の処理をここに書く
	}
}

ここまで書いてて思ったけど、onClick内で分岐処理かまさなくてもいいように、ClickableSpan継承クラスは最初からID用とハッシュタグ用で分けておいた方が良さそう。

TableLayout

一ヶ月以上も空けてしまった\(^O^)/ちょっといろいろ忙しかったもんで。

つぶやきをPOST出来るようにはしたし、時間とクライアントも表示出きるようにした。

画像の描画などをViewBinderで実行してたけど、getViewでやるように変更したり、とか。

で、最近気に食わないのが、スクロールで引っかかること。スクロール時にgetView呼んで描画処理やってるから、しょうがないんだけど。

というわけで、タイムラインの表示を、ListViewじゃなくてTableLayoutで行うように変更してみる。最初に一気にテーブルつくって描画しておけば、スクロールで引っかかることはないだろう。

試してみたら、予想通りすんげースムーズ!もうスルッスル。

・・・ただ、最初の描画にものすごい時間掛かる。これが次の課題か・・・。

画像の保存→表示

画像の保存と、画像ファイルの表示を実装しました。

まず、画像ファイルの保存

// SDカードのフォルダパス
// Environment.getExternalStorageDirectory().getPath()でSDカードのルートを取得できる
static final String SD_FILEPATH = "Environment.getExternalStorageDirectory().getPath() + "/AndTwitter/"

// URLからファイル名取得
// 正規表現でファイル名以前を排除
Pattern pattern = Pattern.compile("^http://.+/");
Matcher matcher = pattern.matcher("http://hoge/hoge/hoge.jpg");
String fileName = matcher.replaceAll("");
								
// 画像ファイルが存在しない場合、保存する
String filePath = SD_FILEPATH + fileName;
File imageFile = new File(filePath);
if(!imageFile.exists()){
	URL imageUrl = new URL(text);			        		
	InputStream imageIs = imageUrl.openStream();
	OutputStream imageOs = new FileOutputStream(filePath);
									
	try{
		byte[] buf = new byte[1024];
		int len = 0;
										
		while((len = imageIs.read(buf)) > 0){
			imageOs.write(buf, 0, len);
		}
										
		imageOs.flush();
	}finally{
		imageIs.close();
		imageOs.close();
	}
}

// データベースにファイル名保存
mCv.put(PROFILE_IMAGE_URL, fileName);

ファイル名をデータベースに格納しておいて、表示するときにそのファイル名でアクセスします。

で、これが表示。前エントリのMyViewBinderのcase文です。

case PROFILE_IMAGE_URL_ID:
	File imageFile = new File(SD_FILEPATH + cursor.getString(columnIndex));
	FileInputStream imageFis = new FileInputStream(imageFile);
	Bitmap bm = BitmapFactory.decodeStream(imageFis);
	((ImageView)view).setImageBitmap(bm);
	break;

調べた限りではImageViewに画像ファイルパスを設定して表示できるようなクラスはなかったので
・動的にリソース登録してImageViewにリソースを貼る
・ファイルからビットマップを生成してImageViewに貼る
の2択になったので、後者を選択。

どっちにしてもビットマップなのに、スクロールがなんかスムーズになったな・・・と思ったけど、前回確認したときはデバッグモードだったのかもしれない。

さて、次は"*分前 from AndTwitter"とか表示させるようにしたいので、日付、時間の扱いだな。それが終わったら・・・

そろそろつぶやけるようにしないとマズイよね(笑)

ListViewへの画像表示

これで数時間ハマった。

XMLを取得、データベースを更新した後、

// 全てのデータのカーソルを取得
Cursor c = mDb.query(TABLE, null, null, null, null, null, STATE_ID + " DESC");
        
// ベースクラスにカーソルのライフサイクルを管理させる
startManagingCursor(c);
	        
// データベースのカラムと、リストビューを関連付ける
String[] from = new String[]{NAME, TEXT, PROFILE_IMAGE_URL};
int[] to = new int[]{R.id.TextView_name, R.id.TextView_text, R.id.ImageView_icon};
mAdapter = new SimpleCursorAdapter(this, R.layout.friends_timeline_row, c, from, to); setListAdapter(mAdapter);

PROFILE_IMAGE_URLカラムにはプロフィール画像のURLが入ってて、R.id.ImageView_iconは画像を表示させるつもりのImageView。

こんな感じでListViewに表示させてるんだけど、画像URLをImageViewにAdaptするだけじゃ、当然画像は表示されない。そりゃそーだ。

いろいろ調べたりした結果、SimpleCursorAdapter.ViewBinderのインターフェースを実装、その中でImageViewに画像を与えることにした。

private class MyViewBinder implements SimpleCursorAdapter.ViewBinder{

	@Override
	public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
		try {
			switch(columnIndex)
			{
				case 1:
					break;
				case 2:
					URL imageUrl = new URL(cursor.getString(columnIndex));
					InputStream imageIs = imageUrl.openStream();
					Bitmap bm = BitmapFactory.decodeStream(imageIs);
					((ImageView)view).setImageBitmap(bm);
					break;
				case 3:
					((TextView)view).setText(cursor.getString(columnIndex));
					break;
				case 4:
					((TextView)view).setText(cursor.getString(columnIndex));
					break;
				default:
					break;
			}
			return true;
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
	}
}

case分岐が実数で見苦しいが、case2がPROFILE_IMAGE_URLカラム。setListAdapterの後に、

mAdapter.setViewBinder(new MyViewBinder());

とすることで、画像が表示されるようになりました!

case2,3はただのTextViewだからsetListAdapterの時点で表示されるんだけど、setViewBinderで設定しないでほっとくと、消えてしまう。だから2度手間なんだけど・・・なんかいい方法他にないんかな。

それと画像を表示すると、いままでスムーズだったスクロールが引っかかるようになった。Bitmapだからか?ローカルにjpg等で保存してから与えたほうがいいんだろうか。まあ、表示の度にイチイチURLから取得してBitmap生成して・・・なんてのはアレだから、そのうちそうするつもりだけど。

そのうち、というか次の課題にするか。そうなると直近の課題はデータファイル保存だな。

emulatorでSDカードを使えるようにする

emulatorでSDカードを使う方法、忘れないようにメモ。


1.SDカードのイメージファイルを生成
コマンドラインで以下を実行

>mksdcard 128M sdcard.img

これで128MのSDカードを作ったことになる。


2.AVD Managerから、SDカードをマウントしたemulatorを作成
EclipseAndroid Visual Device Manager(ツールバー左から4番目、携帯端末アイコンのボタン)を開き、新しいAVDを作成する。その際、"SD Card:"の項目において、手順1で作ったイメージファイルを指定する。

以上。簡単。

SQLiteのinsert失敗

そういえばデータベースアクセスで試行錯誤してる時に、どうしてもデータベースへのレコードinsert時にエラーが出てinsert出来ない、っていう現象があったんだけど。

一度.dbファイルを作成した後に、ソースコードのテーブル作成用のSQL文字列を変更して、データベースのcolumnを増やしたんだか減らしたんだかしてしまって。

同名の.dbファイルが存在してる場合は新たに.dbを作成することはしないで、既存の.dbをオープンする、というコードを書いてたもんだから、既存の.dbファイルとは設定の異なるレコードをinsertしようとして、エラーが出てた。

adb shellで.dbファイルを削除、再度アプリを動かしたら上手くいきましたとさ。

や、作ったテーブルと挿入するレコードのcolumnが違うんじゃないか、ていう可能性は真っ先に浮かんだんだけど、ソースコード眺めながら、コード上は間違ってないよなあ・・・と思ってスルーしてたわけ。

そしたらもう.dbが存在するもんだから、コードに書いてあるテーブルは作られてなかったのよね。そりゃinsert出来んわ。

XMLparserなんて知らなかったんだ

HttpUrlConnectionでタイムラインを取得して、Streamで1行ずつ読んでいって、Matcher使って正規表現でタグ抜いてユーザー名やらつぶやきやらを取得したりしてたんだけど

日本語だと、1文字ずつが"&#*****"なんていう特殊文字のままでデータベースに格納されていくんでとても困った。

いろいろ調べていくうちに、"XMLparser"なんて便利なものがあることを知った。javaXMLも初めてだから、こういう知ってて当たり前みたいなことを全然知らんのよね。XMLparserっつーのは要するに、XMLを解読してくれるってこと。

ってことでコードを大幅に変更。XMLparserについては、以下のサイトを参考にさせて頂きました。
http://www.adakoda.com/adakoda/2009/01/android-xmlpullparserxml.html

今は試行錯誤の時期なので、途中のコードとかも残したりしてないから説明も大ざっぱ!まあもともと本人の性格が大ざっぱだから、あんま細かく書いていく気もないんだけど。

で、とりあえず自分のタイムラインを取得、表示してみたよ!


これで、XML取得→データベース保存→アクティビティ表示 っていう基本的な一連の流れはひとまず実装できた。今後は、これにアイコンやら時間やらの情報を増やしていったり、つぶやけるようにしたり、という機能を付け足していこう。

AndTwitterっていうのがソフト名ですいまのところ。かぶってたら変える。