How to Add Custom Emoticons to Your Android App

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.

insert image description here
ReplacementSpanThe inheritance relationship diagram of ImageSpan is as follows, and DynamicDrawableSpantwo new classes appeared , let’s take a look at them first. MetricAffectingSpanThe and interface are introduced CharacterStylein Android Span Principle Analysis , so I won’t go into details here.
insert image description here

ReplacementSpan interface

ReplacementSpanIt 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

FontMetricsIntis 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
insert image description here

Baseline is the baseline for text drawing. It is not defined in FontMetricsInt, but can be FontMetricsIntobtained through a property of .

As mentioned above, getSizethe method only returns the width, so how is the height determined? In fact, it is FontMetricsIntcontrolled 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

DynamicDrawableSpanImplements ReplacementSpanthe methods of the interface. At the same time, it is an abstract class, which defines getDrawablethe abstract method, which is ImageSpanimplemented 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();

DynamicDrawableSpanThere are two pits that require special attention.

The first pit is that the object in getSizeand Paint.FontMetricsIntobtained through drawthe 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()getSizePaint.FontMetricsIntpaint.getFontMetricsInt()

The second pitfall is "doesn't work" ALIGN_CENTERwhen 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_CENTERhas no effect.

insert image description here
After removing the auxiliary line, it looks more obvious.
insert image description here

ImageSpan

ImageSpanIt'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 DynamicDrawableSpanand 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;
    }
}

insert image description here
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.

insert image description here
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.
insert image description here

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

reference

Guess you like

Origin blog.csdn.net/lichukuan/article/details/126916228