/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
/**
* EmojiSpan subclass used to render emojis using Typeface.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(19)
public final class TypefaceEmojiSpan extends EmojiSpan {
/**
* Paint object used to draw a background in debug mode.
*/
private static @Nullable Paint sDebugPaint;
@Nullable
private TextPaint mWorkingPaint;
/**
* Default constructor.
*
* @param metadata metadata representing the emoji that this span will draw
*/
public TypefaceEmojiSpan(final @NonNull TypefaceEmojiRasterizer metadata) {
super(metadata);
}
@Override
public void draw(@NonNull final Canvas canvas,
@SuppressLint("UnknownNullness") final CharSequence text,
@IntRange(from = 0) final int start, @IntRange(from = 0) final int end, final float x,
final int top, final int y, final int bottom, @NonNull final Paint paint) {
@Nullable TextPaint textPaint = applyCharacterSpanStyles(text, start, end, paint);
if (textPaint != null && textPaint.bgColor != 0) {
drawBackground(canvas, textPaint, x, x + getWidth(), top, bottom);
}
if (EmojiCompat.get().isEmojiSpanIndicatorEnabled()) {
canvas.drawRect(x, top , x + getWidth(), bottom, getDebugPaint());
}
getTypefaceRasterizer().draw(canvas, x, y, textPaint != null ? textPaint : paint);
}
// compat behavior with TextLine.java#handleText background drawing
void drawBackground(Canvas c, TextPaint textPaint, float leftX, float rightX, float top,
float bottom) {
int previousColor = textPaint.getColor();
Paint.Style previousStyle = textPaint.getStyle();
textPaint.setColor(textPaint.bgColor);
textPaint.setStyle(Paint.Style.FILL);
c.drawRect(leftX, top, rightX, bottom, textPaint);
textPaint.setStyle(previousStyle);
textPaint.setColor(previousColor);
}
/**
* This applies the CharacterSpanStyles that _would_ have been applied to this character by
* StaticLayout.
*
* StaticLayout applies CharacterSpanStyles _after_ calling ReplacementSpan.draw, which means
* BackgroundSpan will not be applied before draw is called.
*
* If any CharacterSpanStyles would impact _this_ location, apply them to a TextPaint to
* determine if a background needs draw prior to the emoji.
*
* @param text text that this span is part of
* @param start start position to replace
* @param end end position to replace
* @param paint paint (from TextLine)
* @return TextPaint configured
*/
@Nullable
private TextPaint applyCharacterSpanStyles(@Nullable CharSequence text, int start, int end,
Paint paint) {
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
CharacterStyle[] spans = spanned.getSpans(start, end, CharacterStyle.class);
if (spans.length == 0 || (spans.length == 1 && spans[0] == this)) {
if (paint instanceof TextPaint) {
// happy path goes here, retain color and bgColor from caller
return (TextPaint) paint;
} else {
return null;
}
}
// there are some non-TypefaceEmojiSpan character styles to apply, update a working
// paint to apply each span style, like TextLine would have.
TextPaint wp = mWorkingPaint;
if (wp == null) {
wp = new TextPaint();
mWorkingPaint = wp;
}
wp.set(paint);
//noinspection ForLoopReplaceableByForEach
for (int pos = 0; pos < spans.length; pos++) {
spans[pos].updateDrawState(wp);
}
return wp;
} else {
if (paint instanceof TextPaint) {
// retain any color and bgColor from caller
return (TextPaint) paint;
} else {
return null;
}
}
}
@NonNull
private static Paint getDebugPaint() {
if (sDebugPaint == null) {
sDebugPaint = new TextPaint();
sDebugPaint.setColor(EmojiCompat.get().getEmojiSpanIndicatorColor());
sDebugPaint.setStyle(Paint.Style.FILL);
}
return sDebugPaint;
}
}