在安卓文本区域上方绘制图像

7
我正在创建一个复杂的文本视图,即在同一视图中使用不同的文本样式。其中一些文本需要在其上方放置一个小图像,但文本仍应存在(而不是被替换掉),因此简单的ImageSpan无法胜任。我不能使用一组TextViews,因为我需要文本自动换行(或者我错了,这可以使用TextViews完成吗?)。
我试图将两个Spans组合在相同的字符上,但虽然这对于样式化文本有效,但对于ImageSpan则无效。
我想要实现的效果是:

enter image description here

有什么想法吗?

阅读这篇博客文章:http://old.flavienlaurent.com/blog/2014/01/31/spans/ 很有帮助,但我还没有完全理解。


请参见android.text.style.ReplacementSpan - pskink
1个回答

9
在阅读了你提供的优秀文章、查看了Android源代码并编写了大量Log.d()之后,我终于明白你需要的是——准备好了吗?——一个ReplacementSpan子类。
对于你的情况来说,ReplacementSpan是有些违反直觉的,因为你并没有替换文本,而是在绘制一些额外的东西。但事实证明,ReplacementSpan正是你需要的两个东西:用于为你的图形调整行高的钩子和用于绘制你的图形的钩子。所以你只需在其中绘制文本,因为超类不会这样做。
我一直对spans和文本布局感兴趣,所以我开始了一个演示项目进行尝试。
我为你想出了两种不同的方法。在第一个类中,你有一个可以作为Drawable访问的图标。你将Drawable传递到构造函数中。然后你使用Drawable的尺寸来帮助调整行高。这里的好处是Drawable的尺寸已经针对设备的显示密度进行了调整。
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);
    }
}

第二个想法更好,如果您打算使用非常简单的形状来制作图形。您可以为形状定义一个Path,然后只需渲染Path。现在您必须考虑显示密度,为了使它更容易,我只是从构造函数参数中获取它。
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);
    }
}

以下是我在主活动中如何使用这些类:

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

好的!现在我们都学到了一些东西。


谢谢您提供详细的解决方案。我从中学到了很多。由于规格有些变化(我需要在图标上方添加更多文本,而且明天我不知道还需要什么),所以我将放弃这条路,转而采用FlowLayout > LinearLayout的解决方案。我知道这对性能来说并不理想,但是规格还在变化,我想做一些灵活的东西,以后需要时再进行改进。 - user1852503
我在使用ReplacementSpan处理多行文本时遇到了问题,其中文本上方的图像与上面的行重叠。我发现我需要在_getSize_方法中将fm.ascent更改为与我更改的fm.top相同的数量,以避免这种情况发生。感谢您提供的出色答案,非常有帮助。 - Rory Stephenson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接