天天看點

【用戶端】給你的 Android App 添加自定義表情

作者:小牆程式員

這一篇文章将介紹 Span 的應用,使用 Span 來給 App 添加自定義表情。

原理

添加自定義表情的原理其實很簡單,就是使用 ImageSpan 對文字進行替換。代碼如下:

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

複制代碼上面的代碼把 [可憐] 文字替換成了對應的表情圖檔。效果如下圖,可以看到圖檔的大小不符合預期,這是因為 ImageSpan 會顯示成圖檔原來的大小。

【用戶端】給你的 Android App 添加自定義表情

ImageSpan 的繼承關系圖如下,出現了 ReplacementSpan 和 DynamicDrawableSpan 兩個新的類,先來看一下它們。MetricAffectingSpan 和 CharacterStyle 接口在 介紹了,這裡就不贅述了。

ReplacementSpan 接口

ReplacementSpan 是一個接口,看名字是用來替換文字的。它裡面定義了兩個方法,如下所示。

public abstract int getSize(@NonNull Paint paint,
                                    CharSequence text,
                                    @IntRange(from = 0) int start,
                                    @IntRange(from = 0) int end,
                                    @Nullable Paint.FontMetricsInt fm);
           

複制代碼傳回替換後 Span 的寬,上面的例子中就是傳回圖檔的寬度,參數作用如下:

● paint: Paint 的執行個體

● text: 目前文本,上面的例子中它的值是是 哈哈哈哈[可憐]

● start: Span 的開始位置,這裡是 4

● end: Span 的結束位置,這裡是 8

● fm: FontMetricsInt 的執行個體

FontMetricsInt 是描述給定文本大小的字型的各種度量的類。内部屬性代表的含義如下圖:

● Top:圖中紫線的位置

● Ascent: 圖中綠線的位置

● Descent: 圖中藍線的位置

● Bottom: 圖中黃線的位置

● Leading: 未在圖中标出,是指上一行的 Bottom 與下一行的 Top 之間的距離。

圖檔來源

【用戶端】給你的 Android App 添加自定義表情

Baseline 是文字繪制的基準線。它不定義在 FontMetricsInt 中,但可以通過 FontMetricsInt 的屬性擷取。

上面講到 getSize 方法隻傳回寬度,那高度是怎麼确定的呢?其實它是通過 FontMetricsInt 來控制,不過這裡有個坑,後面會說到。

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

複制代碼在 Canvas 中繪制 Span。參數如下:

● canvas:Canvas 執行個體

● text:目前文本

● start:Span 的開始位置

● end:Span 的結束位置

● x:[可憐] 的 x 坐标位置

● top:目前行的 “Top“ 屬性值

● y:目前行的 Baseline

● bottom: 目前行的 ”Bottom“ 屬性值

● paint:Paint 執行個體,可能為 null

這裡需要特殊注意 Top 和 Bottom,跟上面說的有點不同這裡先記住,後面會一起介紹。

DynamicDrawableSpan

DynamicDrawableSpan 實作了 ReplacementSpan 接口的方法。同時它是一個抽象類,定義了 getDrawable 抽象方法,由 ImageSpan 實作來擷取 Drawable 執行個體。源碼如下:

@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 有兩個坑需要特别注意。

第一個坑就是在 getSize 中的 Paint.FontMetricsInt 對象和 draw 方法中通過 paint.getFontMetricsInt() 擷取的不是一個對象。也就是說,無論我們在 getSize 的 Paint.FontMetricsInt 中設定什麼值,都不會影響到 paint.getFontMetricsInt() 擷取對象中的值。它影響的是 top 和 bottom 的值,這也是剛才介紹參數時給 Top 和 Bottom 打引号的原因。

第二個坑是 ALIGN_CENTER 在圖檔大小超過文字大小時“不起作用”。如下圖所示,為了友善顯示我加了輔助線,白線是代表參數 top,bottom,但是 bottom 被其它顔色覆寫了。可以看到,圖檔是居中的,是文字沒有居中讓我們看上去 ALIGN_CENTER 沒有效果一樣。

【用戶端】給你的 Android App 添加自定義表情

去掉輔助線後,看上去更明顯一些。

ImageSpan

ImageSpan 就簡單多了,它隻實作了 getDrawable() 方法來擷取 Drawable 執行個體,代碼如下:

@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;
}

           

這裡代碼很簡單,我們唯一需要關注的就是擷取 Drawable 時,需要設定它的寬高,讓它别超過文字的大小。

實作

說完前面的原理後,實作起來就非常簡單了。我們隻需要繼承 DynamicDrawableSpan,實作 getDrawable() 方法,讓圖檔的寬高别超過文字的大小就行了。效果如下圖所示:

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;
    }
}
           
【用戶端】給你的 Android App 添加自定義表情

上面看上去很完美,但是事情沒有那麼簡單。因為我們隻是寫死了圖檔的大小,并沒有改變圖檔位置繪制的算法。如果其他地方使用了 EmojiSpan ,但是文字的大小小于圖檔大小時還是會出問題。如下圖,當文字的 textsize 為 10sp 時的情況。

【用戶端】給你的 Android App 添加自定義表情

實際上,文字大于圖檔大小時也有問題。如下圖所示,多行的情況下,隻有表情的行間距明顯小于其他行的間距。

【用戶端】給你的 Android App 添加自定義表情

參考

繼續閱讀