【Android】TextViewの文字列にリンクを設定する

TextViewにはautoLinkと言うAttributeがあって、これをつけておけばURLを自動でリンクしてくれるんですが、内部で使っている正規表現がお粗末なのか、単純にテストパターンに含まれてないのか、日本語URLも受け付けるつもりなのか、ともかく2バイト文字も平気で含めてしまう使いにくいやつです。

使いにくいので仕方ありません。作りましょう。

CharacterStyleとSpannableString

TextViewの文字列内に何らかの情報(文字色とか)を付与したい場合はCharacterStyleを使用します。こいつを継承しているサブクラスの名前をつらつらと眺めるだけでもどんなことが出来るのかなんとなくわかると思います。

オレオレCharacterStyleを作る場合はMetricAffectingSpanを継承してね、と書かれていますが、onClickを設定できるClickableSpanと言う抽象クラスがありますし、そいつを継承したURLSpanと言う今回にぴったりの具象クラスもあります。と言うわけで、これを使いましょう。

接尾語としてSpanがついているものを文字列に設定するには、Spannableインターフェースを使います。setSpanメソッドのwhatに当たる部分が○○Spanです。startとendで範囲を指定します。

じゃあこのSpannableをどうやってTextViewに設定するのかって話ですが、結論から言うと、SpannableStringなるものを使用します。

SpannableStringはSpannableとCharSequenceを実装しており、TextView#setText(CharSequence text)を適用できます。

ちなみにSpannableStringBuilderなるものもあるらしいので、適宜使い分けましょう。使い方はまんまStringBuilderです。

他のSpanに関する説明や細かい設定方法などはここがとても詳しいので参考にしてみてください。

Spannable#setSpanで使用するフラグ

setSpanのシグネチャにはflagsと言う引数が設定されています。これはどうもSpanの影響範囲に関係するみたいです。ここに指定するフラグはSpannedインターフェースが定数として保持しています。

なんかやたらといっぱいフラグがあるんですが、SPAN_EXCLUSIVE_EXCLUSIVEを指定しておけば大丈夫です。

MovementMethod

TextView#setTextでSpanをセットしただけではまだ動きません。TextView#setMovementMethodMovementMethodインターフェースを指定する必要があります。

とは言え、この辺はもう細かいことを考えないでLinkMovementMethodと言う具象クラスを指定しましょう。getInstanceメソッドインスタンスを取得できます。

コード

いい加減ドキュメントを読むのにも飽きたので実際にやってみましょう。正規表現を考えるのが面倒なのでここのをお借りします。

String url = "http://outofmem.tumblr.com/";
SpannableString ss = new SpannableString(url);

final Pattern STANDARD_URL_MATCH_PATTERN = Pattern.compile("(http://|https://){1}[\\w\\.\\-/:\\#\\?\\=\\&\\;\\%\\~\\+]+", Pattern.CASE_INSENSITIVE);

Matcher m = STANDARD_URL_MATCH_PATTERN.matcher(url);

while(m.find()) {
    ss.setSpan(new URLSpan(m.group()), m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

txvUrl.setText(ss);
txvUrl.setMovementMethod(LinkMovementMethod.getInstance());

応用

URLSpanのonClickの挙動は「受け取ったURLをインテントでブラウザに投げる」と言うものですが、当然オーバーライドしてやれば変えることが出来ます。

例えばTwitterクライアントを作っていて、「@付きだったら自前のプロフィール用アクティビティに飛ばし、URLだったらブラウザに投げる」なんてことも可能です。例えばって言うか、それがやりたかったんだけども。

String text = "@null http://outofmem.tumblr.com/";
SpannableString ss = new SpannableString(text);

final Pattern TWITTER_PATTERN = Pattern.compile("@[0-9a-zA-Z_]+|(http://|https://){1}[\\w\\.\\-/:\\#\\?\\=\\&\\;\\%\\~\\+]+", Pattern.CASE_INSENSITIVE);

Matcher m = TWITTER_PATTERN.matcher(text);

while(m.find()) {
    String t = m.group();
    
    if(t.startsWith("@")) {
        ss.setSpan(new URLSpan(t) {
            @Override
            public void onClick(View widget) {
                Context cont = widget.getContext();
                Intent intent = new Intent(cont, ProfileActivity.class);
                intent.putExtra("screenName", this.getURL().replace("@", ""));
                cont.startActivity(intent);
            }
        }, m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    } else {
        ss.setSpan(new URLSpan(t), m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
}

txvUrl.setText(ss);
txvUrl.setMovementMethod(LinkMovementMethod.getInstance());

実際には匿名クラスより専用のClickableSpanを実装したクラスを作ったほうがいいのかもしれませんが。

まとめ

CharacterStyleを使用することでTextViewだけでも結構なんとかなります。まぁ、コードはだらだらと長くなってしまうんですが…。

参考