Android アプリにカスタム絵文字を追加する方法

前回の記事Android Span 原理分析では、 Span の原理を紹介しました。この記事では、Span のアプリケーションを紹介し、Span を使用してカスタム絵文字をアプリに追加します。

原理

カスタム絵文字を追加する原理は実際には非常に簡単です。つまり、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 では元の画像サイズが表示されるため、画像のサイズが期待どおりになっていないことがわかります。

ここに画像の説明を挿入
ReplacementSpanImageSpanの継承関係図は以下の通りで、DynamicDrawableSpan新たに2つのクラスが登場しましたので、まずは見てみましょう。MetricAffectingSpanおよびインターフェースについてはAndroid スパン原理分析CharacterStyle紹介されているので、ここでは詳しく説明しません。
ここに画像の説明を挿入

ReplacementSpan インターフェース

ReplacementSpanこれはインターフェイスであり、名前はテキストを置き換えるために使用されます。以下に示すように、2 つのメソッドが定義されています。

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

置換された Span の幅を返します。上記の例では、画像の幅を返します。パラメータは次のとおりです:

  • ペイント: ペイントのインスタンス
  • text: 現在のテキスト。上記の例の値はhaha​​haha[poor]です。
  • start: スパンの開始位置、ここでは 4
  • end: スパンの終了位置、ここでは 8
  • fm: FontMetricsInt のインスタンス

FontMetricsIntは、特定のテキスト サイズに対するフォントのさまざまなメトリックを記述するクラスです。内部属性が表す意味は以下のとおりです。

  • 上:図の紫線の位置
  • Ascent: 図の緑線の位置
  • Descent: 図の青い線の位置
  • 下:図の黄色線の位置
  • リーディング: 図にはマークされていませんが、前の行の下部と次の行の上部の間の距離を指します。

画像来源Android の FontMetrics におけるトップ、アセント、ベースライン、ディセント、ボトム、リーディングの意味
ここに画像の説明を挿入

ベースラインはテキスト描画のベースラインです。これは では定義されていませんが、のプロパティを通じて取得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: Canvas インスタンス
  • テキスト: 現在のテキスト
  • start: スパンの開始位置
  • end: スパンの終了位置
  • x: [不良]のx座標位置
  • top: 現在の行の「Top」属性値
  • y: 現在の行のベースライン
  • Bottom: 現在の行の「Bottom」属性値
  • ペイント: ペイント インスタンス、null の可能性があります

ここでは、上記とは少し異なるTopBottomに特に注意する必要があります。最初にここを覚えておいてください。

動的DrawableSpan

DynamicDrawableSpanReplacementSpanインターフェースのメソッドを実装します。同時に、これはgetDrawable抽象メソッドを定義する抽象クラスでもあり、ImageSpanDrawable インスタンスを取得するために実装されます。ソースコードは次のとおりです。

@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特に注意が必要な穴が 2 つあります。

最初の落とし穴は、メソッド内のオブジェクトgetSizeメソッドを通じてPaint.FontMetricsInt取得されたオブジェクトはオブジェクトではないということです。つまり、にどのような値を設定しても、フェッチ オブジェクトの値には影響しません。これは、 topbottomの値に影響するため、パラメーターを先ほど紹介したときに Top と Bottom を引用しました。drawpaint.getFontMetricsInt()getSizePaint.FontMetricsIntpaint.getFontMetricsInt()

2 番目の落とし穴は、画像サイズがテキスト サイズを超えるALIGN_CENTERと「機能しない」ことです下図のように、表示の便宜上補助線を追加しましたが、白い線はパラメータの上下を表しますが、下は他の色で隠れています。画像は中央に配置されていますが、テキストは中央に配置されていないため、効果がないように見えます。ALIGN_CENTER

ここに画像の説明を挿入
補助線を削除すると、よりわかりやすくなります。
ここに画像の説明を挿入

画像スパン

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

ここに画像の説明を挿入
上記は完璧に見えますが、物事はそれほど単純ではありません。画像のサイズをハードコーディングしただけで、画像の位置を描画するアルゴリズムを変更していないためです。他の場所で使用する場合EmojiSpanでも、テキストのサイズが画像のサイズより小さい場合は、依然として問題が発生します。下図のように、テキストの文字サイズが10spの場合の様子です。

ここに画像の説明を挿入
実際、テキストが画像サイズよりも大きい場合にも問題が発生します。下図に示すように、複数行の場合、式の行間隔だけが他の行の間隔に比べて大幅に狭くなります。
ここに画像の説明を挿入

このソリューションに興味がある場合は、いいね + お気に入り >= 40 を押してください。ステーション B のカスタム絵文字と、移動できるカスタム絵文字 (実際には Gif 画像) を再現します。

参考

おすすめ

転載: blog.csdn.net/lichukuan/article/details/126916228