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の端まで及ぶ。」という問題も解決することが出来ました。

感想

久々に文章を書いたのでつらかった。

英語が読めないのでつらい。

語彙力の低下を感じたのでつらい。

JavaAndroid開発はたのしいと思う。

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週間経ったわけだけど、このまま記事を書かないとブログの存在を忘れそうなのでちょっとしたことでも記事にすることにした。

人感センサー

名前の通りだけど動いてるものならなんでも反応したのでモーションセンサーって言ったほうがいいと思う。

ちなみにこれを買った。

人感センサー 〈 Arduino 〉

人感センサー 〈 Arduino 〉

 

360円程で安かったから2個買ったけど明細を見たら送料で540円取られてた。

サイズは3.2cm x 2.4cmとかなり小さい。

f:id:lightnet328:20140910183531j:plain

f:id:lightnet328:20140910183541j:plain

可変抵抗器によってセンシング距離(左)と遅延時間(右)を調節できるみたい。

f:id:lightnet328:20140910183603j:plain

使ってみた

一応、コードも載せておく。

コード

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);
}

繋いだ。

f:id:lightnet328:20140910183644j:plain

手をかざすと光る。

f:id:lightnet328:20140910183659j:plain

楽しい。

感想

センサー系楽しい。
Arduinoは去年、室温を計測してTwitterで呟く物を作って以来触ってなかったのでこれを機会に人感センサーを使った何かを作ってみたい。

見たまま編集を続けるのもなんだったのでMarkdownを少し勉強したけどその時間のほうが人感センサーで遊んでいた時間より長かった。

ブログ始めました

今までWordpressでブログを書いてた(あんまり書いてなかった)けどテンプレートから何まで自分で作らないと気がすまない性分で、一つ記事書くのにも他の気になったところを弄る羽目になってとても面倒だったのではてブロを始めて前のブログを消しましたとさ。

 

前みたいにブログに書くネタが無いっつって一年過ごさないようにしたいですね。

終わり