The previous article Android Span principle analysis introduced the principle of Span. This article will introduce the application of Span, and use Span to add custom emoticons to the App.
principle
The principle of adding custom emoticons is actually very simple, that is, to use ImageSpan to replace text. code show as below:
ImageSpan imageSpan = new ImageSpan(this, R.drawable.emoji_kelian);
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("哈哈哈哈[可怜]");
spannableStringBuilder.setSpan(imageSpan, 4, spannableStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableStringBuilder);
The above code replaces [可怜]
the text with the corresponding emoticon picture. The effect is as shown in the figure below. You can see that the size of the picture does not meet expectations, because ImageSpan will display the original size of the picture.
ReplacementSpan
The inheritance relationship diagram of ImageSpan is as follows, and DynamicDrawableSpan
two new classes appeared , let’s take a look at them first. MetricAffectingSpan
The and interface are introduced CharacterStyle
in Android Span Principle Analysis , so I won’t go into details here.
ReplacementSpan interface
ReplacementSpan
It is an interface, and the name is used to replace text. It defines two methods, as shown below.
public abstract int getSize(@NonNull Paint paint,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm);
Returns the width of the replaced Span. In the above example, it returns the width of the image. The parameters are as follows:
- paint: an instance of Paint
- text: the current text, its value in the above example is hahahaha[poor]
- start: The starting position of the Span, here is 4
- end: the end position of the Span, here is 8
- fm: an instance of FontMetricsInt
FontMetricsInt
is a class that describes various metrics of a font for a given text size. The meanings represented by the internal attributes are as follows:
- Top: the position of the purple line in the figure
- Ascent: the position of the green line in the figure
- Descent: the position of the blue line in the figure
- Bottom: The position of the yellow line in the figure
- Leading: Not marked in the figure, refers to the distance between the Bottom of the previous line and the Top of the next line.
图片来源 Meaning of top, ascent, baseline, descent, bottom, and leading in Android’s FontMetrics
Baseline is the baseline for text drawing. It is not defined in FontMetricsInt
, but can be FontMetricsInt
obtained through a property of .
As mentioned above, getSize
the method only returns the width, so how is the height determined? In fact, it is FontMetricsInt
controlled by , but there is a pit here , which will be mentioned later.
public abstract void draw(@NonNull Canvas canvas,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
float x,
int top,
int y,
int bottom,
@NonNull Paint paint);
Draw the Span in the Canvas. The parameters are as follows:
- canvas: Canvas instance
- text: current text
- start: the starting position of the Span
- end: the end position of the Span
- x: the x-coordinate position of [poor]
- top: the "Top" attribute value of the current row
- y: Baseline of the current line
- bottom: The "Bottom" attribute value of the current row
- paint: Paint instance, may be null
Here you need to pay special attention to Top and Bottom , which are a bit different from the above. Remember here first, and they will be introduced later.
DynamicDrawableSpan
DynamicDrawableSpan
Implements ReplacementSpan
the methods of the interface. At the same time, it is an abstract class, which defines getDrawable
the abstract method, which is ImageSpan
implemented to obtain the Drawable instance. The source code is as follows:
@Override
public int getSize(@NonNull Paint paint, CharSequence text,
@IntRange(from = 0) int start, @IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
Drawable d = getCachedDrawable();
Rect rect = d.getBounds();
//设置图片的高
if (fm != null) {
fm.ascent = -rect.bottom;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
return rect.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text,
@IntRange(from = 0) int start, @IntRange(from = 0) int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
Drawable b = getCachedDrawable();
canvas.save();
int transY = bottom - b.getBounds().bottom;
//设置对齐方式,有三种分别是
//ALIGN_BOTTOM 底部对齐,默认
//ALIGN_BASELINE 基线对齐
//ALIGN_CENTER 居中对齐
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= paint.getFontMetricsInt().descent;
} else if (mVerticalAlignment == ALIGN_CENTER) {
transY = top + (bottom - top) / 2 - b.getBounds().height() / 2;
}
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
public abstract Drawable getDrawable();
DynamicDrawableSpan
There are two pits that require special attention.
The first pit is that the object in getSize
and Paint.FontMetricsInt
obtained through draw
the method are not an object . In other words, no matter what value we set in the , it will not affect the value in the fetch object. It affects the values of top and bottom , which is why Top and Bottom were quoted when the parameters were introduced just now.paint.getFontMetricsInt()
getSize
Paint.FontMetricsInt
paint.getFontMetricsInt()
The second pitfall is "doesn't work" ALIGN_CENTER
when the image size exceeds the text size . As shown in the figure below, I added an auxiliary line for the convenience of display. The white line represents the parameters top and bottom, but the bottom is covered by other colors. It can be seen that the picture is centered, but the text is not centered, which makes us look like it ALIGN_CENTER
has no effect.
After removing the auxiliary line, it looks more obvious.
ImageSpan
ImageSpan
It's much simpler, it only implements getDrawable()
the method to get the Drawable instance, the code is as follows:
@Override
public Drawable getDrawable() {
Drawable drawable = null;
if (mDrawable != null) {
drawable = mDrawable;
} else if (mContentUri != null) {
Bitmap bitmap = null;
try {
InputStream is = mContext.getContentResolver().openInputStream(
mContentUri);
bitmap = BitmapFactory.decodeStream(is);
drawable = new BitmapDrawable(mContext.getResources(), bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
is.close();
} catch (Exception e) {
Log.e("ImageSpan", "Failed to loaded content " + mContentUri, e);
}
} else {
try {
drawable = mContext.getDrawable(mResourceId);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());
} catch (Exception e) {
Log.e("ImageSpan", "Unable to find resource: " + mResourceId);
}
}
return drawable;
}
The code here is very simple. The only thing we need to pay attention to is when we get the Drawable, we need to set its width and height so that it does not exceed the size of the text.
accomplish
After talking about the previous principles, it is very simple to implement. We only need to inherit DynamicDrawableSpan
and implement getDrawable()
the method, so that the width and height of the picture do not exceed the size of the text. Results as shown below:
public class EmojiSpan extends DynamicDrawableSpan {
@DrawableRes
private int mResourceId;
private Context mContext;
private Drawable mDrawable;
public EmojiSpan(@NonNull Context context, int resourceId) {
this.mResourceId = resourceId;
this.mContext = context;
}
@Override
public Drawable getDrawable() {
Drawable drawable = null;
if (mDrawable != null) {
drawable = mDrawable;
} else {
try {
drawable = mContext.getDrawable(mResourceId);
drawable.setBounds(0, 0, 48,
48);
} catch (Exception e) {
e.printStackTrace();
}
}
return drawable;
}
}
The above looks perfect, but things are not that simple. Because we just hard-coded the size of the picture, and did not change the algorithm for drawing the picture position. If it is used in other places EmojiSpan
, but the size of the text is smaller than the size of the picture, there will still be problems. As shown in the figure below, the situation when the text size of the text is 10sp.
In fact, there is also a problem when the text is larger than the image size. As shown in the figure below, in the case of multiple lines, only the line spacing of expressions is significantly smaller than the spacing of other lines.
If you are interested in this solution, please like + favorites >= 40, I will reproduce the custom emoticons of station B, plus the custom emoticons that can move (actually Gif pictures).