12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Javaのリフレクションを使ってprivateなクラスを活用しカスタムクラスを作る

12
Last updated at Posted at 2014-11-19

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

autoLinkLinkifyもリンクにできる種類は以下。

種類 内容
ALL リンクにできるものは全てする
EMAIL_ADDRESS メールアドレス
MAP_ADDRESS 住所
PHONE_NUMBER 電話番号
WEB_URLS web

LinkifyをカスタムしてOnClickイベントを各URLに仕込めるようにする。

さくっとLinkifyクラスを継承してきて作ろうと思うと、packageがprivateなので、マスクとaddLinksしか呼び出せない仕様になっている。( ˘ω˘)

スクリーンショット 2014-11-18 18.49.18.png

まるっと完コピしてもいいのですが、不要な処理は元のクラスにやってもらって、必要なインターフェースだけを提供するようにします。
今回はwebのURLをリンクに修正するようにします。

中を見てみると、必要そうなメソッドはaddLinksaddLinkMovementMethodapplyLinkgatherLinkpruneOverlapsで、sUrlMatchFilterフィールドがあればURLをリンクにできます。

重要なのは二つで

  • privateなjavaファイル内にあるクラスにアクセスする
  • applyLinkの中にあるURLSpan

です。

  • privateなjavaファイル内にあるクラスにアクセスする

Linkify.javaにはリンクとなる文字列をLinkSpecというクラスでもつのですが、
privateパッケージかつjavaファイル内にあるため普通にはアクセスできないのが面倒です。

Linkify.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 )というところでアクセス権限を与えないと、エラーも無く結果が帰ってこないので注意。

そんなこんなで作成したのが以下のクラス。

LinkifyProxy.java
/**
 * {@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のstartendを渡しているようです。ではURLSpanを確認してみます。

URLSpanをカスタムする

URLSpan.java
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クラスを作成しました。

CustomURLSpan.java
/**
 * 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が出力されるようになります。あとはお好みで編集してください。

リンクが複数ある場合に、どのリンクを押したかをわかるようにするなどもいいかもしれません。

12
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?