web-dev-qa-db-ja.com

ImageSpanの中心に垂直にテキストを揃える

テキストの中に ImageSpan があります。私が気づいたのは、周囲のテキストが常にテキスト行の下部に描画されることです-より正確には、テキスト行のサイズは画像とともに大きくなりますが、テキストのベースラインは上に移動しません。画像がテキストサイズよりも著しく大きい場合、効果はかなり見苦しくなります。

以下にサンプルを示します。アウトラインはTextViewの境界を示しています。 enter image description here

表示されている画像に対して、周囲のテキストを垂直方向に中央揃えしようとしています。同じサンプルに、目的の場所を示す青色のテキストを示します。

enter image description here

私が拘束している制約は次のとおりです。

  • 複合ドロウアブルは使用できません。画像は単語間で表示できる必要があります。
  • コンテンツによっては、テキストが複数行になる場合があります。これを制御することはできません。
  • 画像が周囲のテキストよりも大きく、サイズを縮小できません。上記のサンプル画像は実際の画像よりも大きいですが(現在の動作を示すため)、実際の画像はまだ十分に大きいため、この問題は顕著です。

TextViewでAndroid:gravity="center_vertical"属性を使用してみましたが、これは効果がありません。これは、テキストをちょうど垂直方向に中央揃えしていると思いますが、テキスト行内ではテキストはまだ下部に描画されます。

私の現在の考え方は、行の高さと現在のテキストサイズに基づいてテキストのベースラインをシフトするカスタムスパンを作成することです。このスパンはテキスト全体を網羅し、ImageSpansとの交差を計算する必要があるため、画像のシフトも回避できます。これはかなり気が遠くなるようで、誰かが別のアプローチを提案できることを期待しています。

すべての助けがありがたいです!

37
Karakuri

少し遅いかもしれませんが、画像サイズに関係なく、それを行う方法を見つけました。 ImageSpanを拡張するクラスを作成し、メソッドgetSize()およびgetCachedDrawable()をオーバーライドする必要があります(最後のメソッドを変更する必要はありませんが、DynamicDrawableSpanのこのメソッドはプライベートであり、子クラスから別の方法でアクセスすることはできません)。 getSize(...)では、DynamicDrawableSpanが行のアセント/トップ/ディセント/ボトムを設定する方法を再定義して、やりたいことを実現できます。

クラスの例を次に示します。

import Android.graphics.Canvas;
import Android.graphics.Paint;
import Android.graphics.Rect;
import Android.graphics.drawable.Drawable;
import Android.text.style.DynamicDrawableSpan;
import Android.text.style.ImageSpan;

import Java.lang.ref.WeakReference;

public class CenteredImageSpan extends ImageSpan {

    // Extra variables used to redefine the Font Metrics when an ImageSpan is added
    private int initialDescent = 0;
    private int extraSpace = 0;

    public CenteredImageSpan(final Drawable drawable) {
        this(drawable, DynamicDrawableSpan.ALIGN_BOTTOM);
    }

    public CenteredImageSpan(final Drawable drawable, final int verticalAlignment) {
        super(drawable, verticalAlignment);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, Paint paint) {
        getDrawable().draw(canvas);
    }

    // Method used to redefined the Font Metrics when an ImageSpan is added
    @Override
    public int getSize(Paint paint, CharSequence text,
                       int start, int end,
                       Paint.FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();

        if (fm != null) {
            // Centers the text with the ImageSpan
            if (rect.bottom - (fm.descent - fm.ascent) >= 0) {
                // Stores the initial descent and computes the margin available
                initialDescent = fm.descent;
                extraSpace = rect.bottom - (fm.descent - fm.ascent);
            }

            fm.descent = extraSpace / 2 + initialDescent;
            fm.bottom = fm.descent;

            fm.ascent = -rect.bottom + fm.descent;
            fm.top = fm.ascent;
        }

        return rect.right;
    }

    // Redefined locally because it is a private member from DynamicDrawableSpan
    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;

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

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

        return d;
    }

    private WeakReference<Drawable> mDrawableRef;
}

そのクラスに問題がある場合はお知らせください!

27
jujux789

私の答えは最初の答えを微調整します。実際、私は上記の2つの方法の両方を試してみましたが、それらは真ん中にあるとは思いません。 ascentdescentではなく、topbottomの間に配置すると、ドロアブルがより中央に配置されます。 2番目の答えについては、ドロウアブルの中心をテキストの中心ではなく、テキストのベースラインに揃えます。私のソリューションは次のとおりです。

public class CenteredImageSpan extends ImageSpan {
  private WeakReference<Drawable> mDrawableRef;

  public CenteredImageSpan(Context context, final int drawableRes) {
    super(context, drawableRes);
  }

  @Override
  public int getSize(Paint paint, CharSequence text,
                     int start, int end,
                     Paint.FontMetricsInt fm) {
    Drawable d = getCachedDrawable();
    Rect rect = d.getBounds();

    if (fm != null) {
      Paint.FontMetricsInt pfm = Paint.getFontMetricsInt();
      // keep it the same as Paint's fm
      fm.ascent = pfm.ascent;
      fm.descent = pfm.descent;
      fm.top = pfm.top;
      fm.bottom = pfm.bottom;
    }

    return rect.right;
  }

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

    int drawableHeight = b.getIntrinsicHeight();
    int fontAscent = Paint.getFontMetricsInt().ascent;
    int fontDescent = Paint.getFontMetricsInt().descent;
    int transY = bottom - b.getBounds().bottom +  // align bottom to bottom
        (drawableHeight - fontDescent + fontAscent) / 2;  // align center to center

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

  // Redefined locally because it is a private member from DynamicDrawableSpan
  private Drawable getCachedDrawable() {
    WeakReference<Drawable> wr = mDrawableRef;
    Drawable d = null;

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

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

    return d;
  }
}

また、DrawableのFontMetricsを他のテキストと同じに保つためにgetSizeを書き換えます。そうしないと、親ビューはコンテンツを正しくラップしません。

59
misaka-10032
ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM) {
                public void draw(Canvas canvas, CharSequence text, int start,
                        int end, float x, int top, int y, int bottom,
                        Paint paint) {
                    Drawable b = getDrawable();
                    canvas.save();

                    int transY = bottom - b.getBounds().bottom;
                    // this is the key 
                    transY -= Paint.getFontMetricsInt().descent / 2;

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

TextViewのソースコードを読んだ後、各テキスト行の「y」であるbaseLineを使用できると思います。また、lineSpaceExtraを設定しても機能します。

public class VerticalImageSpan extends ImageSpan {

    public VerticalImageSpan(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();
    }

}
19
xuqingqi

ImageSpan を継承するクラスを作成することにより、実用的なソリューションを得ました。

次に、DynamicDrawableSpanから描画実装を変更しました。少なくともこの実装は、画像の高さがフォントの高さより低い場合に機能します。あなたのような大きな画像でこれがどのように機能するかはわかりません。

@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 bCenter = b.getIntrinsicHeight() / 2;
    int fontTop = Paint.getFontMetricsInt().top;
    int fontBottom = Paint.getFontMetricsInt().bottom;
    int transY = (bottom - b.getBounds().bottom) -
        (((fontBottom - fontTop) / 2) - bCenter);


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

また、DynamicDrawableSpanの実装はプライベートであるため、再利用する必要がありました。

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;

これは、テキストの前に画像を挿入する静的メソッドとして使用する方法です。

public static CharSequence formatTextWithIcon(Context context, String text,
    int iconResourceId) {
    SpannableStringBuilder sb = new SpannableStringBuilder("X");

    try {
        Drawable d = context.getResources().getDrawable(iconResourceId);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 
        CenteredImageSpan span = new CenteredImageSpan(d); 
        sb.setSpan(span, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        sb.append(" " + text); 
    } catch (Exception e) {
        e.printStackTrace();
        sb.append(text); 
    }

    return sb;

ローカライズを考慮すると、おそらく良いプラクティスではありませんが、私には役立ちます。テキストの中央に画像を設定するには、当然テキスト内のトークンをスパンに置き換える必要があります。

4
ptiili

私の答えはmisaka-10032の答えを微調整します。完璧に動作します!

public static class CenteredImageSpan extends ImageSpan {
    private WeakReference<Drawable> mDrawableRef;

    CenteredImageSpan(Context context, final int drawableRes) {
        super(context, drawableRes);
    }

    public CenteredImageSpan(@NonNull Drawable d) {
        super(d);
    }

    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, @NonNull Paint paint) {
        Drawable b = getCachedDrawable();
        canvas.save();
        int transY = top + (bottom - top - b.getBounds().bottom)/2;
        canvas.translate(x, transY);
        b.draw(canvas);
        canvas.restore();
    }

    // Redefined locally because it is a private member from DynamicDrawableSpan
    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;

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

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

        return d;
    }
}
0
heng li