简体   繁体   English

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

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

I have the following in an xml layout :我在 xml 布局中有以下内容:

在此处输入图片说明

Notice how the hexagon # 4 is NOT center aligned with the text.请注意六边形 #4 如何不与文本居中对齐。 How can i do this: here is what i have tried so far:我该怎么做:这是我迄今为止尝试过的:

To actually get the view with the # inside of it i inflate a view that looks like this:要真正获得其中包含 # 的视图,我会膨胀一个如下所示的视图:

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

After inflating the view i take a copy of its drawing cache and i use that in the ImageSpan.膨胀视图后,我复制了它的绘图缓存,并在 ImageSpan 中使用它。 here is how i get the copy of the drawing cache:这是我获取绘图缓存副本的方法:

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

So now i have a bitmap that looks like the hexagon #4 image in the drawing i attached.所以现在我有一个位图,看起来像我附加的绘图中的六边形 #4 图像。 Now lets use that in the ImageSpan:现在让我们在 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;

               }

then later on i simply set that span on my textview.然后稍后我只是在我的文本视图上设置该跨度。 also dont forget to set the bounds on the drawable or it wont display, and it wroks but the image is not aligned center in the text.也不要忘记在 drawable 上设置边界,否则它不会显示,它会起作用,但图像没有在文本中居中对齐。 Notice how it drops to the bottom.注意它是如何下降到底部的。 How can i resolve this cleanly ?我怎样才能干净地解决这个问题?

You can use this class to align ImageSpan with text您可以使用此类将 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();
    }
}

Thanks to this answer感谢这个答案

You can try my CenteredImageSpan .你可以试试我的CenteredImageSpan You can customize in draw method by calculating transY -= (paint.getFontMetricsInt().descent / 2 - 8);您可以通过计算transY -= (paint.getFontMetricsInt().descent / 2 - 8);draw方法中自定义transY -= (paint.getFontMetricsInt().descent / 2 - 8); . . (Good luck. :) ) (祝你好运。 :) )

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编辑

I implemented above code like this:我像这样实现了上面的代码:

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

For API >29 you can do this using the ImageSpan.ALIGN_CENTER constant.对于 API >29,您可以使用ImageSpan.ALIGN_CENTER常量来执行此操作。 (Code samples below in Kotlin.) (以下 Kotlin 中的代码示例。)

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

If you need to support API levels below 29 (as I imagine most people will for a while) you'll still need to subclass ImageSpan as in RoShan Shan's answer.如果您需要支持低于 29 的 API 级别(我想大多数人会支持一段时间),您仍然需要像 RoShan Shan 的回答那样对 ImageSpan 进行子类化。 You only strictly need to override draw to get the behaviour to work, however:您只需要严格覆盖 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()
    }
}

this is my solution,it support single line and multi-line text这是我的解决方案,它支持单行和多行文本

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

I found a much easier way to handle all the alignment things is to do it another way.我发现处理所有对齐问题的更简单的方法是换一种方式。 we will create a imageSpan, but the bitmap will be from a inflated view.我们将创建一个 imageSpan,但位图将来自膨胀的视图。

so inflate your imageview (the alignments can all be adjusted here with margins) like this:所以膨胀你的图像视图(对齐都可以在这里用边距调整),如下所示:

<?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" />

you can create a parent viewgroup programatically and add this tv to it if you want to.您可以以编程方式创建父视图组,并根据需要将此电视添加到其中。

now lets grab the bitmap from this tv:现在让我们从这台电视中获取位图:

 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
    }

now that you have the image as a bitmap just apply it to the imageSpan and add it how you see fit.现在您将图像作为位图,只需将其应用于 imageSpan 并添加您认为合适的方式。 i love this way as im able to control how the span aligns insteads of relying on font metrics which is so complicated.我喜欢这种方式,因为我能够控制跨度如何对齐,而不是依赖于如此复杂的字体度量。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM