ImageSpan で画像を TextView に 追加したいが、改行があってかつ、lineSpacingMultiplier、lineSpacingExtra を使っていると縦中央にならない

TextView の左に画像を設置するには

  • TextView の左に画像を設定したい、まず最初に考えるのdrawableLeftを使って画像追加し、パディングなどを調整する。
<!--?xml version="1.0" encoding="utf-8"?-->
<framelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="30dp" tools:context=".BlankFragment"></framelayout>

<textview android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:linespacingmultiplier="2" android:drawableleft="@drawable/icon_smoking" tools:text="@string/hello_blank_fragment"></textview>

  • 1 行のときは問題ないが、改行があると 2 行の中央になってしまう。

drawableLeft

  • 今回は改行があったり、なかったりする TextView で改行した場合は1行目に画像を設置したい。

SpannableStringBuilder、ImageSpan で左に画像を設置する

  • SpannableString、SpannableStringBuilder などを使って ImageSpan を追加して、最後に TextView に画像を設定する。
  • Fragment の onCreateView に実装してる 
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_blank, container, false);

// テキストビュー ※ line-height的な「android:lineSpacingMultiplier="2"」を設定してるTextView
TextView textView = view.findViewById(R.id.textView);
// テキストビューの文言
String str = getActivity().getResources().getString(R.string.hello_blank_fragment);
// スパンのビルダー   ※ 1つスペースを設ける、ここに画像を追加する予定
SpannableStringBuilder ssb = new SpannableStringBuilder(" " + str);
// 画像のサイズ 15dp
int size = getActivity().getResources().getDimensionPixelSize(R.dimen.icon_size);
// 禁煙マークのアイコン
Drawable drawable = getActivity().getDrawable(R.drawable.icon_nosmoking);
// 禁煙マークのアイコンを15dp x 15dpにする
drawable.setBounds(0, 0, size, size);
// ImageSpanに設定する
ImageSpan imageSpan = new CustomImageSpan(drawable);
// スパンに追加
ssb.setSpan(imageSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
// TextViewに設定
textView.setText(ssb);

return view;
}

  • TextView にandroid:lineSpacingMultiplierandroid:lineSpacingExtraを設定した状態(css でいうところ line-height)を設定していたら、ImageSpan の位置をうまく計算できないようです。

ALIGN_BASELINE、ALIGN_BOTTOM を確認したがダメだった

  DynamicDrawableSpan.ALIGN_BASELINE DynamicDrawableSpan.ALIGN_BOTTOM  
1 行 baseline1 bottom1  
2 行 baseline2 bottom2  

ImageSpan を継承した独自のクラスを作って調整するみたい

参考サイト

  • https://stackoverflow.com/questions/25628258/align-text-around-imagespan-center-vertical/31491580

ImageSpan をオーバーライドしたクラス


public class CustomImageSpan extends ImageSpan {

public CustomImageSpan(Drawable drawable) {
super(drawable);
}

/**
* update the text line height
*/
@Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetricsInt) {
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int drHeight = rect.bottom - rect.top;
int centerY = fmPaint.ascent + fontHeight / 2;

fontMetricsInt.ascent = centerY - drHeight / 2;
fontMetricsInt.top = fontMetricsInt.ascent;
fontMetricsInt.bottom = centerY + drHeight / 2;
fontMetricsInt.descent = fontMetricsInt.bottom;
}
return rect.right;
}

/**
* see detail message in android.text.TextLine
*
* @param canvas the canvas, can be null if not rendering
* @param text   the text to be draw
* @param start  the text start position
* @param end    the text end position
* @param x      the edge of the replacement closest to the leading margin
* @param top    the top of the line
* @param y      the baseline
* @param bottom the bottom of the line
* @param paint  the work paint
*/
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {

Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int centerY = y + fmPaint.descent - fontHeight / 2;
int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}

}

先ほどのFragmentの実装を修正

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_blank, container, false);

// テキストビュー ※ line-height的な「android:lineSpacingMultiplier="2"」を設定してるTextView
TextView textView = view.findViewById(R.id.textView);
// テキストビューの文言
String str = getActivity().getResources().getString(R.string.hello_blank_fragment);
// スパンのビルダー   ※ 1つスペースを設ける、ここに画像を追加する予定
SpannableStringBuilder ssb = new SpannableStringBuilder(" " + str);
// 画像のサイズ 15dp
int size = getActivity().getResources().getDimensionPixelSize(R.dimen.icon_size);
// 禁煙マークのアイコン
Drawable drawable = getActivity().getDrawable(R.drawable.icon_nosmoking);
// 禁煙マークのアイコンを15dp x 15dpにする
drawable.setBounds(0, 0, size, size);
// 独自のCustomImageSpanを使う
CustomImageSpan customImageSpan = new CustomImageSpan(drawable);
// スパンに追加
ssb.setSpan(customImageSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
// TextViewに設定
textView.setText(ssb);

return view;
}

完成

fix