package org.wikipedia.views; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.style.ImageSpan; import android.util.AttributeSet; import java.util.ArrayList; import java.util.List; // Credit: https://stackoverflow.com/a/38977396 public class AppTextViewWithImages extends AppTextView { public AppTextViewWithImages(Context context) { super(context); } public AppTextViewWithImages(Context context, AttributeSet attrs) { super(context, attrs); } public AppTextViewWithImages(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * A method to set a Spanned character sequence containing drawable resources. * * @param text A CharSequence formatted for use in android.text.TextUtils.expandTemplate(), * e.g.: "^1 is my favorite mobile operating system." Placeholders are expected in * the format "^1", "^2", and so on. * @param drawableIds Numeric drawable IDs for the drawables which are to replace the * placeholders, in the order in which they should appear. */ public void setTextWithDrawables(@NonNull CharSequence text, @DrawableRes int... drawableIds) { setText(text, getImageSpans(drawableIds)); } private List<Spanned> getImageSpans(@DrawableRes int... drawableIds) { List<Spanned> result = new ArrayList<>(); for (int id : drawableIds) { Spanned span = makeImageSpan(id, getTextSize(), getCurrentTextColor()); result.add(span); } return result; } private void setText(@NonNull CharSequence text, @NonNull List<Spanned> spans) { if (!spans.isEmpty()) { CharSequence spanned = TextUtils.expandTemplate(text, spans.toArray(new CharSequence[spans.size()])); super.setText(spanned, BufferType.SPANNABLE); } else { super.setText(text); } } /** * Create an ImageSpan containing a drawable to be inserted in a TextView. This also sets the * image size and color. * * @param drawableId A drawable resource Id. * @param size The desired size (i.e. width and height) of the image icon in pixels. * @param color The color to apply to the image. * @return A single-length ImageSpan that can be swapped into a CharSequence to replace a * placeholder. */ @NonNull @VisibleForTesting Spannable makeImageSpan(@DrawableRes int drawableId, float size, @ColorInt int color) { Spannable result = Spannable.Factory.getInstance().newSpannable(" "); Drawable drawable = getFormattedDrawable(drawableId, size, color); result.setSpan(new BaselineAlignedYTranslationImageSpan(drawable, getLineSpacingMultiplier()), 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); return result; } @NonNull @VisibleForTesting Drawable getFormattedDrawable(@DrawableRes int drawableId, float size, @ColorInt int color) { Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId); DrawableCompat.setTint(drawable, color); float ratio = drawable.getIntrinsicWidth() / drawable.getIntrinsicHeight(); drawable.setBounds(0, 0, Math.round(size), Math.round(size * ratio)); return drawable; } /* Helper method for testing */ @NonNull @VisibleForTesting <T> T[] getSpans(@NonNull Class<T> clazz) { return ((SpannableString) getText()).getSpans(0, getText().length(), clazz); } /** * An ImageSpan subclass that manually adjusts the vertical position of the drawable it contains * to correct for the failure of ImageSpan.ALIGN_BASELINE to take into account any adjustments * to the parent view's line height (via lineSpacingMultiplier or lineSpacingExtra). * * The general approach is adapted (and simplified) from http://stackoverflow.com/a/28361364. * * Not written as generically as it could be since I don't think there's any need for this kind * of tweak elsewhere at present, but could probably be made more generic (i.e., made not to * assume ALIGN_BASELINE and to also account for any lineSpacingExtra) and broken out into a * standalone class if need be. * * A possibly related issue is https://code.google.com/p/android/issues/detail?id=21397, * but note that the problem this works around affects an ImageSpan on any line, not just the * last line as reported there. */ private static class BaselineAlignedYTranslationImageSpan extends ImageSpan { private float lineSpacingMultiplier; BaselineAlignedYTranslationImageSpan(@NonNull Drawable drawable, float lineSpacingMultiplier) { super(drawable, ImageSpan.ALIGN_BASELINE); this.lineSpacingMultiplier = lineSpacingMultiplier; } @Override @SuppressWarnings("checkstyle:parameternumber") public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable drawable = getDrawable(); canvas.save(); int transY = bottom - drawable.getBounds().bottom; transY -= paint.getFontMetricsInt().descent * lineSpacingMultiplier; canvas.translate(x, transY); drawable.draw(canvas); canvas.restore(); } } }