Javaのリフレクションを使ってprivateなクラスを活用しカスタムクラスを作る
privateなパッケージやクラスの中にあるメソッドやフィールドを使ってカスタムクラスを作ったときにハマった部分など備忘録
参考:リフレクション
クラスのカスタムなどしたいとき、全てコピーするのは面倒なときなどにリフレクションをつかいます。JavaやAndroidのAPIがprivateな場合でも取得できるようなAPIが用意されています。
今回はTextViewに表示される文字列にLinkifyを使ってリンクを作成したときのクリックイベントを取るためのカスタムです。
Linkifyをリフレクションする
TextViewに入る文字列で、URLなどをリンクにする方法は二つある。
一つ目はxmlでautoLinkを指定する
<TextView
android:id="@+id/link"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:autoLink="web" />
もう一つはLinkifyを使ってaddLinkする方法。
TextView textview = (TextView) findViewById(R.id.link_textview);
textview.setText("http://google.com");
Linkify.addLinks(textview, Linkify.ALL);
autoLinkもLinkifyもリンクにできる種類は以下。
| 種類 | 内容 |
|---|---|
| ALL | リンクにできるものは全てする |
| EMAIL_ADDRESS | メールアドレス |
| MAP_ADDRESS | 住所 |
| PHONE_NUMBER | 電話番号 |
| WEB_URLS | web |
LinkifyをカスタムしてOnClickイベントを各URLに仕込めるようにする。
さくっとLinkifyクラスを継承してきて作ろうと思うと、packageがprivateなので、マスクとaddLinksしか呼び出せない仕様になっている。( ˘ω˘)
まるっと完コピしてもいいのですが、不要な処理は元のクラスにやってもらって、必要なインターフェースだけを提供するようにします。
今回はwebのURLをリンクに修正するようにします。
中を見てみると、必要そうなメソッドはaddLinksとaddLinkMovementMethodとapplyLinkとgatherLinkとpruneOverlapsで、sUrlMatchFilterフィールドがあればURLをリンクにできます。
重要なのは二つで
- privateなjavaファイル内にあるクラスにアクセスする
- applyLinkの中にあるURLSpan
です。
- privateなjavaファイル内にあるクラスにアクセスする
Linkify.javaにはリンクとなる文字列をLinkSpecというクラスでもつのですが、
privateパッケージかつjavaファイル内にあるため普通にはアクセスできないのが面倒です。
public class Linkify {
…
}
//どうやってこいつ取得すんねん…
class LinkSpec {
String url;
int start;
int end;
}
方法は最初から言っているリフレクションを使うのですが、LinkSpecクラスを使用しているメソッドをリフレクションして、
その結果をObjectに渡し、フィールドにアクセスすると取得可能です。以下がprivateなクラスのfieldへアクセスする部分。
//LinkSpecクラスからfieldを取得
for( Object link : links ) {
Class clazz = link.getClass();
String url = null;
int start = 0;
int end = 0;
//privateなクラスからfieldを取得してリンクを適用
for( Field field : clazz.getDeclaredFields() ) {
//privateなfieldへのアクセスを可能にする
field.setAccessible( true );
try {
if( field.getName().equals( "url" ) ) {
url = (String) field.get( link );
}
else if( field.getName().equals( "start" ) ) {
start = field.getInt( link );
}
else if( field.getName().equals( "end" ) ) {
end = field.getInt( link );
}
}
catch( IllegalAccessException e ) {
e.printStackTrace();
}
}
applyLink( url, start, end, text );
}
途中setAccessible( true )というところでアクセス権限を与えないと、エラーも無く結果が帰ってこないので注意。
そんなこんなで作成したのが以下のクラス。
/**
* {@link android.text.util.Linkify}のリフレクションクラス
*/
public class LinkifyProxy extends Linkify {
public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
public final boolean acceptMatch( CharSequence s, int start, int end ) {
return start == 0 || s.charAt( start - 1 ) != '@';
}
};
private static Linkify mLinkify = null;
private LinkifyProxy() {}
//Linkifyインスタンスを生成(singleton)
private static Linkify getLinkify() {
if( mLinkify == null ) {
mLinkify = new Linkify();
return mLinkify;
}
return mLinkify;
}
//リフレクションしたメソッドを実行
private static Object invokeMethod( Linkify linkify, String methodName, Class[] clazz, Object... args ) {
Method method;
Object object = null;
try {
method = linkify.getClass().getDeclaredMethod( methodName, clazz );
method.setAccessible( true );
object = method.invoke( new ArrayList<Object>(), args );
}
catch( NoSuchMethodException e ) {
e.printStackTrace();
}
catch( IllegalAccessException e ) {
e.printStackTrace();
}
catch( InvocationTargetException e ) {
e.printStackTrace();
}
return object;
}
//提供するインターフェースに応じてpublicにしたりしなかったり
private static boolean addClickEventLinks( Spannable text, int mask ) {
LogUtil.min();
if( mask == 0 ) {
return false;
}
URLSpan[] old = text.getSpans( 0, text.length(), URLSpan.class );
for( int i = old.length - 1; i >= 0; i-- ) {
text.removeSpan( old[i] );
}
ArrayList links = new ArrayList();
if( (mask & WEB_URLS) != 0 ) {
gatherLinks( links, text, Patterns.WEB_URL, new String[] {"http://", "https://", "rtsp://"}, sUrlMatchFilter, null );
}
pruneOverlaps( links );
if( links.size() == 0 ) {
return false;
}
//LinkSpecクラスからfieldを取得
for( Object link : links ) {
Class clazz = link.getClass();
String url = null;
int start = 0;
int end = 0;
//privateなクラスからfieldを取得してリンクを適用
for( Field field : clazz.getDeclaredFields() ) {
//privateなfieldへのアクセスを可能にする
field.setAccessible( true );
try {
if( field.getName().equals( "url" ) ) {
url = (String) field.get( link );
}
else if( field.getName().equals( "start" ) ) {
start = field.getInt( link );
}
else if( field.getName().equals( "end" ) ) {
end = field.getInt( link );
}
}
catch( IllegalAccessException e ) {
e.printStackTrace();
}
}
applyLink( url, start, end, text );
}
return true;
}
//提供するインターフェースはaddLinksと同じ
public static boolean addClickEventLinks( TextView text, int mask ) {
if( mask == 0 ) {
return false;
}
CharSequence t = text.getText();
if( t instanceof Spannable ) {
//変更
if( addTrackingLinks( (Spannable) t, mask ) ) {
addLinkMovementMethod( text );
return true;
}
return false;
}
else {
SpannableString s = SpannableString.valueOf( t );
if( addTrackingLinks( s, mask ) ) {
addLinkMovementMethod( text );
text.setText( s );
return true;
}
return false;
}
}
//リフレクション
private static void addLinkMovementMethod( TextView t ) {
Linkify linkify = getLinkify();
Class[] clazz = { TextView.class };
invokeMethod( linkify, "addLinkMovementMethod", clazz, t );
}
//ここが重要CustomしたURLSpanを渡すことでClickイベントをハンドリングできるようにする
private static void applyLink( String url, int start, int end, Spannable text ) {
CustomURLSpan customURLSpan = CustomURLSpan.createURL( url );
text.setSpan( customURLSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
}
//リフレクション
private static void gatherLinks( ArrayList links, Spannable s,
Pattern pattern, String[] schemes,
MatchFilter matchFilter, TransformFilter transformFilter ) {
Linkify linkify = getLinkify();
Class[] clazz = {ArrayList.class, Spannable.class,
Pattern.class, String[].class, MatchFilter.class, TransformFilter.class};
invokeMethod( linkify, "gatherLinks", clazz, links, s, pattern, schemes, matchFilter, transformFilter );
}
//リフレクション
private static void pruneOverlaps( ArrayList links ) {
Linkify linkify = getLinkify();
Class[] clazz = {ArrayList.class};
invokeMethod( linkify, "pruneOverlaps", clazz, links );
}
}
ここで重要なポイントの二つ目
- applyLinkの中にあるURLSpan
を確認します。
Linkifyクラスでは次のようになっています。
private static final void applyLink(String url, int start, int end, Spannable text) {
URLSpan span = new URLSpan(url);
text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
なにやらこのURLSpanがURLの文字列を持ち、TextView内のどのURLかを区別するためにLinkSpecクラスが持っていた
intのstartとendを渡しているようです。ではURLSpanを確認してみます。
URLSpanをカスタムする
public class URLSpan extends ClickableSpan implements ParcelableSpan {
private final String mURL;
public URLSpan(String url) {
mURL = url;
}
public URLSpan(Parcel src) {
mURL = src.readString();
}
public int getSpanTypeId() {
return TextUtils.URL_SPAN;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mURL);
}
public String getURL() {
return mURL;
}
@Override
public void onClick(View widget) {
Uri uri = Uri.parse(getURL());
Context context = widget.getContext();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
context.startActivity(intent);
}
}
確認してみると、onClickイベントをオーバーライドしているので、
このクラスを継承してonClickをオーバーライドすればClickイベント時に自由な処理を行えそうです。
よって以下のCustomクラスを作成しました。
/**
* URLをクリックしたときにイベントを変更できる{@link android.text.style.URLSpan}カスタムクラス
*/
public class CustomURLSpan extends URLSpan {
private String mURL;
//applyLinkがstaticなのでついでにFactoryMethodパターンへ変更
public static CustomURLSpan createURL( String url ) {
return new CustomURLSpan( url );
}
private CustomURLSpan( String url ) {
super( url );
mURL = url;
}
@Override
public void onClick( View widget ) {
super.onClick( widget );
Toast toast = Toast.makeText( widget.getContext(), mURL, Toast.LENGTH_SHORT );
toast.show();
}
}
これでクリックしたらToastが出力されるようになります。あとはお好みで編集してください。
リンクが複数ある場合に、どのリンクを押したかをわかるようにするなどもいいかもしれません。
