Better char counter and custom emoji in compose

This commit is contained in:
Grishka
2022-02-02 09:40:29 +03:00
parent b9bdf7caec
commit c885a5fc28
19 changed files with 927 additions and 16 deletions

View File

@@ -18,6 +18,7 @@ public class MainActivity extends FragmentStackActivity{
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new SplashFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
HomeFragment fragment=new HomeFragment();

View File

@@ -163,4 +163,8 @@ public class MastodonAPIController{
}
}, 0);
}
public static void runInBackground(Runnable action){
thread.postRunnable(action, 0);
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Emoji;
import java.util.List;
public class GetCustomEmojis extends MastodonAPIRequest<List<Emoji>>{
public GetCustomEmojis(){
super(HttpMethod.GET, "/custom_emojis", new TypeToken<>(){});
}
}

View File

@@ -11,6 +11,7 @@ public class AccountSession{
public String domain;
public int tootCharLimit;
public Application app;
public long infoLastUpdated;
private transient MastodonAPIController apiController;
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit){
@@ -19,6 +20,7 @@ public class AccountSession{
this.domain=domain;
this.app=app;
this.tootCharLimit=tootCharLimit;
infoLastUpdated=System.currentTimeMillis();
}
AccountSession(){}

View File

@@ -2,21 +2,22 @@ package org.joinmastodon.android.api.session;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.OAuthActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
@@ -28,8 +29,13 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -46,11 +52,14 @@ public class AccountSessionManager{
private static final AccountSessionManager instance=new AccountSessionManager();
private HashMap<String, AccountSession> sessions=new HashMap<>();
private HashMap<String, List<EmojiCategory>> customEmojis=new HashMap<>();
private HashMap<String, Long> customEmojisLastUpdated=new HashMap<>();
private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
private Instance authenticatingInstance;
private Application authenticatingApp;
private String lastActiveAccountID;
private SharedPreferences prefs;
private boolean loadedCustomEmojis;
public static AccountSessionManager getInstance(){
return instance;
@@ -61,15 +70,18 @@ public class AccountSessionManager{
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
if(!file.exists())
return;
HashSet<String> domains=new HashSet<>();
try(FileInputStream in=new FileInputStream(file)){
SessionsStorageWrapper w=MastodonAPIController.gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), SessionsStorageWrapper.class);
for(AccountSession session:w.accounts){
domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session);
}
}catch(IOException x){
}catch(IOException|JsonParseException x){
Log.e(TAG, "Error loading accounts", x);
}
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readCustomEmojis(domains));
}
public void addAccount(Instance instance, Token token, Account self, Application app){
@@ -129,6 +141,10 @@ public class AccountSessionManager{
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
}
writeAccountsFile();
String domain=session.domain.toLowerCase();
if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){
getCustomEmojisFile(domain).delete();
}
}
@NonNull
@@ -181,7 +197,122 @@ public class AccountSessionManager{
return authenticatingApp;
}
public void maybeUpdateLocalInfo(){
long now=System.currentTimeMillis();
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionLocalInfo(session);
}
}
if(loadedCustomEmojis){
maybeUpdateCustomEmojis(domains);
}
}
private void maybeUpdateCustomEmojis(Set<String> domains){
long now=System.currentTimeMillis();
for(String domain:domains){
Long lastUpdated=customEmojisLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateCustomEmojis(domain);
}
}
}
private void updateSessionLocalInfo(AccountSession session){
new GetOwnAccount()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
session.self=result;
session.infoLastUpdated=System.currentTimeMillis();
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
}
})
.exec(session.getID());
}
private void updateCustomEmojis(String domain){
new GetCustomEmojis()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Emoji> result){
CustomEmojisStorageWrapper emojis=new CustomEmojisStorageWrapper();
emojis.lastUpdated=System.currentTimeMillis();
emojis.emojis=result;
customEmojis.put(domain, groupCustomEmojis(emojis));
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
MastodonAPIController.runInBackground(()->writeCustomEmojisFile(emojis, domain));
}
@Override
public void onError(ErrorResponse error){
}
})
.execNoAuth(domain);
}
private File getCustomEmojisFile(String domain){
return new File(MastodonApp.context.getFilesDir(), "emojis_"+domain.replace('.', '_')+".json");
}
private void writeCustomEmojisFile(CustomEmojisStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getCustomEmojisFile(domain))){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer);
writer.flush();
}catch(IOException x){
Log.w(TAG, "Error writing emojis file for "+domain, x);
}
}
private void readCustomEmojis(Set<String> domains){
for(String domain:domains){
try(FileInputStream in=new FileInputStream(getCustomEmojisFile(domain))){
InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8);
CustomEmojisStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, CustomEmojisStorageWrapper.class);
customEmojis.put(domain, groupCustomEmojis(emojis));
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
}catch(IOException|JsonParseException x){
Log.w(TAG, "Error reading emojis file for "+domain, x);
}
}
if(!loadedCustomEmojis){
loadedCustomEmojis=true;
maybeUpdateCustomEmojis(domains);
}
}
private List<EmojiCategory> groupCustomEmojis(CustomEmojisStorageWrapper emojis){
return emojis.emojis.stream()
.filter(e->e.visibleInPicker)
.collect(Collectors.groupingBy(e->e.category==null ? "" : e.category))
.entrySet()
.stream()
.map(e->new EmojiCategory(e.getKey(), e.getValue()))
.sorted(Comparator.comparing(c->c.title))
.collect(Collectors.toList());
}
public List<EmojiCategory> getCustomEmojis(String domain){
List<EmojiCategory> r=customEmojis.get(domain.toLowerCase());
return r==null ? Collections.emptyList() : r;
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
}
private static class CustomEmojisStorageWrapper{
public List<Emoji> emojis;
public long lastUpdated;
}
}

View File

@@ -1,7 +1,10 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.Outline;
import android.icu.text.BreakIterator;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@@ -14,10 +17,12 @@ import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.twitter.twittertext.Regex;
import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
@@ -26,9 +31,14 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.PopupKeyboard;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import java.text.BreakIterator;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
@@ -59,8 +69,10 @@ public class ComposeFragment extends ToolbarFragment{
")" +
")";
private static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
@SuppressLint("NewApi") // this class actually exists on 6.0
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
private SizeListenerLinearLayout contentView;
private TextView selfName, selfUsername;
private ImageView selfAvatar;
private Account self;
@@ -72,6 +84,10 @@ public class ComposeFragment extends ToolbarFragment{
private int charCount, charLimit;
private MenuItem publishButton;
private ImageButton emojiBtn;
private List<EmojiCategory> customEmojis;
private CustomEmojiPopupKeyboard emojiKeyboard;
@Override
public void onAttach(Activity activity){
@@ -84,6 +100,9 @@ public class ComposeFragment extends ToolbarFragment{
charLimit=500;
self=session.self;
instanceDomain=session.domain;
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
emojiKeyboard=new CustomEmojiPopupKeyboard(activity, customEmojis, instanceDomain);
emojiKeyboard.setListener(this::onCustomEmojiClick);
}
@Override
@@ -108,6 +127,18 @@ public class ComposeFragment extends ToolbarFragment{
selfAvatar.setOutlineProvider(roundCornersOutline);
selfAvatar.setClipToOutline(true);
emojiBtn=view.findViewById(R.id.btn_emoji);
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
public void onIconChanged(int icon){
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
}
});
contentView=(SizeListenerLinearLayout) view;
contentView.addView(emojiKeyboard.getView());
return view;
}
@@ -119,9 +150,10 @@ public class ComposeFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
contentView.setSizeListener(emojiKeyboard::onContentViewSizeChanged);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
mainEditText.requestFocus();
view.postDelayed(()->{
mainEditText.requestFocus();
imm.showSoftInput(mainEditText, 0);
}, 100);
@@ -173,10 +205,21 @@ public class ComposeFragment extends ToolbarFragment{
return true;
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
emojiKeyboard.onConfigurationChanged();
}
@SuppressLint("NewApi")
private void updateCharCounter(CharSequence text){
String countableText=MENTION_PATTERN.matcher(URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")).replaceAll("$1@$3");
breakIterator.setText(countableText);
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
MENTION_PATTERN.matcher(
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
).replaceAll("$1@$3")
).replaceAll("x");
charCount=0;
breakIterator.setText(countableText);
while(breakIterator.next()!=BreakIterator.DONE){
charCount++;
}
@@ -188,4 +231,8 @@ public class ComposeFragment extends ToolbarFragment{
private void updatePublishButtonState(){
publishButton.setEnabled(charCount>0 && charCount<=charLimit);
}
private void onCustomEmojiClick(Emoji emoji){
mainEditText.getText().replace(mainEditText.getSelectionStart(), mainEditText.getSelectionEnd(), ':'+emoji.shortcode+':');
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.model;
import java.util.List;
public class EmojiCategory{
public String title;
public List<Emoji> emojis;
public EmojiCategory(String title, List<Emoji> emojis){
this.title=title;
this.emojis=emojis;
}
}

View File

@@ -0,0 +1,194 @@
package org.joinmastodon.android.ui;
import android.app.Activity;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
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.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class CustomEmojiPopupKeyboard extends PopupKeyboard{
private List<EmojiCategory> emojis;
private UsableRecyclerView list;
private ListImageLoaderWrapper imgLoader;
private MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
private String domain;
private int gridGap;
private int spanCount=6;
private Consumer<Emoji> listener;
public CustomEmojiPopupKeyboard(Activity activity, List<EmojiCategory> emojis, String domain){
super(activity);
this.emojis=emojis;
this.domain=domain;
}
@Override
protected View onCreateView(){
GridLayoutManager lm=new GridLayoutManager(activity, spanCount);
list=new UsableRecyclerView(activity){
@Override
protected void onMeasure(int widthSpec, int heightSpec){
// it's important to do this in onMeasure so the child views will be measured with correct paddings already set
spanCount=Math.round(MeasureSpec.getSize(widthSpec)/(float)V.dp(44+20));
lm.setSpanCount(spanCount);
int pad=V.dp(16);
gridGap=(MeasureSpec.getSize(widthSpec)-pad*2-V.dp(44)*spanCount)/(spanCount-1);
setPadding(pad, 0, pad-gridGap, 0);
invalidateItemDecorations();
super.onMeasure(widthSpec, heightSpec);
}
};
lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position){
if(adapter.getItemViewType(position)==0)
return lm.getSpanCount();
return 1;
}
});
list.setLayoutManager(lm);
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
for(EmojiCategory category:emojis)
adapter.addAdapter(new SingleCategoryAdapter(category));
list.setAdapter(adapter);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.right=gridGap;
if(view instanceof TextView){ // section header
if(parent.getChildAdapterPosition(view)>0)
outRect.top=-gridGap; // negate the margin added by the emojis above
}else{
outRect.bottom=gridGap;
}
}
});
list.setBackgroundResource(R.color.gray_100);
list.setSelector(null);
return list;
}
public void setListener(Consumer<Emoji> listener){
this.listener=listener;
}
private class SingleCategoryAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
private final EmojiCategory category;
private final List<ImageLoaderRequest> requests;
public SingleCategoryAdapter(EmojiCategory category){
super(imgLoader);
this.category=category;
requests=category.emojis.stream().map(e->new UrlImageLoaderRequest(e.url, V.dp(44), V.dp(44))).collect(Collectors.toList());
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return viewType==0 ? new SectionHeaderViewHolder() : new EmojiViewHolder();
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position){
if(holder instanceof EmojiViewHolder){
((EmojiViewHolder) holder).bind(category.emojis.get(position-1));
((EmojiViewHolder) holder).positionWithinCategory=position-1;
}else if(holder instanceof SectionHeaderViewHolder){
((SectionHeaderViewHolder) holder).bind(TextUtils.isEmpty(category.title) ? domain : category.title);
}
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return category.emojis.size()+1;
}
@Override
public int getItemViewType(int position){
return position==0 ? 0 : 1;
}
@Override
public int getImageCountForItem(int position){
return position>0 ? 1 : 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return requests.get(position-1);
}
}
private class SectionHeaderViewHolder extends BindableViewHolder<String>{
public SectionHeaderViewHolder(){
super(activity, R.layout.item_emoji_section, list);
}
@Override
public void onBind(String item){
((TextView)itemView).setText(item);
}
}
private class EmojiViewHolder extends BindableViewHolder<Emoji> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
public int positionWithinCategory;
public EmojiViewHolder(){
super(new ImageView(activity));
ImageView img=(ImageView) itemView;
img.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(44)));
img.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
@Override
public void onBind(Emoji item){
}
@Override
public void setImage(int index, Drawable image){
((ImageView)itemView).setImageDrawable(image);
if(image instanceof Animatable)
((Animatable) image).start();
}
@Override
public void clearImage(int index){
((ImageView)itemView).setImageDrawable(null);
}
@Override
public void onClick(){
listener.accept(item);
}
}
}

View File

@@ -0,0 +1,170 @@
package org.joinmastodon.android.ui;
import android.app.Activity;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import me.grishka.appkit.utils.V;
/**
* Created by grishka on 17.08.15.
*/
public abstract class PopupKeyboard{
protected View keyboardPopupView;
protected Activity activity;
private int initialHeight;
private int prevWidth;
private int keyboardHeight;
private boolean needShowOnHide=false;
private boolean keyboardWasVisible=false;
private OnIconChangeListener iconListener;
public static final int ICON_HIDDEN=0;
public static final int ICON_ARROW=1;
public static final int ICON_KEYBOARD=2;
public PopupKeyboard(Activity activity){
this.activity=activity;
}
protected abstract View onCreateView();
private void ensureView(){
if(keyboardPopupView==null){
keyboardPopupView=onCreateView();
keyboardPopupView.setVisibility(View.GONE);
}
}
public View getView(){
ensureView();
return keyboardPopupView;
}
public boolean isVisible(){
ensureView();
return keyboardPopupView.getVisibility()==View.VISIBLE;
}
public void toggleKeyboardPopup(View textField){
ensureView();
if(keyboardPopupView.getVisibility()==View.VISIBLE){
if(keyboardWasVisible){
keyboardWasVisible=false;
InputMethodManager imm=(InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(textField, 0);
}else{
keyboardPopupView.setVisibility(View.GONE);
}
if(iconListener!=null)
iconListener.onIconChanged(ICON_HIDDEN);
return;
}
if(keyboardHeight>0){
needShowOnHide=true;
keyboardWasVisible=true;
InputMethodManager imm=(InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
if(iconListener!=null)
iconListener.onIconChanged(ICON_KEYBOARD);
}else{
doShowKeyboardPopup();
if(iconListener!=null)
iconListener.onIconChanged(ICON_ARROW);
}
}
protected Window getWindow(){
return activity.getWindow();
}
public void setOnIconChangedListener(OnIconChangeListener l){
iconListener=l;
}
public void onContentViewSizeChanged(int w, int h, int oldw, int oldh){
if(oldw==0 || w!=prevWidth){
initialHeight=h;
prevWidth=w;
onWidthChanged(w);
}
if(h>initialHeight){
initialHeight=h;
}
if(initialHeight!=0 && w==oldw){
keyboardHeight=initialHeight-h;
if(keyboardHeight!=0){
DisplayMetrics dm=activity.getResources().getDisplayMetrics();
activity.getSharedPreferences("emoji", Context.MODE_PRIVATE).edit().putInt("kb_size"+dm.widthPixels+"_"+dm.heightPixels, keyboardHeight).commit();
}
if(needShowOnHide && keyboardHeight==0){
((View)keyboardPopupView.getParent()).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
((View)keyboardPopupView.getParent()).getViewTreeObserver().removeOnPreDrawListener(this);
doShowKeyboardPopup();
return false;
}
});
needShowOnHide=false;
}
if(keyboardHeight>0 && keyboardPopupView.getVisibility()==View.VISIBLE){
if(iconListener!=null)
iconListener.onIconChanged(ICON_HIDDEN);
((View)keyboardPopupView.getParent()).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
((View)keyboardPopupView.getParent()).getViewTreeObserver().removeOnPreDrawListener(this);
keyboardPopupView.setVisibility(View.GONE);
return false;
}
});
}
}
}
public void hide(){
ensureView();
if(keyboardPopupView.getVisibility()==View.VISIBLE){
keyboardPopupView.setVisibility(View.GONE);
keyboardWasVisible=false;
if(iconListener!=null)
iconListener.onIconChanged(ICON_HIDDEN);
}
}
public void onConfigurationChanged(){
}
protected void onWidthChanged(int w){
}
protected boolean needWrapContent(){
return false;
}
private void doShowKeyboardPopup(){
ensureView();
DisplayMetrics dm=activity.getResources().getDisplayMetrics();
int height=activity.getSharedPreferences("emoji", Context.MODE_PRIVATE).getInt("kb_size"+dm.widthPixels+"_"+dm.heightPixels, V.dp(200));
if(needWrapContent()){
keyboardPopupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.AT_MOST | height);
height=keyboardPopupView.getMeasuredHeight();
}
keyboardPopupView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));
keyboardPopupView.setVisibility(View.VISIBLE);
}
public interface OnIconChangeListener{
public void onIconChanged(int icon);
}
}

View File

@@ -0,0 +1,47 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
public class SizeListenerLinearLayout extends LinearLayout{
private OnSizeChangedListener sizeListener;
public SizeListenerLinearLayout(Context context){
super(context);
}
public SizeListenerLinearLayout(Context context, @Nullable AttributeSet attrs){
super(context, attrs);
}
public SizeListenerLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
if(sizeListener!=null)
sizeListener.onSizeChanged(w, h, oldw, oldh);
}
public void setSizeListener(OnSizeChangedListener sizeListener){
this.sizeListener=sizeListener;
}
//
// @Override
// public View findFocus(){
// View v=super.findFocus();
// Log.w("11", "findFocus() "+v);
// return v;
// }
@FunctionalInterface
public interface OnSizeChangedListener{
void onSizeChanged(int w, int h, int oldw, int oldh);
}
}

View File

@@ -0,0 +1,41 @@
package org.joinmastodon.android.utils;
import java.util.regex.Pattern;
/**
* from https://github.com/Richienb/char-regex/blob/master/index.js
*/
public class CharRegex{
// Used to compose unicode character classes.
private static final String astralRange = "\\ud800-\\udfff";
private static final String comboMarksRange = "\\u0300-\\u036f";
private static final String comboHalfMarksRange = "\\ufe20-\\ufe2f";
private static final String comboSymbolsRange = "\\u20d0-\\u20ff";
private static final String comboMarksExtendedRange = "\\u1ab0-\\u1aff";
private static final String comboMarksSupplementRange = "\\u1dc0-\\u1dff";
private static final String comboRange = comboMarksRange + comboHalfMarksRange + comboSymbolsRange + comboMarksExtendedRange + comboMarksSupplementRange;
private static final String varRange = "\\ufe0e\\ufe0f";
// Used to compose unicode capture groups.
private static final String astral = "["+astralRange+"]";
private static final String combo = "["+comboRange+"]";
private static final String fitz = "\\ud83c[\\udffb-\\udfff]";
private static final String modifier = "(?:"+combo+"|"+fitz+")";
private static final String nonAstral = "[^"+astralRange+"]";
private static final String regional = "(?:\\ud83c[\\udde6-\\uddff]){2}";
private static final String surrogatePair = "[\\ud800-\\udbff][\\udc00-\\udfff]";
private static final String zeroWidthJoiner = "\\u200d";
private static final String blackFlag = "(?:\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40(?:\\udc65|\\udc73|\\udc77)\\udb40(?:\\udc6e|\\udc63|\\udc6c)\\udb40(?:\\udc67|\\udc74|\\udc73)\\udb40\\udc7f)";
// Used to compose unicode regexes.
private static final String optModifier = modifier+"?";
private static final String optVar = "["+varRange+"]?";
private static final String optJoin = "(?:"+zeroWidthJoiner+"(?:"+nonAstral+"|"+regional+"|"+surrogatePair+")"+optVar + optModifier+")*";
private static final String seq = optVar + optModifier + optJoin;
private static final String nonAstralCombo = nonAstral+combo+"?";
private static final String symbol = "(?:"+blackFlag+"|"+nonAstralCombo+"|"+combo+"|"+regional+"|"+surrogatePair+"|"+astral+")";
public static final Pattern REGEX=Pattern.compile(fitz+"(?="+fitz+")|"+symbol + seq);
// public static final Pattern REGEX=Pattern.compile("\\ud83c[\\udffb-\\udfff](?=\\ud83c[\\udffb-\\udfff])|(?:(?:\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40(?:\\udc65|\\udc73|\\udc77)\\udb40(?:\\udc6e|\\udc63|\\udc6c)\\udb40(?:\\udc67|\\udc74|\\udc73)\\udb40\\udc7f)|[^\\ud800-\\udfff][\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]?|[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff]|[\\ud800-\\udfff])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]|\\ud83c[\\udffb-\\udfff])?(?:\\u200d(?:[^\\ud800-\\udfff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]|\\ud83c[\\udffb-\\udfff])?)*");
}