在 android 文本范围上方绘制图像
Drawing an image above an android text span
我正在创建一个复杂的文本视图,这意味着同一视图中的不同文本样式。一些文本需要在其正上方有一个小图像。但文本应该仍然存在(不仅仅是替换)所以简单的 ImageSpan 不会做。我不能使用 TextViews 的集合,因为我需要文本换行(或者我错了,这可以用 TextViews 完成吗?)。
我尝试将两个 span 组合到相同的字符上,但虽然这适用于设置文本样式,但不适用于 ImageSpan。
我的目标是:
有什么想法吗?
阅读此博客post:http://old.flavienlaurent.com/blog/2014/01/31/spans/
帮助了很多,但我仍然不在那里。
在阅读了您引用的优秀文章、仔细研究了 Android 源代码并编写了大量 Log.d()
代码之后,我终于弄清楚了您的需求,那就是 - 您准备好了吗? -- 一个 ReplacementSpan
子 class.
ReplacementSpan
对您的案例来说是违反直觉的,因为您 没有 替换文本,您正在绘制一些额外的东西。但事实证明 ReplacementSpan
为您提供了您需要的两件事:调整图形行高大小的挂钩和绘制图形的挂钩。所以你也可以在那里绘制文本,因为 superclass 不会这样做。
我一直有兴趣了解更多关于 span 和文本布局的信息,所以我开始了一个演示项目来玩。
我为您提出了两个不同的想法。在第一个 class 中,您有一个可以作为 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);
}
}
以下是我在主要 activity:
中使用这些 classes 的方式
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);
到了!现在我们都学到了一些东西。
我正在创建一个复杂的文本视图,这意味着同一视图中的不同文本样式。一些文本需要在其正上方有一个小图像。但文本应该仍然存在(不仅仅是替换)所以简单的 ImageSpan 不会做。我不能使用 TextViews 的集合,因为我需要文本换行(或者我错了,这可以用 TextViews 完成吗?)。
我尝试将两个 span 组合到相同的字符上,但虽然这适用于设置文本样式,但不适用于 ImageSpan。
我的目标是:
有什么想法吗?
阅读此博客post:http://old.flavienlaurent.com/blog/2014/01/31/spans/ 帮助了很多,但我仍然不在那里。
在阅读了您引用的优秀文章、仔细研究了 Android 源代码并编写了大量 Log.d()
代码之后,我终于弄清楚了您的需求,那就是 - 您准备好了吗? -- 一个 ReplacementSpan
子 class.
ReplacementSpan
对您的案例来说是违反直觉的,因为您 没有 替换文本,您正在绘制一些额外的东西。但事实证明 ReplacementSpan
为您提供了您需要的两件事:调整图形行高大小的挂钩和绘制图形的挂钩。所以你也可以在那里绘制文本,因为 superclass 不会这样做。
我一直有兴趣了解更多关于 span 和文本布局的信息,所以我开始了一个演示项目来玩。
我为您提出了两个不同的想法。在第一个 class 中,您有一个可以作为 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);
}
}
以下是我在主要 activity:
中使用这些 classes 的方式 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);
到了!现在我们都学到了一些东西。