複数行のTextViewでImageSpanが下付きに描画される事象を回避する

  • 18
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

なんかやけに苦労したのでメモしておく。

2015-03-09 01.50.56 自作ImageSpanとの比較.png

前提

拙作の TwitPane では絵文字を Html.fromHtml の img タグとして(つまり ImageSpan として)描画している。
そのTextViewの行間を広げ(150%)、複数行表示する中に絵文字が混在していると下図の通り絵文字の縦位置がズレてしまった。

2015-03-09 02.19.18s.png

具体的には1行の場合(「よろしくお願いします」の後ろの「!」)は問題ないんだけど、3行の場合は1行目と2行目にある絵文字が下付きで描画されてしまっている。

ちなみに行間の設定は textView.setLineSpacing(0, 1.5f) のように設定している。

調査とか修正とか

BitmapDrawable.setGravityが必要?

まずはBitmapDrawableの作り方(いわゆるImageGetterの実装)のほうで調べてみたんだけど、
この記事で言及されているように setGravity(Gravity.TOP) を指定したが改善しなかった。

ALIGN_BASELINE を指定してみる

次に ImageSpan をよく調べるとコンストラクタに ImageSpan.ALIGN_BASELINE が指定できるようなので、(残念ながら Html.fromHtml からはどう頑張っても指定できないんだけど、ともかく試しに) ALIGN_BASELINE にしてみたのが下図の青と緑の四角。

画像1.png

確かに1行の場合は BASELINE にあわせて描画されてるんだけど、依然として複数行の1行目は下付きっぽいまま(若干上にズレたけど)。

ImageSpan(<DynamicDrawableSpan) の描画部分を置き換える

ImageSpan の実際の描画処理はスーパークラスの DynamicDrawableSpan が行っているのでその draw メソッドを確認。

DynamicDrawableSpan.java
    @Override
    public void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x, 
                     int top, int y, int bottom, Paint paint) {
        Drawable b = getCachedDrawable();
        canvas.save();

        int transY = bottom - b.getBounds().bottom;
        if (mVerticalAlignment == ALIGN_BASELINE) {
            transY -= paint.getFontMetricsInt().descent;
        }

        canvas.translate(x, transY);
        b.draw(canvas);
        canvas.restore();
    }

描画エリアのY方向の設定を transY で行ってるんだけど、これを引数の bottom を基準に設定している。
これが微妙に間違ってるようで、y を基準にする必要があるみたい。

ありがたいことに(当たり前だけど)drawメソッドがpublicなのでこれを置き換える感じで ImageSpan を自作して修正してみた。

EmojiImageSpan.java
class EmojiImageSpan extends ImageSpan {
    public EmojiImageSpan(Drawable d, String src) {
        super(d, src);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, Paint paint) {
        Drawable b = getCachedDrawable();
        canvas.save();

//        int transY = bottom - b.getBounds().bottom;
//        transY -= paint.getFontMetricsInt().descent;
        int transY = y - b.getBounds().bottom;
        transY += paint.getFontMetricsInt().descent;

        canvas.translate(x, transY);
        b.draw(canvas);
        canvas.restore();
    }

    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;


        if (wr != null)
            d = wr.get();

        if (d == null) {
            d = getDrawable();
            mDrawableRef = new WeakReference<Drawable>(d);
        }

        return d;
    }

    private WeakReference<Drawable> mDrawableRef;
}

すると下図の青四角の通り、いい感じの高さに描画できるようになった。

画像4.png

というわけで Html.fromHtml は諦めて、絵文字部分を自作ImageSpanに差し替えることでこの事象は回避できるようです。

補足:絵文字の正しい描画位置

Twitter絵文字の描画は https://github.com/twitter/twemoji#inline-styles にあるように上方向に -10% ずらす必要がある(あと左側に5%、右側に10%のマージンも)。

img.emoji {
   height: 1em;
   width: 1em;
   margin: 0 .05em 0 .1em;
   vertical-align: -0.1em;
}

これらも踏まえて位置を調整してあげる必要がありますね。

  • 修正前
    2015-03-09 02.19.18s.png

  • 修正後
    2015-03-09 06.28.57s.png