(これはAPI Level 25(Android 7.1.1)のNexus5XおよびAPI Level 23(Android6.0)のNexus5(エミュレータ)で確認した)
TextView
はsetCompoundDrawables(Drawable, Drawable, Drawable, Drawable)
を使うと文字領域の上下左右に一つずつDrawable
を置くことができるが、文章の中に画像を置いたり、例えば右の領域にDrawable
を二つ並べて置くということはできない。そんな時の代替手段の一つにImageSpan
がある。HTMLのimg
タグに相当するものだろうか。
https://developer.android.com/reference/android/text/style/ImageSpan.htmldeveloper.android.com
これを使って、
RT by [アイコン画像] @screen_name
とか、
ユーザ名 @screen_name [認証マーク] [鍵マーク]
のように文字と画像とを一列に並べようとしていたところ、画像の底辺をどこに合わせるか指定する引数にALIGN_BOTTOM
を選ぶ(デフォルト)と上の方に不思議な隙間ができてしまったのでどんな処理をしているのか見てみることにした。軽くぐぐってみると高さが合わない問題はすでに認識されている様子(たとえば、複数行のTextViewでImageSpanが下付きに描画される事象を回避する - Qiita)。この情報にならってDynamicDrawableSpan#draw()
をオーバライドして解決を試みようとしたのだが、問題はどうもDynamicDrawableSpan#getSize()
の方にあるのではないかという気がした。その理由を次から説明する。
DynamicDrawableSpan#draw()
の引数の意味
DynamicDrawableSpan | Android Developersをみると引数についての説明がある。ここで重要なのは次の三つ。
top
: 行(Canvas
)の上辺(ふつう0?)y
: ベースラインbottom
: 行(Canvas
)の底辺
DynamicDrawableSpan#getSize()
では何をやっているのか
Cross Reference: /frameworks/base/core/java/android/text/style/DynamicDrawableSpan.java
(2016/12/19 12:28 追記)親クラスgetSize()
やdraw()
の説明がある:Cross Reference: /frameworks/base/core/java/android/text/style/ReplacementSpan.java
getSize()
の戻り値はint
なのだが、これはdraw()
で描こうとするものの幅がどれだけかという意味。なのでgetDrawable().getBounds().right
を返せばよいだけのはずなのだが、ついでに引数であるPaint.FontMetricsInt
の中身を書き換えている。具体的には次に挙げる変数の値を書き換えているのだが、これはdraw()
で描こうとするものの高さ方向のデータになる。
top
: グリフの中で最も背の高い文字の、ベースラインから上側の距離(負の数)ascent
: 一つの文字で推奨されるベースラインから上側の距離(負の数)descent
: 一つの文字で推奨されるベースラインから下側の距離(正の数)bottom
: グリフの中で最も足が長い文字の、ベースラインより下側の距離(正の数)
ちなみにこの説明は、Paint.FontMetrics
の方にしか書いてない。top
やascent
が負の数である理由は、スクリーンの座標系が上から下に向かって大きくなっているためらしい。ここで設定した値に応じてdraw()
のtop
やy
やbottom
の値が決まるようである。
getSize()
の元の処理は、ベースラインから上側の距離をDrawable
の高さと等しくして、ベースラインから下の距離を0に設定している。その上でdraw()
ではbottom
にDrawable
の底辺を揃えて、ALIGN_BASELINE
の時にはさらにベースラインからdescent
の分だけ上に引きあげようという計算をしている。つまり、ベースラインの上側にDrawable
の高さ分のスペースを確保しているのに、それよりさらに低いbottom
の位置に底辺を合わせているので、ALIGN_BOTTOM
の時にはbottom
の分だけDrawable
の上に隙間ができるということである。その行の他の文字がベースラインの下側を使っているために、getSize()
で指定したbottom=0
が却下されてしまうのではないかと思われる。
上の隙間をなくすには
画像の領域をベースラインの上側にのみ確保しようとしているのがそもそもおかしいのではないか。そうしたいのはALIGN_BASELINE
を指定した時だけのはず。なので単純に次のようにしてみた。ポイントはDrawable
のベースラインをどこにするか。getCachedDrawable()
はprivate
メソッドなので再定義する必要がある。
(2016/12/21 22:37:paint.getFontMetrics()
を使って計算するよう修正)
@Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { final Rect bounds = getCachedDrawable().getBounds(); if (fm != null) { final Paint.FontMetrics fontMetrics = paint.getFontMetrics(); final int verticalAlignment = getVerticalAlignment(); if (verticalAlignment == ALIGN_BASELINE) { fm.ascent = -bounds.bottom; } else if (verticalAlignment == ALIGN_BOTTOM) { fm.ascent = (int) (bounds.bottom * fontMetrics.top / (-fontMetrics.top + fontMetrics.bottom)); } fm.descent = Math.max(bounds.bottom + fm.ascent, 0); fm.top = fm.ascent; fm.bottom = fm.descent; } return bounds.right; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int baseline, int bottom, Paint paint) { final Drawable cachedDrawable = getCachedDrawable(); final int drawableHeight = cachedDrawable.getBounds().bottom; canvas.save(); int transY = top; if (getVerticalAlignment() == ALIGN_BASELINE) { transY = baseline - drawableHeight; } else if (getVerticalAlignment() == ALIGN_BOTTOM) { transY = bottom - drawableHeight; } canvas.translate(x, transY); cachedDrawable.draw(canvas); canvas.restore(); }
例に挙げた先のページでは、複数行のTextView
でline spacingを1.5に設定すると画像が下付き文字のようになってしまうという現象が確認されているが、これはdraw()
の引数であるbottom
が行間の高さも含んでいるためではないかと考えられる。Paint.FontMetricsInt
は行間の高さをleading
という変数名で持っている。このleading
の分だけ上に移動させてやることで意図した通りに表示できるのではないかと思われる。上のソースでいうと
transY = bottom - drawableHeight;
を
transY = bottom - drawableHeight - paint.getFontMetricsInt().leading;
にするという感じ。これは手元では確認していないので間違っているかもしれない。
今後の展望
中央揃えができたらいいなと思っている。一連のソースは[WIP] introduced RetweetUserView to replace RT user container by akihito104 · Pull Request #166 · akihito104/UdonRoad · GitHubに上がっており、そのうちマージされる予定。パッチ書いてAOSPに取り込まれるのが一番いいんだと思うんだけどいろんなケースに対応できるかどうかはちょっと自信がない。でもチャレンジしてみるのもいいのかな。
関係ありそうなissueとか
android - Align text around ImageSpan center vertical - Stack Overflow