Android - ImageSpan - 如何在文本末尾居中对齐图像

Android - ImageSpan - How to center align the image at the end of the text

我在 xml 布局中有以下内容:

注意六边形 #4 没有与文本居中对齐。我该怎么做:这是我到目前为止尝试过的方法:

为了实际获得带有 # 的视图,我膨胀了一个如下所示的视图:

//my_hexagon_button.xml:

     <?xml version="1.0" encoding="utf-8"?>
       <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                     xmlns:tools="http://schemas.android.com/tools"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:orientation="vertical"
                     android:padding="0dp"
                     tools:ignore="MissingPrefix">


           <Button
               android:id="@+id/tv_icon"
               fontPath="proxima_nova_semi_bold.otf"
               android:layout_width="16dp"
               android:layout_height="17.5dp"
               android:layout_marginBottom="5dp"
               android:layout_marginLeft="10dp"
               android:alpha=".25"
               android:background="@drawable/hexagon"
               android:clickable="true"
               android:contentDescription="@string/content_description"
               android:focusable="false"
               android:padding="0dp"
               android:text="4"
               android:textColor="@color/white"
               android:textSize="8dp"
               />

       </LinearLayout>

膨胀视图后,我复制了它的绘图缓存,并在 ImageSpan 中使用它。这是我获取绘图缓存副本的方法:

public Bitmap getIconBitmap() {
               LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
               LinearLayout myRoot = new LinearLayout(getActivity());

               // inflate and measure the button then grab its image from the view cache
               ViewGroup parent = (ViewGroup) inflater.inflate(R.layout.my_hexagon_button, myRoot);
               TextView tv = (TextView) parent.findViewById(R.id.tv_icon);

               parent.setDrawingCacheEnabled(true);
               parent.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                       View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
               parent.layout(0, 0, parent.getMeasuredWidth(), parent.getMeasuredHeight());

               parent.buildDrawingCache(true);
               // if you need bounds on the view, swap bitmap for a drawable and call setbounds, im not using bounds
               Bitmap b = Bitmap.createBitmap(parent.getDrawingCache());
               parent.setDrawingCacheEnabled(false); // clear drawing cache

               return b;
           }

所以现在我有一个位图,它看起来像我附加的绘图中的六边形 #4 图像。现在让我们在 ImageSpan 中使用它:

public Spannable createImageSpan(TextView tv,Bitmap bitmapIcon) {

                   Spannable span = new SpannableString(tv.getText());
                   int start = span.length() - 1;
                   int end = span.length();

                   ImageSpan image = new ImageSpan(new BitmapDrawable(getResources(), bitmapIcon),ImageSpan.ALIGN_BASELINE);
                   span.setSpan(image, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

                   return span;

               }

然后我只是在我的文本视图上设置了那个跨度。也不要忘记在可绘制对象上设置边界,否则它不会显示,它可以工作,但图像未在文本中居中对齐。注意它是如何下降到底部的。我怎样才能彻底解决这个问题?

你可以试试我的CenteredImageSpan。 您可以通过计算 transY -= (paint.getFontMetricsInt().descent / 2 - 8);draw 方法中自定义。 (祝你好运。:))

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

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

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

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

    @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;
//        }

        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;
    }

    @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

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

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


    // 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;
    }
}

EDIT

我是这样实现上面的代码的:

Drawable myIcon = getResources().getDrawable(R.drawable.btn_feedback_yellow);
            int width = (int) Functions.convertDpToPixel(75, getActivity());
            int height = (int) Functions.convertDpToPixel(23, getActivity());
            myIcon.setBounds(0, 0, width, height);
            CenteredImageSpan btnFeedback = new CenteredImageSpan(myIcon, ImageSpan.ALIGN_BASELINE);
            ssBuilder.setSpan(
                    btnFeedback, // Span to add
                    getString(R.string.text_header_answer).length() - 1, // Start of the span (inclusive)
                    getString(R.string.text_header_answer).length(), // End of the span (exclusive)
                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);// Do not extend the span when text add later

您可以使用此 class 将 ImageSpan 与文本对齐

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

多亏了这个answer

对于 API >29,您可以使用 ImageSpan.ALIGN_CENTER 常量来执行此操作。 (下面的 Kotlin 代码示例。)

val image: ImageSpan = ImageSpan(
    BitmapDrawable(resources, bitmapIcon),
    ImageSpan.ALIGN_CENTER);
span.setSpan(image, start, end, 0);

如果您需要支持低于 29 的 API 水平(我想大多数人会持续一段时间),您仍然需要像 RoShan Shan 的回答那样子类化 ImageSpan。您只需要严格重写 draw 即可使行为正常工作,但是:

class CenteredImageSpanSubclass(
    context: Context, 
    bitmap: Bitmap): ImageSpan(context, bitmap) {

    override fun draw(...) {

        canvas.save()

        val transY = (bottom - top) / 2 - drawable.bounds.height() / 2

        canvas.translate(x, transY.toFloat())
        drawable.draw(canvas)
        canvas.restore()
    }
}

这是我的解决方案,它支持单行和多行文本

class CenteredImageSpan(dr: Drawable) : ImageSpan(dr) {
    private var mDrawableRef: WeakReference<Drawable>? = null
    override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
        val d = cachedDrawable
        val rect: Rect = d!!.bounds
        val pfm = paint.fontMetricsInt
        if (fm != null) {
            fm.ascent = -rect.height() / 2 + pfm.ascent / 2
            fm.descent = Math.max(0, rect.height() / 2 + pfm.ascent / 2)
            fm.top = fm.ascent
            fm.bottom = fm.descent
        }
        return rect.right
    }

    override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, @NonNull paint: Paint) {
        val b = cachedDrawable!!
        canvas.save()
        var transY = (bottom + top) / 2 - b.bounds.height() / 2
        canvas.translate(x, transY.toFloat())
        b.draw(canvas)
        canvas.restore()
    }

    // Redefined locally because it is a private member from DynamicDrawableSpan
    private val cachedDrawable: Drawable?
        private get() {
            val wr: WeakReference<Drawable>? = mDrawableRef
            var d: Drawable? = null
            if (wr != null) d = wr.get()
            if (d == null) {
                d = drawable
                mDrawableRef = WeakReference(d)
            }
            return d
        }
}

我发现处理所有对齐问题的更简单方法是换一种方法。我们将创建一个 imageSpan,但位图将来自膨胀视图。

所以像这样膨胀你的图像视图(对齐方式都可以在这里用边距调整):

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/myImage"
    android:paddingLeft="3dp"
    android:paddingTop="1dp"
    android:paddingRight="3dp"
    android:paddingBottom="1dp"
    android:layout_marginEnd="6dp"
    tools:text="for sale" />

您可以通过编程方式创建父视图组,并根据需要将此电视添加到其中。

现在让我们从这台电视上抓取位图:

 private fun getBitmap(myTextView: View): Bitmap {
        val bitmap = Bitmap.createBitmap(myTextView.width, myTextView.height, Bitmap.Config.ARGB_8888)
        val myCanvas = Canvas(bitmap)
        view.draw(myCanvas)
        return bitmap
    }

既然您已将图像作为位图,只需将其应用于 imageSpan 并按照您认为合适的方式添加即可。我喜欢这种方式,因为我能够控制跨度对齐方式,而不是依赖如此复杂的字体指标。