前回の記事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 では元の画像サイズが表示されるため、画像のサイズが期待どおりになっていないことがわかります。
ReplacementSpan
ImageSpanの継承関係図は以下の通りで、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: 現在のテキスト。上記の例の値はhahahaha[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 の可能性があります
ここでは、上記とは少し異なるTopとBottomに特に注意する必要があります。最初にここを覚えておいてください。
動的DrawableSpan
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
特に注意が必要な穴が 2 つあります。
最初の落とし穴は、メソッド内のオブジェクトgetSize
とメソッドを通じてPaint.FontMetricsInt
取得されたオブジェクトはオブジェクトではないということです。つまり、にどのような値を設定しても、フェッチ オブジェクトの値には影響しません。これは、 topとbottomの値に影響するため、パラメーターを先ほど紹介したときに Top と Bottom を引用しました。draw
paint.getFontMetricsInt()
getSize
Paint.FontMetricsInt
paint.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 画像) を再現します。