Custom emoji in more places

This commit is contained in:
Grishka 2022-02-10 22:10:16 +03:00
parent 82a8d0cc29
commit c9078ca8d7
7 changed files with 140 additions and 16 deletions

View File

@ -5,6 +5,7 @@ import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape; import android.graphics.drawable.shapes.RoundRectShape;
import android.os.Bundle; import android.os.Bundle;
@ -17,6 +18,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.LinkedTextView;
@ -28,7 +30,11 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper; import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
@ -44,6 +50,7 @@ public class ProfileAboutFragment extends Fragment{
private boolean isInEditMode; private boolean isInEditMode;
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback()); private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private RecyclerView.ViewHolder draggedViewHolder; private RecyclerView.ViewHolder draggedViewHolder;
private ListImageLoaderWrapper imgLoader;
public void setFields(List<AccountField> fields){ public void setFields(List<AccountField> fields){
this.fields=fields; this.fields=fields;
@ -61,6 +68,7 @@ public class ProfileAboutFragment extends Fragment{
list=new UsableRecyclerView(getActivity()); list=new UsableRecyclerView(getActivity());
list.setId(R.id.list); list.setId(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
list.setAdapter(adapter=new AboutAdapter()); list.setAdapter(adapter=new AboutAdapter());
int pad=V.dp(16); int pad=V.dp(16);
list.setPadding(pad, pad, pad, pad); list.setPadding(pad, pad, pad, pad);
@ -95,9 +103,9 @@ public class ProfileAboutFragment extends Fragment{
return fields; return fields;
} }
private class AboutAdapter extends UsableRecyclerView.Adapter<BaseViewHolder>{ private class AboutAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter{
public AboutAdapter(){ public AboutAdapter(){
super(null); super(imgLoader);
} }
@NonNull @NonNull
@ -139,6 +147,16 @@ public class ProfileAboutFragment extends Fragment{
} }
return 0; return 0;
} }
@Override
public int getImageCountForItem(int position){
return isInEditMode || fields.get(position).emojiRequests==null ? 0 : fields.get(position).emojiRequests.size();
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return fields.get(position).emojiRequests.get(image);
}
} }
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{ private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
@ -164,7 +182,7 @@ public class ProfileAboutFragment extends Fragment{
} }
} }
private class AboutViewHolder extends BaseViewHolder{ private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
private TextView title; private TextView title;
private LinkedTextView value; private LinkedTextView value;
@ -177,9 +195,22 @@ public class ProfileAboutFragment extends Fragment{
@Override @Override
public void onBind(AccountField item){ public void onBind(AccountField item){
super.onBind(item); super.onBind(item);
title.setText(item.name); title.setText(item.parsedName);
value.setText(item.parsedValue); value.setText(item.parsedValue);
} }
@Override
public void setImage(int index, Drawable image){
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
span.setDrawable(image);
title.invalidate();
value.invalidate();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
} }
private class EditableAboutViewHolder extends BaseViewHolder{ private class EditableAboutViewHolder extends BaseViewHolder{

View File

@ -12,6 +12,7 @@ import android.graphics.Outline;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -42,6 +43,7 @@ import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable; import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView; import org.joinmastodon.android.ui.views.CoverImageView;
@ -266,7 +268,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount)); setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(account.avatar, V.dp(100), V.dp(100))); ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(account.avatar, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(account.header, 1000, 1000)); ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(account.header, 1000, 1000));
name.setText(account.displayName); SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
username.setText('@'+account.acct); username.setText('@'+account.acct);
bio.setText(HtmlParser.parse(account.note, account.emojis)); bio.setText(HtmlParser.parse(account.note, account.emojis));
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount)); followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
@ -276,6 +281,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingLabel.setText(getResources().getQuantityString(R.plurals.following, account.followingCount)); followingLabel.setText(getResources().getQuantityString(R.plurals.following, account.followingCount));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, account.statusesCount)); postsLabel.setText(getResources().getQuantityString(R.plurals.posts, account.statusesCount));
UiUtils.loadCustomEmojiInTextView(name);
UiUtils.loadCustomEmojiInTextView(bio);
if(AccountSessionManager.getInstance().isSelf(accountID, account)){ if(AccountSessionManager.getInstance().isSelf(accountID, account)){
actionButton.setText(R.string.edit_profile); actionButton.setText(R.string.edit_profile);
}else{ }else{
@ -285,12 +293,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
fields.clear(); fields.clear();
AccountField joined=new AccountField(); AccountField joined=new AccountField();
joined.name=getString(R.string.profile_joined); joined.parsedName=joined.name=getString(R.string.profile_joined);
joined.parsedValue=joined.value=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(LocalDateTime.ofInstant(account.createdAt, ZoneId.systemDefault())); joined.parsedValue=joined.value=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(LocalDateTime.ofInstant(account.createdAt, ZoneId.systemDefault()));
fields.add(joined); fields.add(joined);
for(AccountField field:account.fields){ for(AccountField field:account.fields){
field.parsedValue=HtmlParser.parse(field.value, account.emojis); field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis);
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
field.parsedName=ssb;
field.nameEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
field.emojiRequests=new ArrayList<>(field.nameEmojis.length+field.valueEmojis.length);
for(CustomEmojiSpan span:field.nameEmojis){
field.emojiRequests.add(span.createImageLoaderRequest());
}
for(CustomEmojiSpan span:field.valueEmojis){
field.emojiRequests.add(span.createImageLoaderRequest());
}
fields.add(field); fields.add(field);
} }

View File

@ -1,10 +1,13 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.parceler.Parcel; import org.parceler.Parcel;
import org.parceler.Transient;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
/** /**
* Represents a profile field as a name-value pair with optional verification. * Represents a profile field as a name-value pair with optional verification.
@ -26,7 +29,9 @@ public class AccountField extends BaseModel{
*/ */
public Instant verifiedAt; public Instant verifiedAt;
public transient CharSequence parsedValue; public transient CharSequence parsedValue, parsedName;
public transient CustomEmojiSpan[] valueEmojis, nameEmojis;
public transient ArrayList<UrlImageLoaderRequest> emojiRequests;
@Override @Override
public String toString(){ public String toString(){

View File

@ -6,6 +6,8 @@ import android.graphics.Outline;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider; import android.view.ViewOutlineProvider;
@ -16,6 +18,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
@ -34,6 +38,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private ImageLoaderRequest avaRequest; private ImageLoaderRequest avaRequest;
private Fragment parentFragment; private Fragment parentFragment;
private String accountID; private String accountID;
private ImageLoaderRequest[] emojiRequests;
private CustomEmojiSpan[] emojiSpans;
private SpannableStringBuilder parsedName;
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID){ public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID){
super(parentID, parentFragment); super(parentID, parentFragment);
@ -42,6 +49,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
avaRequest=new UrlImageLoaderRequest(user.avatar); avaRequest=new UrlImageLoaderRequest(user.avatar);
this.parentFragment=parentFragment; this.parentFragment=parentFragment;
this.accountID=accountID; this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName);
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
emojiSpans=parsedName.getSpans(0, parsedName.length(), CustomEmojiSpan.class);
emojiRequests=new ImageLoaderRequest[emojiSpans.length];
for(int i=0; i<emojiSpans.length; i++){
emojiRequests[i]=emojiSpans[i].createImageLoaderRequest();
}
} }
@Override @Override
@ -51,11 +65,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public int getImageCount(){ public int getImageCount(){
return 1; return 1+emojiRequests.length;
} }
@Override @Override
public ImageLoaderRequest getImageRequest(int index){ public ImageLoaderRequest getImageRequest(int index){
if(index>0){
return emojiRequests[index-1];
}
return avaRequest; return avaRequest;
} }
@ -85,21 +102,25 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void onBind(HeaderStatusDisplayItem item){ public void onBind(HeaderStatusDisplayItem item){
name.setText(item.user.displayName); name.setText(item.parsedName);
username.setText('@'+item.user.acct); username.setText('@'+item.user.acct);
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt)); timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
} }
@Override @Override
public void setImage(int index, Drawable drawable){ public void setImage(int index, Drawable drawable){
avatar.setImageDrawable(drawable); if(index>0){
item.emojiSpans[index-1].setDrawable(drawable);
}else{
avatar.setImageDrawable(drawable);
}
if(drawable instanceof Animatable) if(drawable instanceof Animatable)
((Animatable) drawable).start(); ((Animatable) drawable).start();
} }
@Override @Override
public void clearImage(int index){ public void clearImage(int index){
avatar.setImageBitmap(null); setImage(index, null);
} }
private void onAvaClick(View v){ private void onAvaClick(View v){

View File

@ -33,9 +33,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
if(text instanceof Spanned){ if(text instanceof Spanned){
CustomEmojiSpan[] emojiSpans=((Spanned) text).getSpans(0, text.length(), CustomEmojiSpan.class); CustomEmojiSpan[] emojiSpans=((Spanned) text).getSpans(0, text.length(), CustomEmojiSpan.class);
emojiRequests=new ImageLoaderRequest[emojiSpans.length]; emojiRequests=new ImageLoaderRequest[emojiSpans.length];
int emojiSize=V.dp(20);
for(int i=0; i<emojiSpans.length; i++){ for(int i=0; i<emojiSpans.length; i++){
emojiRequests[i]=new UrlImageLoaderRequest(emojiSpans[i].emoji.url, emojiSize, emojiSize); emojiRequests[i]=emojiSpans[i].createImageLoaderRequest();
} }
}else{ }else{
emojiRequests=new ImageLoaderRequest[0]; emojiRequests=new ImageLoaderRequest[0];

View File

@ -10,6 +10,8 @@ import org.joinmastodon.android.model.Emoji;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class CustomEmojiSpan extends ReplacementSpan{ public class CustomEmojiSpan extends ReplacementSpan{
public final Emoji emoji; public final Emoji emoji;
@ -48,4 +50,9 @@ public class CustomEmojiSpan extends ReplacementSpan{
public void setDrawable(Drawable drawable){ public void setDrawable(Drawable drawable){
this.drawable=drawable; this.drawable=drawable;
} }
public UrlImageLoaderRequest createImageLoaderRequest(){
int size=V.dp(20);
return new UrlImageLoaderRequest(emoji.url, size, size);
}
} }

View File

@ -2,23 +2,34 @@ package org.joinmastodon.android.ui.utils;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.text.Spanned;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import androidx.annotation.ColorRes; import androidx.annotation.ColorRes;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class UiUtils{ public class UiUtils{
private static Handler mainHandler=new Handler(Looper.getMainLooper()); private static Handler mainHandler=new Handler(Looper.getMainLooper());
@ -93,4 +104,34 @@ public class UiUtils{
} }
return uri.getLastPathSegment(); return uri.getLastPathSegment();
} }
public static void loadCustomEmojiInTextView(TextView view){
CharSequence _text=view.getText();
if(!(_text instanceof Spanned))
return;
Spanned text=(Spanned)_text;
CustomEmojiSpan[] spans=text.getSpans(0, text.length(), CustomEmojiSpan.class);
if(spans.length==0)
return;
int emojiSize=V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji=Arrays.stream(spans).collect(Collectors.groupingBy(s->s.emoji));
for(Map.Entry<Emoji, List<CustomEmojiSpan>> emoji:spansByEmoji.entrySet()){
ViewImageLoader.load(new ViewImageLoader.Target(){
@Override
public void setImageDrawable(Drawable d){
if(d==null)
return;
for(CustomEmojiSpan span:emoji.getValue()){
span.setDrawable(d);
}
view.invalidate();
}
@Override
public View getView(){
return view;
}
}, null, new UrlImageLoaderRequest(emoji.getKey().url, emojiSize, emojiSize), null, false, true);
}
}
} }