Align text around ImageSpan center vertical

前端 未结 9 1807
抹茶落季
抹茶落季 2020-11-28 23:59

I have an ImageSpan inside of a piece of text. What I\'ve noticed is that the surrounding text is always drawn at the bottom of the text line -- to be more precise, the size

相关标签:
9条回答
  • 2020-11-29 00:08
    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();
                    }
                };
    
    0 讨论(0)
  • 2020-11-29 00:09

    My improved version: drawable font metrics zoomed relative to text font metrics. So that line spacing will be calculate correctly.

    @Override
    public int getSize(Paint paint, CharSequence text,
                       int start, int end,
                       Paint.FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();
        float drawableHeight = Float.valueOf(rect.height());
    
    
        if (fm != null) {
            Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
            float fontHeight = pfm.descent - pfm.ascent;
            float ratio = drawableHeight / fontHeight;
    
            fm.ascent = Float.valueOf(pfm.ascent * ratio).intValue();
            fm.descent = Float.valueOf(pfm.descent * ratio).intValue();
            fm.top = fm.ascent;
            fm.bottom = fm.descent;
        }
    
    0 讨论(0)
  • 2020-11-29 00:15

    It might be a bit late but I've found a way to do it, no matter the image size. You need to create a class extending ImageSpan and override the methods getSize() and getCachedDrawable() (we don't need to change the last one, but this method from DynamicDrawableSpan is private and cannot be accessed in another way from the child class). In getSize(...), you can then redefined the way DynamicDrawableSpan set the ascent/top/descent/bottom of the line and achieve what you want to do.

    Here's my class example:

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

    Let me know if you have any trouble with that class!

    0 讨论(0)
  • 2020-11-29 00:22

    This solution provides a vertical centering based on actual letter size. It supports centering using capital letters and lower-case letters. For example, look at the marker character near a letter: X•. This solution achieves a similar effect.

    This is a modified version of @WindRider's answer. Also, it's in Kotlin. And it supports drawable size customization.

    The reason why this solution is created is to provide a better visual result. A lot of other solutions use font ascent. But it appears to cause visual problems in some cases. Android's default Roboto font, for example, has ascent higher than a typical capital letter top border. And because of it, some manual adjustments were needed to properly center an image.

    class CenteredImageSpan(context: Context,
                            drawableRes: Int,
                            private val centerType: CenterType = CenterType.CAPITAL_LETTER,
                            private val customHeight: Int? = null,
                            private val customWidth: Int? = null) : ImageSpan(context, drawableRes) {
    
        private var mDrawableRef: WeakReference<Drawable?>? = null
    
        override fun getSize(paint: Paint, text: CharSequence,
                             start: Int, end: Int,
                             fontMetrics: FontMetricsInt?): Int {
    
            if (fontMetrics != null) {
                val currentFontMetrics = paint.fontMetricsInt
                // keep it the same as paint's Font Metrics
                fontMetrics.ascent = currentFontMetrics.ascent
                fontMetrics.descent = currentFontMetrics.descent
                fontMetrics.top = currentFontMetrics.top
                fontMetrics.bottom = currentFontMetrics.bottom
            }
    
            val drawable = getCachedDrawable()
            val rect = drawable.bounds
            return rect.right
        }
    
        override fun draw(canvas: Canvas,
                          text: CharSequence,
                          start: Int,
                          end: Int,
                          x: Float,
                          lineTop: Int,
                          baselineY: Int,
                          lineBottom: Int,
                          paint: Paint) {
            val cachedDrawable = getCachedDrawable()
            val drawableHeight = cachedDrawable.bounds.height()
    
            val relativeVerticalCenter = getLetterVerticalCenter(paint)
    
            val drawableCenter = baselineY + relativeVerticalCenter
            val drawableBottom = drawableCenter - drawableHeight / 2
    
            canvas.save()
            canvas.translate(x, drawableBottom.toFloat())
            cachedDrawable.draw(canvas)
            canvas.restore()
        }
    
        private fun getLetterVerticalCenter(paint: Paint): Int =
             when (centerType) {
                CenterType.CAPITAL_LETTER -> getCapitalVerticalCenter(paint)
                CenterType.LOWER_CASE_LETTER -> getLowerCaseVerticalCenter(paint)
            }
    
        private fun getCapitalVerticalCenter(paint: Paint): Int {
            val bounds = Rect()
            paint.getTextBounds("X", 0, 1, bounds)
            return (bounds.bottom + bounds.top) / 2
        }
    
        private fun getLowerCaseVerticalCenter(paint: Paint): Int {
            val bounds = Rect()
            paint.getTextBounds("x", 0, 1, bounds)
            return (bounds.bottom + bounds.top) / 2
        }
    
    
        // Redefined here because it's private in DynamicDrawableSpan
        private fun getCachedDrawable(): Drawable {
    
            val drawableWeakReference = mDrawableRef
            var drawable: Drawable? = null
            if (drawableWeakReference != null) drawable = drawableWeakReference.get()
            if (drawable == null) {
                drawable = getDrawable()!!
    
                val width = customWidth ?: drawable.intrinsicWidth
                val height = customHeight ?: drawable.intrinsicHeight
    
                drawable.setBounds(0, 0,
                                   width, height)
                mDrawableRef = WeakReference(drawable)
            }
            return drawable
    
        }
    
        enum class CenterType {
            CAPITAL_LETTER, LOWER_CASE_LETTER
        }
    
    }
    
    0 讨论(0)
  • 2020-11-29 00:23

    After reading the source code of TextView, I think we can use the baseLine of eache text line which is "y". And it will work even if you set 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();
        }
    
    }
    
    0 讨论(0)
  • 2020-11-29 00:25

    This solution works. I have tested it and am using it for sometime. It doesn't consider the ascent and decent but it Aligns the drawable in the center.

    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.graphics.drawable.Drawable;
    import android.support.annotation.NonNull;
    import android.text.style.ImageSpan;
    
    import java.lang.ref.WeakReference;
    
    public class CustomImageSpan extends ImageSpan {
    
      /**
       * A constant indicating that the center of this span should be aligned
       * with the center of the surrounding text
       */
      public static final int ALIGN_CENTER = -12;
      private WeakReference<Drawable> mDrawable;
      private int mAlignment;
    
      public CustomImageSpan(Context context, final int drawableRes, int alignment) {
        super(context, drawableRes);
        mAlignment = alignment;
      }
    
      @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();
          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) {
        if (mAlignment == ALIGN_CENTER) {
          Drawable cachedDrawable = getCachedDrawable();
          canvas.save();
          //Get the center point and set the Y coordinate considering the drawable height for aligning the icon vertically
          int transY = ((top + bottom) / 2) - cachedDrawable.getIntrinsicHeight() / 2;
          canvas.translate(x, transY);
          cachedDrawable.draw(canvas);
          canvas.restore();
        } else {
          super.draw(canvas, text, start, end, x, top, y , bottom, paint);
        }
      }
    
      // Redefined locally because it is a private member from DynamicDrawableSpan
      private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawable;
        Drawable d = null;
        if (wr != null) {
          d = wr.get();
        }
        if (d == null) {
          d = getDrawable();
          mDrawable = new WeakReference<>(d);
        }
        return d;
      }
    }
    
    0 讨论(0)
提交回复
热议问题