CustomEmojiHelper: rewrite to Kotlin (#1787)

* CustomEmojiHelper: rewrite to Kotlin

* CustomEmojiHelper: PR fixes
This commit is contained in:
Alibek Omarov 2020-05-15 23:09:12 +03:00 committed by GitHub
commit 2fc7ad13bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 151 additions and 190 deletions

View file

@ -1,146 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.style.ReplacementSpan;
import android.view.View;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import com.keylesspalace.tusky.entity.Emoji;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CustomEmojiHelper {
/**
* replaces emoji shortcodes in a text with EmojiSpans
* @param text the text containing custom emojis
* @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances)
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
* @return the text with the shortcodes replaced by EmojiSpans
*/
@NonNull
public static Spanned emojifyText(@NonNull Spanned text, @Nullable List<Emoji> emojis, @NonNull final View view) {
if (emojis != null && !emojis.isEmpty()) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
for (Emoji emoji : emojis) {
CharSequence pattern = new StringBuilder(":").append(emoji.getShortcode()).append(':');
Matcher matcher = Pattern.compile(pattern.toString()).matcher(text);
while (matcher.find()) {
EmojiSpan span = new EmojiSpan(view);
builder.setSpan(span, matcher.start(), matcher.end(), 0);
Glide.with(view)
.asBitmap()
.load(emoji.getUrl())
.into(span.getTarget());
}
}
return builder;
}
return text;
}
@NonNull
public static Spanned emojifyString(@NonNull String string, @Nullable List<Emoji> emojis, @NonNull final View ciew) {
return emojifyText(new SpannedString(string), emojis, ciew);
}
public static class EmojiSpan extends ReplacementSpan {
@Nullable
private Drawable imageDrawable;
private WeakReference<View> viewWeakReference;
EmojiSpan(View view) {
this.viewWeakReference = new WeakReference<>(view);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
@Nullable Paint.FontMetricsInt fm) {
/* update FontMetricsInt or otherwise span does not get drawn when
it covers the whole text */
Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
if (fm != null) {
fm.top = metrics.top;
fm.ascent = metrics.ascent;
fm.descent = metrics.descent;
fm.bottom = metrics.bottom;
}
return (int) (paint.getTextSize()*1.2);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
if (imageDrawable == null) return;
canvas.save();
int emojiSize = (int) (paint.getTextSize() * 1.1);
imageDrawable.setBounds(0, 0, emojiSize, emojiSize);
int transY = bottom - imageDrawable.getBounds().bottom;
transY -= paint.getFontMetricsInt().descent/2;
canvas.translate(x, transY);
imageDrawable.draw(canvas);
canvas.restore();
}
Target<Bitmap> getTarget(){
return new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
View view = viewWeakReference.get();
if (view != null) {
imageDrawable = new BitmapDrawable(view.getContext().getResources(), resource);
view.invalidate();
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
//Do nothing on load cleared
}
};
}
}
}

View file

@ -0,0 +1,112 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("CustomEmojiHelper")
package com.keylesspalace.tusky.util
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.text.SpannableStringBuilder
import android.text.style.ReplacementSpan
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import com.keylesspalace.tusky.entity.Emoji
import java.lang.ref.WeakReference
import java.util.regex.Pattern
/**
* replaces emoji shortcodes in a text with EmojiSpans
* @param text the text containing custom emojis
* @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances)
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
* @return the text with the shortcodes replaced by EmojiSpans
*/
fun CharSequence.emojify(emojis: List<Emoji>?, view: View) : CharSequence {
if(emojis.isNullOrEmpty())
return this
val builder = SpannableStringBuilder.valueOf(this)
emojis.forEach { (shortcode, url) ->
val matcher = Pattern.compile(":$shortcode:", Pattern.LITERAL)
.matcher(this)
while(matcher.find()) {
val span = EmojiSpan(WeakReference(view))
builder.setSpan(span, matcher.start(), matcher.end(), 0);
Glide.with(view)
.asBitmap()
.load(url)
.into(span.getTarget())
}
}
return builder
}
class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan() {
var imageDrawable: Drawable? = null
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) : Int {
if (fm != null) {
/* update FontMetricsInt or otherwise span does not get drawn when
* it covers the whole text */
val metrics = paint.fontMetricsInt
fm.top = metrics.top
fm.ascent = metrics.ascent
fm.descent = metrics.descent
fm.bottom = metrics.bottom
}
return (paint.textSize * 1.2).toInt()
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
imageDrawable?.let { drawable ->
canvas.save()
val emojiSize = (paint.textSize * 1.1).toInt()
drawable.setBounds(0, 0, emojiSize, emojiSize)
var transY = bottom - drawable.bounds.bottom
transY -= paint.fontMetricsInt.descent / 2;
canvas.translate(x, transY.toFloat())
drawable.draw(canvas)
canvas.restore()
}
}
fun getTarget(): Target<Bitmap> {
return object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
viewWeakReference.get()?.let { view ->
imageDrawable = BitmapDrawable(view.context.resources, resource)
view.invalidate()
}
}
override fun onLoadCleared(placeholder: Drawable?) {}
}
}
}

View file

@ -68,10 +68,10 @@ public class LinkHelper {
* @param mentions any '@' mentions which are known to be in the content
* @param listener to notify about particular spans that are clicked
*/
public static void setClickableText(TextView view, Spanned content,
public static void setClickableText(TextView view, CharSequence content,
@Nullable Status.Mention[] mentions, final LinkListener listener) {
SpannableStringBuilder builder = new SpannableStringBuilder(content);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content);
URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
int start = builder.getSpanStart(span);
int end = builder.getSpanEnd(span);

View file

@ -302,7 +302,7 @@ class StatusViewHelper(private val itemView: View) {
val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount)
val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context)
pollResults[i].text = CustomEmojiHelper.emojifyText(pollOptionText, emojis, pollResults[i])
pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i])
pollResults[i].visibility = View.VISIBLE
val level = percent * 100