【问题标题】:Android - ImageSpan - How to center align the image at the end of the textAndroid - ImageSpan - 如何在文本末尾居中对齐图像
【发布时间】:2017-04-14 02:52:16
【问题描述】:

我在 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;

               }

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

【问题讨论】:

    标签: android spannablestring


    【解决方案1】:

    您可以使用该类将 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

    【讨论】:

    • 这个怎么用?
    • @SachinTanpure 你可以使用它的跨度字符串
    • 谢谢。其实我是安卓新手。可以举个例子吗?
    【解决方案2】:

    对于 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()
        }
    }
    

    【讨论】:

    • 在 API 29 以下,您可以使用DynamicDrawableSpan.ALIGN_BASELINE 代替ImageSpan.ALIGN_CENTER。我认为即使对于 29 岁以上的 API,它也是一个更好的选择。
    • ALIGN_BASELINEALIGN_CENTER 不是一回事。如果您想将可绘制对象与基线对齐,那么您肯定会使用ALIGN_BASELINE,但是如果您想根据文本在跨度中居中图像,那么使用该常量不会给您你想要的行为。
    • 实际上我在 API 29 之前的模拟器和设备中对其进行了测试,它似乎可以完美运行
    【解决方案3】:

    你可以试试我的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;
        }
    }
    

    编辑

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

    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
    

    【讨论】:

    • 您的代码我没有修改。我传递了一个 bitmapdrawable 给它,但它不会显示想象。当我点击图像区域时,我放在那里的可点击跨度仍然有效。它只是图像不可见。
    • 似乎很有希望。一个问题。对于线 transY -= (paint.getFontMetricsInt().descent / 2 - 8);当我调整 #8 值时,我可以调整高度,但这是以像素为单位的吗?所以我应该从 DP 转换为 px 然后调整 fontMetrics 对吗?因为它不能在不同的屏幕尺寸上始终如一地工作。例如 xhdpi vs xxhdpi 图标在 xhdpi 上显示得更高。
    • 如果您希望它适用于多屏幕,您可以为多屏幕定义多维度:例如:文件夹 values-xhdpi 与 dimens.xml,values-xxhdpi 与 dimens.xml。并定义与每个屏幕尺寸相关的值。您必须手动制作,因为您的文字大小和图像在不同的屏幕上是不同的。
    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

      我发现一个更简单的方法来处理所有对齐的事情是用另一种方式来做。我们将创建一个 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 并添加到您认为合适的位置。我喜欢这种方式,因为我能够控制跨度的对齐方式,而不是依赖于如此复杂的字体指标。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-07-05
        • 2014-11-19
        • 1970-01-01
        • 2016-12-13
        • 1970-01-01
        相关资源
        最近更新 更多