Drawing an image above an android text span

若如初见. 提交于 2019-12-01 04:29:27

问题


I am creating a complex text view, meaning different text styles in the same view. some of the text needs to have a small image just above it. but the text should still be there (not just replaced) so a simple ImageSpan will not do. I can't use a collection of TextViews because I need the text to wrap(or Am I wrong and this can be done with TextViews?).

I tried to combine two spans over the same characters but while that works for styling the text it does not for the ImageSpan.

What I am going for:

Any ideas?

Reading this: http://flavienlaurent.com/blog/2014/01/31/spans/ Helped a lot but i'm still not there.


回答1:


After reading the excellent article you referenced, poring over Android source code, and coding lots of Log.d()s, I finally figured out what you need and it is -- are you ready? -- a ReplacementSpan subclass.

ReplacementSpan is counter-intuitive for your case because you aren't replacing the text, you're drawing some additional stuff. But it turns out that ReplacementSpan is what gives you the two things you need: the hook to size the line height for your graphic and the hook to draw your graphic. So you'll just draw the text in there too, since the superclass isn't going to do it.

I've been interested in learning more about spans and text layout, so I started a demo project to play with.

I came up with two different ideas for you. In the first class, you have an icon that you can access as a Drawable. You pass the Drawable in on the constructor. Then you use the Drawable's dimensions to help size your line height. A benefit here is that the Drawable's dimensions have already been adjusted for the device's display density.

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.style.ReplacementSpan;
import android.util.Log;

public class IconOverSpan extends ReplacementSpan {

    private static final String TAG = "IconOverSpan";

    private Drawable mIcon;

    public IconOverSpan(Drawable icon) {
        mIcon = icon;
        Log.d(TAG, "<ctor>, icon intrinsic dimensions: " + icon.getIntrinsicWidth() + " x " + icon.getIntrinsicHeight());
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {

        /*
         * This method is where we make room for the drawing.
         * We are passed in a FontMetrics that we can check to see if there is enough space.
         * If we need to, we can alter these FontMetrics to suit our needs.
         */
        if (fm != null) {  // test for null because sometimes fm isn't passed in
            /*
             * Everything is measured from the baseline, so the ascent is a negative number,
             * and the top is an even more negative number.  We are going to make sure that
             * there is enough room between the top and the ascent line for the graphic.
             */
            int h = mIcon.getIntrinsicHeight();
            if (- fm.top + fm.ascent < h) {
                // if there is not enough room, "raise" the top
                fm.top = fm.ascent - h;
            }
        }

        /*
         * the number returned is actually the width of the span.
         * you will want to make sure the span is wide enough for your graphic.
         */
        int textWidth = (int) Math.ceil(paint.measureText(text, start, end));
        int w = mIcon.getIntrinsicWidth();
        Log.d(TAG, "getSize(), returning " + textWidth + ", fm = " + fm);
        return Math.max(textWidth, w);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        Log.d(TAG, "draw(), x = " + x + ", top = " + top + ", y = " + y + ", bottom = " + bottom);

        // first thing we do is draw the text that is not drawn because it is being "replaced"
        // you may have to adjust x if the graphic is wider and you want to center-align
        canvas.drawText(text, start, end, x, y, paint);

        // Set the bounds on the drawable.  If bouinds aren't set, drawable won't render at all
        // we set the bounds relative to upper left corner of the span
        mIcon.setBounds((int) x, top, (int) x + mIcon.getIntrinsicWidth(), top + mIcon.getIntrinsicHeight());
        mIcon.draw(canvas);
    }
}

The second idea is better if you are going to use really simple shapes for your graphics. You can define a Path for your shape and then just render the Path. Now you have to take display density into account, and to make it easy I just take it from a constructor parameter.

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.text.style.ReplacementSpan;
import android.util.Log;

public class PathOverSpan extends ReplacementSpan {

    private static final String TAG = "PathOverSpan";

    private float mDensity;

    private Path mPath;

    private int mWidth;

    private int mHeight;

    private Paint mPaint;

    public PathOverSpan(float density) {

        mDensity = density;
        mPath = new Path();
        mWidth = (int) Math.ceil(16 * mDensity);
        mHeight = (int) Math.ceil(16 * mDensity);
        // we will make a small triangle
        mPath.moveTo(mWidth/2, 0);
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();

        /*
         * set up a paint for our shape.
         * The important things are the color and style = fill
         */
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {

        /*
         * This method is where we make room for the drawing.
         * We are passed in a FontMetrics that we can check to see if there is enough space.
         * If we need to, we can alter these FontMetrics to suit our needs.
         */
        if (fm != null) {
            /*
             * Everything is measured from the baseline, so the ascent is a negative number,
             * and the top is an even more negative number.  We are going to make sure that
             * there is enough room between the top and the ascent line for the graphic.
             */
            if (- fm.top + fm.ascent < mHeight) {
                // if there is not enough room, "raise" the top
                fm.top = fm.ascent - mHeight;
            }
        }

        /*
         * the number returned is actually the width of the span.
         * you will want to make sure the span is wide enough for your graphic.
         */
        int textWidth = (int) Math.ceil(paint.measureText(text, start, end));
        return Math.max(textWidth, mWidth);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
        Log.d(TAG, "draw(), x = " + x + ", top = " + top + ", y = " + y + ", bottom = " + bottom);

        // first thing we do is draw the text that is not drawn because it is being "replaced"
        // you may have to adjust x if the graphic is wider and you want to center-align
        canvas.drawText(text, start, end, x, y, paint);

        // calculate an offset to center the shape
        int textWidth = (int) Math.ceil(paint.measureText(text, start, end));
        int offset = 0;
        if (textWidth > mWidth) {
            offset = (textWidth - mWidth) / 2;
        }

        // we set the bounds relative to upper left corner of the span
        canvas.translate(x + offset, top);
        canvas.drawPath(mPath, mPaint);
        canvas.translate(-x - offset, -top);
    }
}

Here's how I used these classes in the main activity:

    SpannableString spannableString = new SpannableString("Some text and it can have an icon over it");
    UnderlineSpan underlineSpan = new UnderlineSpan();
    IconOverSpan iconOverSpan = new IconOverSpan(getResources().getDrawable(R.drawable.ic_star));
    PathOverSpan pathOverSpan = new PathOverSpan(getResources().getDisplayMetrics().density);
    spannableString.setSpan(underlineSpan, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    spannableString.setSpan(iconOverSpan, 21, 25, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    spannableString.setSpan(pathOverSpan, 29, 38, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

    TextView textView = (TextView) findViewById(R.id.textView);
    textView.setText(spannableString);

There! Now we both learned something.



来源:https://stackoverflow.com/questions/34367961/drawing-an-image-above-an-android-text-span

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!