package com.joanzapata.iconify.internal;
import android.annotation.TargetApi;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.SystemClock;
import android.support.annotation.CheckResult;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.text.TextDirectionHeuristic;
import android.text.TextDirectionHeuristics;
import android.text.method.PasswordTransformationMethod;
import android.text.style.ReplacementSpan;
import android.widget.TextView;
import com.joanzapata.iconify.Icon;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import static android.view.View.TEXT_DIRECTION_ANY_RTL;
import static android.view.View.TEXT_DIRECTION_FIRST_STRONG;
import static android.view.View.TEXT_DIRECTION_FIRST_STRONG_LTR;
import static android.view.View.TEXT_DIRECTION_FIRST_STRONG_RTL;
import static android.view.View.TEXT_DIRECTION_LOCALE;
import static android.view.View.TEXT_DIRECTION_LTR;
import static android.view.View.TEXT_DIRECTION_RTL;
public class CustomTypefaceSpan extends ReplacementSpan {
private static final int ROTATION_DURATION = 600;
// Font Awesome uses 8-step rotation for pulse, and
// it seems to have the only pulsing spinner. If
// spinners with different pulses are introduced at
// some point, then a pulse property can be
// implemented for the icons.
private static final int ROTATION_PULSES = 8;
private static final int ROTATION_PULSE_DURATION = ROTATION_DURATION / ROTATION_PULSES;
private static final Rect TEXT_BOUNDS = new Rect();
private static final Rect DIRTY_REGION = new Rect();
private static final RectF DIRTY_REGION_FLOAT = new RectF();
private static final Matrix ROTATION_MATRIX = new Matrix();
private static final Paint LOCAL_PAINT = new Paint();
private static final float BASELINE_RATIO = 1 / 7f;
@NonNull
private final TextView view;
@NonNull
private final String icon;
@NonNull
private final Typeface type;
@Size
private final float iconSizePx;
@FloatRange(from = -1f, to = 1f)
private final float iconSizeRatio;
@ColorInt
private final int iconColor;
private final boolean mirrorable;
@NonNull
private final Animation animation;
private final boolean baselineAligned;
@IntRange(from = -1)
private long spinStartTime;
public CustomTypefaceSpan(@NonNull TextView view, @NonNull Icon icon,
@NonNull Typeface type, @Size float iconSizePx,
@FloatRange(from = 0f, to = 1f) float iconSizeRatio,
@ColorInt int iconColor, @NonNull Animation animation,
boolean baselineAligned) {
this.view = view;
this.animation = animation;
this.baselineAligned = baselineAligned;
this.icon = String.valueOf(icon.character());
this.type = type;
this.iconSizePx = iconSizePx;
this.iconSizeRatio = iconSizeRatio;
this.iconColor = iconColor;
this.mirrorable = SDK_INT >= JELLY_BEAN_MR1 && icon.supportsRtl();
}
@Override
@CheckResult
public int getSize(@NonNull Paint paint, @NonNull CharSequence text,
@IntRange(from = 0) int start, @IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
LOCAL_PAINT.set(paint);
applyCustomTypeFace(LOCAL_PAINT, type);
LOCAL_PAINT.getTextBounds(icon, 0, 1, TEXT_BOUNDS);
if (fm != null) {
float baselineRatio = baselineAligned ? 0 : BASELINE_RATIO;
fm.descent = (int) (TEXT_BOUNDS.height() * baselineRatio);
fm.ascent = -(TEXT_BOUNDS.height() - fm.descent);
fm.top = fm.ascent;
fm.bottom = fm.descent;
}
return TEXT_BOUNDS.width();
}
@Override
@TargetApi(JELLY_BEAN_MR1)
public void draw(@NonNull Canvas canvas, @NonNull CharSequence text,
@IntRange(from = 0) int start, @IntRange(from = 0) int end,
float x, int top, int y, int bottom, @NonNull Paint paint) {
applyCustomTypeFace(paint, type);
paint.getTextBounds(icon, 0, 1, TEXT_BOUNDS);
int width = TEXT_BOUNDS.width();
int height = TEXT_BOUNDS.height();
float baselineRatio = baselineAligned ? 0f : BASELINE_RATIO;
canvas.save();
float baselineOffset = height * baselineRatio;
float offsetY = y - TEXT_BOUNDS.bottom + baselineOffset;
if (!needMirroring()) {
canvas.translate(x - TEXT_BOUNDS.left, offsetY);
} else {
canvas.translate(x + width + TEXT_BOUNDS.left, offsetY);
canvas.scale(-1.0f, 1.0f);
}
if (animation != Animation.NONE) {
DIRTY_REGION.set(TEXT_BOUNDS);
DIRTY_REGION.offsetTo((int) x, y - height + Math.round(baselineOffset));
long currentTime = SystemClock.uptimeMillis();
if (spinStartTime < 0) {
spinStartTime = currentTime;
switch (animation) {
case PULSE:
view.postInvalidateDelayed(ROTATION_PULSE_DURATION,
DIRTY_REGION.left, DIRTY_REGION.top,
DIRTY_REGION.right, DIRTY_REGION.bottom);
break;
case SPIN:
view.invalidate(DIRTY_REGION);
break;
}
} else {
long timeElapsed = currentTime - spinStartTime;
float rotation;
switch (animation) {
case PULSE:
rotation = timeElapsed / (float) ROTATION_PULSE_DURATION;
long invalidationDelay = ROTATION_PULSE_DURATION -
(timeElapsed % ROTATION_PULSE_DURATION);
rotation = ((int) Math.floor(rotation)) * 360f / ROTATION_PULSES;
rotateDirtyRegion(rotation);
view.postInvalidateDelayed(invalidationDelay,
DIRTY_REGION.left, DIRTY_REGION.top,
DIRTY_REGION.right, DIRTY_REGION.bottom);
break;
case SPIN:
rotation = timeElapsed / (float) ROTATION_DURATION * 360f;
rotateDirtyRegion(rotation);
view.invalidate(DIRTY_REGION);
break;
default:
throw new IllegalStateException();
}
float centerX = TEXT_BOUNDS.left + width / 2f;
float centerY = TEXT_BOUNDS.bottom - height / 2f;
canvas.rotate(rotation, centerX, centerY);
}
}
canvas.drawText(icon, 0, 0, paint);
canvas.restore();
}
// Rotate the dirty rectangle and set it to the new
// bounds containing the rotated rectangle.
private static void rotateDirtyRegion(@FloatRange(from = 0) float rotation) {
DIRTY_REGION_FLOAT.set(DIRTY_REGION);
ROTATION_MATRIX.postRotate(rotation,
DIRTY_REGION_FLOAT.centerX(), DIRTY_REGION_FLOAT.centerY());
ROTATION_MATRIX.mapRect(DIRTY_REGION_FLOAT);
DIRTY_REGION_FLOAT.round(DIRTY_REGION);
}
private void applyCustomTypeFace(@NonNull Paint paint, @NonNull Typeface tf) {
paint.setFakeBoldText(false);
paint.setTextSkewX(0f);
paint.setTypeface(tf);
if (animation != Animation.NONE) paint.clearShadowLayer();
if (iconSizeRatio > 0) paint.setTextSize(paint.getTextSize() * iconSizeRatio);
else if (iconSizePx > 0) paint.setTextSize(iconSizePx);
if (iconColor < Integer.MAX_VALUE) paint.setColor(iconColor);
}
// Since the 'mirrorable' flag is only set to true if the SDK
// version supports it, we don't need an explicit check for that
// before calling getLayoutDirection().
@TargetApi(JELLY_BEAN_MR1)
@CheckResult
private boolean needMirroring() {
if (!mirrorable) return false;
// Passwords fields should be LTR
if (view.getTransformationMethod() instanceof PasswordTransformationMethod) {
return false;
}
// Always need to resolve layout direction first
final boolean defaultIsRtl = view.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
if (SDK_INT < JELLY_BEAN_MR2) {
return defaultIsRtl;
}
// Select the text direction heuristic according to the
// package-private getTextDirectionHeuristic() method in TextView
TextDirectionHeuristic textDirectionHeuristic;
switch (view.getTextDirection()) {
default:
case TEXT_DIRECTION_FIRST_STRONG:
textDirectionHeuristic = defaultIsRtl ?
TextDirectionHeuristics.FIRSTSTRONG_RTL :
TextDirectionHeuristics.FIRSTSTRONG_LTR;
break;
case TEXT_DIRECTION_ANY_RTL:
textDirectionHeuristic = TextDirectionHeuristics.ANYRTL_LTR;
break;
case TEXT_DIRECTION_LTR:
textDirectionHeuristic = TextDirectionHeuristics.LTR;
break;
case TEXT_DIRECTION_RTL:
textDirectionHeuristic = TextDirectionHeuristics.RTL;
break;
case TEXT_DIRECTION_LOCALE:
textDirectionHeuristic = TextDirectionHeuristics.LOCALE;
break;
case TEXT_DIRECTION_FIRST_STRONG_LTR:
textDirectionHeuristic = TextDirectionHeuristics.FIRSTSTRONG_LTR;
break;
case TEXT_DIRECTION_FIRST_STRONG_RTL:
textDirectionHeuristic = TextDirectionHeuristics.FIRSTSTRONG_RTL;
break;
}
CharSequence text = view.getText();
return textDirectionHeuristic.isRtl(text, 0, text.length());
}
}