ReplacementSpanでTextViewの一部を装飾する

例えばTextViewの一部をリンクにしたり、一部を強調したい、などの時にTextViewの一部に装飾したいときってありますよね？
実はTextViewはSpannableStringを使うことにより、一部を装飾したり、リンクにしたりといったことは簡単にできます。

詳しい内容は @wasabeef_jp さんの投稿 [Android Advanced TextView](Android Advanced TextView) にまとまっているのでそちらを参照してください。

今回は、その中で `ReplacementSpan` を使って背景や点線の下線の描画などをやりたいと思います。

といってもとても簡単で、 `ReplacementSpan` を継承したクラスを作り、 `draw` メソッドをオーバーライドして上げてそこで `Canvas` に描画してあげるだけです。
ね、簡単でしょう？

# サンプル
文章だけでもいいかな、と思ったんですがてきとーに実装もしたんで紹介します。
手順としては、先に下線や背景を描画したあとに、Textを描画してあげます。

## スクリーンショット
上から、順に

- 点線の下線
- 背景色と角丸のborder
- `drawable` の背景画像

<img width="380" alt="スクリーンショット 2016-09-05 16.34.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/103069/58987109-0f58-97ac-ac4b-543eb372ac1d.png">

## サンプルコード
今回実装したものは以下のようになります。 

```java
public class BackgroundSpan extends ReplacementSpan {

    public static final int TYPE_DASH_UNDERLINE = 1;

    public static final int TYPE_BORDER_RECT = 2;

    public static final int TYPE_BACKGROUND_IMAGE = 3;

    private int type;

    private Bitmap bitmap;

    public BackgroundSpan(int type) {
        this.type = type;
    }

    public BackgroundSpan(int type, Bitmap bitmap) {
        this.type = type;
        this.bitmap = bitmap;
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return (int) paint.measureText(text, start, end);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        RectF rectF = new RectF(x, top, x + measureText(paint, text, start, end), bottom - 5);
        paint.setStrokeWidth(2.0f);

        switch (type) {
            case TYPE_DASH_UNDERLINE:
                drawDashUnderline(canvas, paint, rectF);
                break;
            case TYPE_BORDER_RECT:
                drawBorderRect(canvas, paint, rectF);
                break;
            case TYPE_BACKGROUND_IMAGE:
                drawBackgourdImage(canvas, paint, rectF);
                break;
        }

        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.BLACK);
        canvas.drawText(text, start, end, x, y, paint);
    }

    // 点線の下線を描画するメソッド
    private void drawDashUnderline(Canvas canvas, Paint paint, RectF rectF) {
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.BLACK);
        float interval = 10;
        paint.setPathEffect(new DashPathEffect(new float[]{interval, interval}, 0));
        canvas.drawLine(rectF.left, rectF.bottom, rectF.right, rectF.bottom, paint);
    }

    // 背景色と角丸のborderを描画するメソッド
    private void drawBorderRect(Canvas canvas, Paint paint, RectF rectF) {
        int cornerRadius = 10;
        paint.setColor(Color.LTGRAY);
        canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint);

        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.BLACK);
        paint.setAntiAlias(true);
        canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint);
    }

    // 背景画像を描画するメソッド
    private void drawBackgourdImage(Canvas canvas, Paint paint, RectF rectF) {
        Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        canvas.drawBitmap(bitmap, rect, rectF, paint);
    }

    private float measureText(Paint paint, CharSequence text, int start, int end) {
        return paint.measureText(text, start, end);
    }
}
```

ここで実装したクラスを以下のように `TextView` にセットしてあげると適応されます。

```java
final String DUMMY_TEXT = "これはダミーテキストです。";
SpannableStringBuilder builder = new SpannableStringBuilder();

builder.append(DUMMY_TEXT);
builder.setSpan(new BackgroundSpan(BackgroundSpan.TYPE_DASH_UNDERLINE), 3, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
binding.textView1.setText(builder);
binding.textView1.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

builder = new SpannableStringBuilder();
builder.append(DUMMY_TEXT);
builder.setSpan(new BackgroundSpan(BackgroundSpan.TYPE_BORDER_RECT), 3, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
binding.textView2.setText(builder);

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), android.R.drawable.sym_def_app_icon);
builder = new SpannableStringBuilder();
builder.append(DUMMY_TEXT);
builder.setSpan(new BackgroundSpan(BackgroundSpan.TYPE_BACKGROUND_IMAGE, bitmap), 3, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
binding.textView3.setText(builder);
```

以上です。なにか他にわからない点や、間違っている点があれば、気軽にコメントしていただけると幸いです。

# 参考
[Android Advanced TextView](Android Advanced TextView)