Loading app/src/main/java/app/fedilab/android/mastodon/helper/CustomEmoji.java +33 −122 Original line number Diff line number Diff line Loading @@ -14,10 +14,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; 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 java.lang.ref.WeakReference; import java.util.List; Loading Loading @@ -50,23 +46,23 @@ public class CustomEmoji extends ReplacementSpan { } public SpannableStringBuilder makeEmoji(SpannableStringBuilder content, List<Emoji> emojiList, boolean animate, Status.Callback callback) { if (emojiList != null && !emojiList.isEmpty()) { View view = viewWeakReference.get(); if (view == null || emojiList == null || emojiList.isEmpty()) { return content; } int count = 1; for (Emoji emoji : emojiList) { Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) .matcher(content); while (matcher.find()) { CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(viewWeakReference.get())); CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); String emojiUrl = animate ? emoji.url : emoji.static_url; Glide.with(viewWeakReference.get()) .asDrawable() .load(emojiUrl) .into(customEmoji.getTarget(animate, count == emojiList.size() && !callbackCalled ? callback : null, emojiUrl)); boolean isLastEmoji = count == emojiList.size() && !callbackCalled; customEmoji.loadEmoji(view, emojiUrl, animate, isLastEmoji ? callback : null); } count++; } } return content; } Loading @@ -78,7 +74,7 @@ public class CustomEmoji extends ReplacementSpan { @Override public void invalidateDrawable(@NonNull Drawable drawable) { if (view != null) { view.invalidate(); view.postInvalidate(); } } Loading @@ -100,8 +96,17 @@ public class CustomEmoji extends ReplacementSpan { ((Animatable) resource).start(); } imageDrawable = resource; if (view != null) { if (view instanceof android.widget.TextView) { android.widget.TextView tv = (android.widget.TextView) view; tv.post(() -> { CharSequence text = tv.getText(); tv.setText(text, android.widget.TextView.BufferType.SPANNABLE); }); } else if (view != null) { view.post(() -> { view.invalidate(); view.requestLayout(); }); } if (callback != null && !callbackCalled) { callbackCalled = true; Loading @@ -113,15 +118,18 @@ public class CustomEmoji extends ReplacementSpan { loadFailed = true; View view = viewWeakReference.get(); if (view != null) { view.post(() -> { view.invalidate(); view.requestLayout(); }); } } public void loadEmoji(View view, String url, boolean animate) { public void loadEmoji(View view, String url, boolean animate, Status.Callback callback) { EmojiLoader.loadEmojiSpan(view, url, animate, new EmojiLoader.DrawableCallback() { @Override public void onLoaded(Drawable drawable, boolean shouldAnimate) { onEmojiLoaded(drawable, shouldAnimate, null); onEmojiLoaded(drawable, shouldAnimate, callback); } @Override Loading @@ -131,101 +139,6 @@ public class CustomEmoji extends ReplacementSpan { }); } public Target<Drawable> getTarget(boolean animate, Status.Callback callback, String url) { return new CustomTarget<>() { @Override public void onStart() { if (imageDrawable instanceof Animatable) { ((Animatable) imageDrawable).start(); } } @Override public void onStop() { if (imageDrawable instanceof Animatable) { ((Animatable) imageDrawable).stop(); } } @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { View view = viewWeakReference.get(); if (animate && resource instanceof Animatable) { resource.setCallback(new Drawable.Callback() { @Override public void invalidateDrawable(@NonNull Drawable drawable) { if (view != null) { view.invalidate(); } } @Override public void scheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable, long l) { if (view != null) { view.postDelayed(runnable, l); } } @Override public void unscheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable) { if (view != null) { view.removeCallbacks(runnable); } } }); ((Animatable) resource).start(); } else if (animate && view != null) { EmojiLoader.loadEmojiSpan(view, url, true, new EmojiLoader.DrawableCallback() { @Override public void onLoaded(Drawable drawable, boolean shouldAnimate) { if (drawable instanceof Animatable) { onEmojiLoaded(drawable, true, null); } } @Override public void onFailed() { } }); } imageDrawable = resource; if (view != null) { view.invalidate(); } if (callback != null && !callbackCalled) { callbackCalled = true; callback.emojiFetched(); } } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { loadFailed = true; View view = viewWeakReference.get(); if (view != null) { view.invalidate(); } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { View view = viewWeakReference.get(); if (imageDrawable != null) { if (imageDrawable instanceof Animatable) { ((Animatable) imageDrawable).stop(); imageDrawable.setCallback(null); } } imageDrawable = null; if (view != null) { view.invalidate(); } } }; } @Override public int getSize(@NonNull Paint paint, CharSequence charSequence, int i, int i1, @Nullable Paint.FontMetricsInt fontMetricsInt) { return (int) (paint.getTextSize() * scale); Loading @@ -242,8 +155,6 @@ public class CustomEmoji extends ReplacementSpan { canvas.translate(x, (float) transY); imageDrawable.draw(canvas); canvas.restore(); } else if (loadFailed) { canvas.drawText(charSequence, start, end, x, y, paint); } } Loading app/src/main/java/app/fedilab/android/mastodon/helper/EmojiLoader.java +39 −21 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; 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.github.penfeizhou.animation.apng.APNGDrawable; import com.github.penfeizhou.animation.gif.GifDrawable; Loading @@ -36,6 +37,10 @@ import com.github.penfeizhou.animation.webp.WebPDrawable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import app.fedilab.android.R; public class EmojiLoader { Loading @@ -59,14 +64,20 @@ public class EmojiLoader { }); } @SuppressWarnings("unchecked") public static void loadEmojiSpan(View view, String url, boolean animate, DrawableCallback callback) { if (!Helper.isValidContextForGlide(view.getContext())) { return; } Glide.with(view) .asFile() .load(url) .into(new CustomTarget<File>() { // Store target in view tag to prevent garbage collection List<Target<?>> targets = (List<Target<?>>) view.getTag(R.id.custom_emoji_targets); if (targets == null) { targets = new ArrayList<>(); view.setTag(R.id.custom_emoji_targets, targets); } CustomTarget<File> target = new CustomTarget<File>() { @Override public void onResourceReady(@NonNull File file, @Nullable Transition<? super File> transition) { Drawable drawable = decodeFile(view.getResources(), file); Loading @@ -85,7 +96,14 @@ public class EmojiLoader { @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); }; targets.add(target); Glide.with(view.getContext()) .asFile() .load(url) .into(target); } private static Drawable decodeFile(Resources resources, File file) { Loading app/src/main/java/app/fedilab/android/mastodon/helper/SpannableHelper.java +22 −11 Original line number Diff line number Diff line Loading @@ -393,8 +393,21 @@ public class SpannableHelper { } } boolean animate = !sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); // Process emojis inline (same as convertEmoji) if (view != null && emojiList != null && !emojiList.isEmpty()) { for (Emoji emoji : emojiList) { Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) .matcher(content); while (matcher.find()) { CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content = customEmoji.makeEmoji(content, emojiList, animate, callback); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); if (Helper.isValidContextForGlide(context)) { String emojiUrl = animate ? emoji.url : emoji.static_url; customEmoji.loadEmoji(view, emojiUrl, animate, null); } } } } if (!imagesToReplace.isEmpty()) { for (Map.Entry<String, String> entry : imagesToReplace.entrySet()) { Loading @@ -405,10 +418,7 @@ public class SpannableHelper { while (matcher.find()) { CustomEmoji spanEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(spanEmoji, matcher.start(), matcher.end(), 0); Glide.with(view) .asDrawable() .load(url) .into(spanEmoji.getTarget(animate, null, url)); spanEmoji.loadEmoji(view, url, animate, null); } } Loading Loading @@ -1024,6 +1034,10 @@ public class SpannableHelper { if (text == null) { return null; } View view = viewWeakReference.get(); if (view == null) { return new SpannableString(text); } SpannableString initialContent = new SpannableString(text); SpannableStringBuilder content = new SpannableStringBuilder(initialContent); Loading @@ -1035,14 +1049,11 @@ public class SpannableHelper { Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) .matcher(content); while (matcher.find()) { CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(viewWeakReference.get())); CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); if (Helper.isValidContextForGlide(activity)) { String emojiUrl = animate ? emoji.url : emoji.static_url; Glide.with(viewWeakReference.get()) .asDrawable() .load(emojiUrl) .into(customEmoji.getTarget(animate, null, emojiUrl)); customEmoji.loadEmoji(view, emojiUrl, animate, null); } } } Loading app/src/main/res/values/ids.xml 0 → 100644 +4 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <resources> <item name="custom_emoji_targets" type="id" /> </resources> Loading
app/src/main/java/app/fedilab/android/mastodon/helper/CustomEmoji.java +33 −122 Original line number Diff line number Diff line Loading @@ -14,10 +14,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; 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 java.lang.ref.WeakReference; import java.util.List; Loading Loading @@ -50,23 +46,23 @@ public class CustomEmoji extends ReplacementSpan { } public SpannableStringBuilder makeEmoji(SpannableStringBuilder content, List<Emoji> emojiList, boolean animate, Status.Callback callback) { if (emojiList != null && !emojiList.isEmpty()) { View view = viewWeakReference.get(); if (view == null || emojiList == null || emojiList.isEmpty()) { return content; } int count = 1; for (Emoji emoji : emojiList) { Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) .matcher(content); while (matcher.find()) { CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(viewWeakReference.get())); CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); String emojiUrl = animate ? emoji.url : emoji.static_url; Glide.with(viewWeakReference.get()) .asDrawable() .load(emojiUrl) .into(customEmoji.getTarget(animate, count == emojiList.size() && !callbackCalled ? callback : null, emojiUrl)); boolean isLastEmoji = count == emojiList.size() && !callbackCalled; customEmoji.loadEmoji(view, emojiUrl, animate, isLastEmoji ? callback : null); } count++; } } return content; } Loading @@ -78,7 +74,7 @@ public class CustomEmoji extends ReplacementSpan { @Override public void invalidateDrawable(@NonNull Drawable drawable) { if (view != null) { view.invalidate(); view.postInvalidate(); } } Loading @@ -100,8 +96,17 @@ public class CustomEmoji extends ReplacementSpan { ((Animatable) resource).start(); } imageDrawable = resource; if (view != null) { if (view instanceof android.widget.TextView) { android.widget.TextView tv = (android.widget.TextView) view; tv.post(() -> { CharSequence text = tv.getText(); tv.setText(text, android.widget.TextView.BufferType.SPANNABLE); }); } else if (view != null) { view.post(() -> { view.invalidate(); view.requestLayout(); }); } if (callback != null && !callbackCalled) { callbackCalled = true; Loading @@ -113,15 +118,18 @@ public class CustomEmoji extends ReplacementSpan { loadFailed = true; View view = viewWeakReference.get(); if (view != null) { view.post(() -> { view.invalidate(); view.requestLayout(); }); } } public void loadEmoji(View view, String url, boolean animate) { public void loadEmoji(View view, String url, boolean animate, Status.Callback callback) { EmojiLoader.loadEmojiSpan(view, url, animate, new EmojiLoader.DrawableCallback() { @Override public void onLoaded(Drawable drawable, boolean shouldAnimate) { onEmojiLoaded(drawable, shouldAnimate, null); onEmojiLoaded(drawable, shouldAnimate, callback); } @Override Loading @@ -131,101 +139,6 @@ public class CustomEmoji extends ReplacementSpan { }); } public Target<Drawable> getTarget(boolean animate, Status.Callback callback, String url) { return new CustomTarget<>() { @Override public void onStart() { if (imageDrawable instanceof Animatable) { ((Animatable) imageDrawable).start(); } } @Override public void onStop() { if (imageDrawable instanceof Animatable) { ((Animatable) imageDrawable).stop(); } } @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { View view = viewWeakReference.get(); if (animate && resource instanceof Animatable) { resource.setCallback(new Drawable.Callback() { @Override public void invalidateDrawable(@NonNull Drawable drawable) { if (view != null) { view.invalidate(); } } @Override public void scheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable, long l) { if (view != null) { view.postDelayed(runnable, l); } } @Override public void unscheduleDrawable(@NonNull Drawable drawable, @NonNull Runnable runnable) { if (view != null) { view.removeCallbacks(runnable); } } }); ((Animatable) resource).start(); } else if (animate && view != null) { EmojiLoader.loadEmojiSpan(view, url, true, new EmojiLoader.DrawableCallback() { @Override public void onLoaded(Drawable drawable, boolean shouldAnimate) { if (drawable instanceof Animatable) { onEmojiLoaded(drawable, true, null); } } @Override public void onFailed() { } }); } imageDrawable = resource; if (view != null) { view.invalidate(); } if (callback != null && !callbackCalled) { callbackCalled = true; callback.emojiFetched(); } } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { loadFailed = true; View view = viewWeakReference.get(); if (view != null) { view.invalidate(); } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { View view = viewWeakReference.get(); if (imageDrawable != null) { if (imageDrawable instanceof Animatable) { ((Animatable) imageDrawable).stop(); imageDrawable.setCallback(null); } } imageDrawable = null; if (view != null) { view.invalidate(); } } }; } @Override public int getSize(@NonNull Paint paint, CharSequence charSequence, int i, int i1, @Nullable Paint.FontMetricsInt fontMetricsInt) { return (int) (paint.getTextSize() * scale); Loading @@ -242,8 +155,6 @@ public class CustomEmoji extends ReplacementSpan { canvas.translate(x, (float) transY); imageDrawable.draw(canvas); canvas.restore(); } else if (loadFailed) { canvas.drawText(charSequence, start, end, x, y, paint); } } Loading
app/src/main/java/app/fedilab/android/mastodon/helper/EmojiLoader.java +39 −21 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; 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.github.penfeizhou.animation.apng.APNGDrawable; import com.github.penfeizhou.animation.gif.GifDrawable; Loading @@ -36,6 +37,10 @@ import com.github.penfeizhou.animation.webp.WebPDrawable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import app.fedilab.android.R; public class EmojiLoader { Loading @@ -59,14 +64,20 @@ public class EmojiLoader { }); } @SuppressWarnings("unchecked") public static void loadEmojiSpan(View view, String url, boolean animate, DrawableCallback callback) { if (!Helper.isValidContextForGlide(view.getContext())) { return; } Glide.with(view) .asFile() .load(url) .into(new CustomTarget<File>() { // Store target in view tag to prevent garbage collection List<Target<?>> targets = (List<Target<?>>) view.getTag(R.id.custom_emoji_targets); if (targets == null) { targets = new ArrayList<>(); view.setTag(R.id.custom_emoji_targets, targets); } CustomTarget<File> target = new CustomTarget<File>() { @Override public void onResourceReady(@NonNull File file, @Nullable Transition<? super File> transition) { Drawable drawable = decodeFile(view.getResources(), file); Loading @@ -85,7 +96,14 @@ public class EmojiLoader { @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); }; targets.add(target); Glide.with(view.getContext()) .asFile() .load(url) .into(target); } private static Drawable decodeFile(Resources resources, File file) { Loading
app/src/main/java/app/fedilab/android/mastodon/helper/SpannableHelper.java +22 −11 Original line number Diff line number Diff line Loading @@ -393,8 +393,21 @@ public class SpannableHelper { } } boolean animate = !sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); // Process emojis inline (same as convertEmoji) if (view != null && emojiList != null && !emojiList.isEmpty()) { for (Emoji emoji : emojiList) { Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) .matcher(content); while (matcher.find()) { CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content = customEmoji.makeEmoji(content, emojiList, animate, callback); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); if (Helper.isValidContextForGlide(context)) { String emojiUrl = animate ? emoji.url : emoji.static_url; customEmoji.loadEmoji(view, emojiUrl, animate, null); } } } } if (!imagesToReplace.isEmpty()) { for (Map.Entry<String, String> entry : imagesToReplace.entrySet()) { Loading @@ -405,10 +418,7 @@ public class SpannableHelper { while (matcher.find()) { CustomEmoji spanEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(spanEmoji, matcher.start(), matcher.end(), 0); Glide.with(view) .asDrawable() .load(url) .into(spanEmoji.getTarget(animate, null, url)); spanEmoji.loadEmoji(view, url, animate, null); } } Loading Loading @@ -1024,6 +1034,10 @@ public class SpannableHelper { if (text == null) { return null; } View view = viewWeakReference.get(); if (view == null) { return new SpannableString(text); } SpannableString initialContent = new SpannableString(text); SpannableStringBuilder content = new SpannableStringBuilder(initialContent); Loading @@ -1035,14 +1049,11 @@ public class SpannableHelper { Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) .matcher(content); while (matcher.find()) { CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(viewWeakReference.get())); CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); if (Helper.isValidContextForGlide(activity)) { String emojiUrl = animate ? emoji.url : emoji.static_url; Glide.with(viewWeakReference.get()) .asDrawable() .load(emojiUrl) .into(customEmoji.getTarget(animate, null, emojiUrl)); customEmoji.loadEmoji(view, emojiUrl, animate, null); } } } Loading
app/src/main/res/values/ids.xml 0 → 100644 +4 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <resources> <item name="custom_emoji_targets" type="id" /> </resources>