ListViewに含まれるTextViewのリンクをクリックできるようにする。
最近は自由登校期間で学校が休みで殆ど家に引きこもっています。
折角の休みなのでAndroid開発をするためにJavaの勉強をしていたのですが、自分の調べ方が悪いのか思う通りの結果が出てこなかった部分があり、先ほど解決したので備忘録として残しておきます。
起きた問題
ListViewにOnItemClickListenerをセット、ListViewに含まれるTextViewのテキストの一部にリンクを貼るためにURLSpan(ClickableSpanでもいい)をセット。
この状態で、
・TextViewをクリックするとリンクをクリックしたかどうがにかかわらず、ListViewにセットしたOnItemClickListenerが反応しない。
・行末がリンクの場合、リンクがTextViewの端まで及ぶ。
などの問題が起こる。
原因
タッチイベントは親Viewから子Viewに伝搬され、子Viewで処理した場合そこでタッチイベントが終了します。
また、子Viewで処理しなかった場合、子Viewがあれば子Viewにイベントを伝え、無ければ親Viewにイベントを伝えます。
要するに、親からイベントが伝わり、子から処理される仕組みらしいです。
自分の曖昧な理解を書いたため、これらについて間違いがあるかもしれません。
詳しくは、 Android のタッチイベントを理解する(その1) - Unmotivated が参考になります。
今回はTextViewにセットしたLinkMovementMethodがそこでイベントを遮っていたようです。
解決策
TextViewのonTouch時の動作を書き換えてあげればいいようです。
TextViewのリンク以外をクリックした時はListViewのクリックに行く方法 - oigamishunta’s blog
android - ListView: TextView with LinkMovementMethod makes list item unclickable? - Stack Overflow
これについてはいくつか解決策がありましたが、今回は「android - ListView: TextView with LinkMovementMethod makes list item unclickable? - Stack Overflow」の2番目の回答を参考にしました。
textView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { CharSequence text = ((TextView) v).getText(); Spannable stext = Spannable.Factory.getInstance().newSpannable(text); TextView widget = (TextView) v; int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); x -= widget.getTotalPaddingLeft(); y -= widget.getTotalPaddingTop(); x += widget.getScrollX(); y += widget.getScrollY(); Layout layout = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] link = stext.getSpans(off, off, ClickableSpan.class); if (link.length != 0) { if (action == MotionEvent.ACTION_UP) { link[0].onClick(widget); } return true; } } return false; } });
「参考にしました」、というかほぼそのままです。
これで、「TextViewをクリックするとリンクをクリックしたかどうがにかかわらず、ListViewにセットしたOnItemClickListenerが反応しない。」という問題が解決しました。
しかし、このままだとリンクの範囲が少しおかしなことになり、行末がリンクの場合、リンクがTextViewの端まで及んでしまうようです。
というか、
SpannableString ss = new SpannableString(text); ss.setSpan(new URLSpan("URLな文字列"), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); textView.setText(ss); textView.setMovementMethod(LinkMovementMethod.getInstance());
みたいにした場合、「行末がリンクの場合、リンクがTextViewの端まで及ぶ。」ことになると思う。
解決策のコードでgetSpansしてる辺り(=リンクを取得してる辺り)を見てみます。
SpannableStringBuilder | Android Developers 英語よくわからないけどgetSpansの部分を見ると、queryStartとqueryEndに重なるspansを返すみたいですね。
getSpans(off, off, ClickableSpan.class);の部分は問題なさそうなのでoffとlineについて見てみます。
Layout | Android Developers getLineForVerticalは引数で指定した垂直位置に存在する行番号を、getOffsetForHorizontalは引数で指定した行番号の行に存在する文字の中で、引数で指定した水平位置に最も近い文字の文字位置を返すみたいです。
getOffsetForHorizontalの最も近い文字の位置を返す部分に問題がありそうです。
Layout | Android Developersについて詳しく見てみたら、getLineMaxというメソッドがありました。
引数で行番号を指定するとその行の長さを返すみたいです。
getLineMaxで行の長さを取得し、これがクリックしたTextViewの位置より小さく、尚且つクリックした位置がリンクであればリンククリック時の処理をさせれば上手く行きそうです。
textView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { CharSequence text = ((TextView) v).getText(); Spannable stext = Spannable.Factory.getInstance().newSpannable(text); TextView widget = (TextView) v; int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); x -= widget.getTotalPaddingLeft(); y -= widget.getTotalPaddingTop(); x += widget.getScrollX(); y += widget.getScrollY(); Layout layout = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); if(x < layout.getLineMax(line)) { ClickableSpan[] link = stext.getSpans(off, off, ClickableSpan.class); if (link.length != 0) { if (action == MotionEvent.ACTION_UP) { link[0].onClick(widget); } return true; } } } return false; } });
if(x < layout.getLineMax(line)) {}
これを加えただけですが、これで「行末がリンクの場合、リンクがTextViewの端まで及ぶ。」という問題も解決することが出来ました。
感想
久々に文章を書いたのでつらかった。
英語が読めないのでつらい。
語彙力の低下を感じたのでつらい。
2進数を64進数に変換する
2進数を64進数に変換しようと思ったんだけど検索してもいい感じに出てこなかったから自分で書いた。
コード
<?php $chars = array_merge(range(0,9), range('a','z'), range('A','Z'), array('+','/')); $b64map = []; foreach($chars as $k => $c) { $b64map[bn(decbin($k), 6)] = $c; } function binToB64($binnum) { $chars = array_merge(range(0,9), range('a','z'), range('A','Z'), array('+','/')); $b64map = []; foreach($chars as $k => $c) { $b64map[bn(decbin($k), 6)] = $c; } $length = strlen($binnum); for($m = 0; $m < $length; $m = $m + 6) {} $bin = bn($binnum, $m); $binArray = str_split($bin, 6); $b64 = ""; foreach($binArray as $b) $b64 .= $b64map[$b]; return $b64; } function bn($binnum, $n) { $length = strlen($binnum); $l = $n - $length; $zero = ""; for($i = 0; $i < $l; $i++) $zero .= "0"; return $zero .$binnum; } ?>
仕組み
汚いコードなので忘れた時に備えて仕組みも書いておく。
まず2進数と64進数の変換表を作る。
2進数の桁が6n桁でない場合桁が6n桁になるまで上位桁を0で埋める。
次に文字列の2進数を6桁毎に区切る。
区切られた2進数をそれぞれ64進数に変換して結合する。
終わり。
感想
2も64も基数が2で揃っているので2進16進変換と同じ要領で変換できた。
デコード部分は今のところ使う予定が無いので作ってません。
より良い方法があれば教えてください。
人感センサーが届いたので遊んだ
この間買った人感センサーが届いた。
はてブロを始めて2週間経ったわけだけど、このまま記事を書かないとブログの存在を忘れそうなのでちょっとしたことでも記事にすることにした。
人感センサー
名前の通りだけど動いてるものならなんでも反応したのでモーションセンサーって言ったほうがいいと思う。
ちなみにこれを買った。
360円程で安かったから2個買ったけど明細を見たら送料で540円取られてた。
サイズは3.2cm x 2.4cmとかなり小さい。
可変抵抗器によってセンシング距離(左)と遅延時間(右)を調節できるみたい。
使ってみた
一応、コードも載せておく。
コード
int pir = 2; int led = 13; int state = LOW; void setup() { pinMode(pir, INPUT); pinMode(led, OUTPUT); Serial.begin(9600); } void loop(){ if(digitalRead(pir)) { if(state == LOW) { Serial.println("on"); state = HIGH; } } else { if (state == HIGH){ Serial.println("off"); state = LOW; } } digitalWrite(led, state); }
繋いだ。
手をかざすと光る。
楽しい。
感想
センサー系楽しい。
Arduinoは去年、室温を計測してTwitterで呟く物を作って以来触ってなかったのでこれを機会に人感センサーを使った何かを作ってみたい。
見たまま編集を続けるのもなんだったのでMarkdownを少し勉強したけどその時間のほうが人感センサーで遊んでいた時間より長かった。