Compose autocomplete improvements

This commit is contained in:
Grishka
2023-05-12 03:39:46 +03:00
parent 968a6ea9b3
commit 89501271ce
13 changed files with 429 additions and 66 deletions

View File

@@ -28,6 +28,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
@@ -52,6 +53,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
@@ -96,6 +98,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final int MEDIA_RESULT=717;
public static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int AUTOCOMPLETE_ACCOUNT_RESULT=779;
private static final String TAG="ComposeFragment";
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
@@ -262,6 +265,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onIconChanged(int icon){
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
if(icon==PopupKeyboard.ICON_HIDDEN)
showAutocomplete();
else
hideAutocomplete();
}
}
});
@@ -294,7 +304,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateVisibilityIcon();
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
@Override
public void onCompletionSelected(String completion){
onAutocompleteOptionSelected(completion);
}
@Override
public void onSetEmojiPanelOpen(boolean open){
if(open!=emojiKeyboard.isVisible())
emojiKeyboard.toggleKeyboardPopup(mainEditText);
}
@Override
public void onLaunchAccountSearch(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
}
});
View autocompleteView=autocompleteViewController.getView();
autocompleteView.setVisibility(View.INVISIBLE);
bottomBar.addView(autocompleteView, 0, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(56)));
@@ -315,6 +343,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mediaViewController.onSaveInstanceState(outState);
outState.putBoolean("hasSpoiler", hasSpoiler);
outState.putSerializable("visibility", statusVisibility);
if(currentAutocompleteSpan!=null){
Editable e=mainEditText.getText();
outState.putInt("autocompleteStart", e.getSpanStart(currentAutocompleteSpan));
outState.putInt("autocompleteEnd", e.getSpanEnd(currentAutocompleteSpan));
}
}
@Override
@@ -471,6 +504,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateMediaPollStates();
}
@Override
public void onViewStateRestored(Bundle savedInstanceState){
super.onViewStateRestored(savedInstanceState);
if(savedInstanceState!=null && savedInstanceState.containsKey("autocompleteStart")){
int start=savedInstanceState.getInt("autocompleteStart"), end=savedInstanceState.getInt("autocompleteEnd");
currentAutocompleteSpan=new ComposeAutocompleteSpan();
mainEditText.getText().setSpan(currentAutocompleteSpan, start, end, Editable.SPAN_EXCLUSIVE_INCLUSIVE);
startAutocomplete(currentAutocompleteSpan);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu);
@@ -537,6 +581,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void onCustomEmojiClick(Emoji emoji){
if(getActivity().getCurrentFocus() instanceof EditText edit){
if(edit==mainEditText && currentAutocompleteSpan!=null && autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
Editable text=mainEditText.getText();
int start=text.getSpanStart(currentAutocompleteSpan);
int end=text.getSpanEnd(currentAutocompleteSpan);
finishAutocomplete();
text.replace(start, end, ':'+emoji.shortcode+':');
return;
}
int start=edit.getSelectionStart();
String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":";
edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':');
@@ -549,6 +601,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, 0.11f);
getToolbar().setBackgroundColor(color);
setStatusBarColor(color);
setNavigationBarColor(color);
bottomBar.setBackgroundColor(color);
}
@@ -694,6 +747,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
String attID=result.getString("attachment");
String text=result.getString("text");
mediaViewController.setAltTextByID(attID, text);
}else if(reqCode==AUTOCOMPLETE_ACCOUNT_RESULT && success){
Account acc=Parcels.unwrap(result.getParcelable("selectedAccount"));
if(currentAutocompleteSpan==null)
return;
Editable e=mainEditText.getText();
int start=e.getSpanStart(currentAutocompleteSpan);
int end=e.getSpanEnd(currentAutocompleteSpan);
e.removeSpan(currentAutocompleteSpan);
e.replace(start, end, '@'+acc.acct+' ');
finishAutocomplete();
}
}
@@ -905,6 +968,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Editable e=mainEditText.getText();
String spanText=e.toString().substring(e.getSpanStart(span), e.getSpanEnd(span));
autocompleteViewController.setText(spanText);
showAutocomplete();
}
private void finishAutocomplete(){
if(currentAutocompleteSpan==null)
return;
autocompleteViewController.setText(null);
currentAutocompleteSpan=null;
hideAutocomplete();
}
private void showAutocomplete(){
UiUtils.beginLayoutTransition(bottomBar);
View autocompleteView=autocompleteViewController.getView();
bottomBar.getLayoutParams().height=ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -913,11 +988,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
autocompleteDivider.setVisibility(View.VISIBLE);
}
private void finishAutocomplete(){
if(currentAutocompleteSpan==null)
return;
autocompleteViewController.setText(null);
currentAutocompleteSpan=null;
private void hideAutocomplete(){
UiUtils.beginLayoutTransition(bottomBar);
bottomBar.getLayoutParams().height=V.dp(48);
bottomBar.requestLayout();
@@ -930,8 +1001,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
int start=e.getSpanStart(currentAutocompleteSpan);
int end=e.getSpanEnd(currentAutocompleteSpan);
e.replace(start, end, text+" ");
mainEditText.setSelection(start+text.length()+1);
finishAutocomplete();
InputConnection conn=mainEditText.getCurrentInputConnection();
if(conn!=null)
conn.finishComposingText();
}
@Override

View File

@@ -136,6 +136,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, V.dp(16), 0, V.dp(16));
@@ -143,6 +145,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
super.onApplyWindowInsets(insets);
}
protected void onConfigureViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@@ -151,7 +155,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
onConfigureViewHolder(holder);
return holder;
}
@Override

View File

@@ -0,0 +1,135 @@
package org.joinmastodon.android.fragments.account_list;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private LinearLayout searchLayout;
private EditText searchEdit;
private ImageButton clearSearchButton;
private String currentQuery;
private Runnable debouncer=()->{
currentQuery=searchEdit.getText().toString();
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(!TextUtils.isEmpty(currentQuery))
loadData();
};
private boolean resultDelivered;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRefreshEnabled(false);
setEmptyText("");
dataLoaded();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchLayout=new LinearLayout(view.getContext());
searchLayout.setOrientation(LinearLayout.HORIZONTAL);
searchEdit=new EditText(view.getContext());
searchEdit.setHint(R.string.search_hint);
searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
searchEdit.setBackground(null);
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
searchEdit.removeCallbacks(debouncer);
searchEdit.postDelayed(debouncer, 300);
}));
searchEdit.setImeActionLabel(null, EditorInfo.IME_ACTION_SEARCH);
searchEdit.setOnEditorActionListener((v, actionId, event)->{
searchEdit.removeCallbacks(debouncer);
debouncer.run();
return true;
});
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
clearSearchButton=new ImageButton(view.getContext());
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24);
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant)));
clearSearchButton.setBackground(UiUtils.getThemeDrawable(getToolbarContext(), android.R.attr.actionBarItemBackground));
clearSearchButton.setOnClickListener(v->searchEdit.setText(""));
searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT));
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
setStatusBarColor(color);
setNavigationBarColor(color);
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.accounts.stream().map(AccountViewModel::new).collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(searchLayout.getParent()!=null)
((ViewGroup) searchLayout.getParent()).removeView(searchLayout);
getToolbar().addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
getToolbar().setBackgroundResource(R.drawable.bg_m3_surface3);
searchEdit.requestFocus();
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setOnClickListener(this::onItemClick);
}
private void onItemClick(AccountViewHolder holder){
if(resultDelivered)
return;
resultDelivered=true;
Bundle res=new Bundle();
res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account));
setResult(true, res);
Nav.finish(this, false);
}
}

View File

@@ -733,4 +733,11 @@ public class UiUtils{
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
}
public static Drawable getThemeDrawable(Context context, @AttrRes int attr){
TypedArray ta=context.obtainStyledAttributes(new int[]{attr});
Drawable d=ta.getDrawable(0);
ta.recycle();
return d;
}
}

View File

@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.viewcontrollers;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -23,11 +23,13 @@ import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -44,16 +46,18 @@ 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 ComposeAutocompleteViewController{
private static final int LOADING_FAKE_USER_COUNT=3;
private Activity activity;
private String accountID;
private FrameLayout contentView;
private UsableRecyclerView list;
private ListImageLoaderWrapper imgLoader;
private ProgressBar progress;
private List<WrappedAccount> users=Collections.emptyList();
private List<Hashtag> hashtags=Collections.emptyList();
private List<WrappedEmoji> emojis=Collections.emptyList();
@@ -61,13 +65,17 @@ public class ComposeAutocompleteViewController{
private APIRequest currentRequest;
private Runnable usersDebouncer=this::doSearchUsers, hashtagsDebouncer=this::doSearchHashtags;
private String lastText;
private boolean listIsHidden=true;
private boolean isLoading;
private FilterChipView emptyButton;
private HideableSingleViewRecyclerAdapter emptyButtonAdapter;
private UsersAdapter usersAdapter;
private HashtagsAdapter hashtagsAdapter;
private EmojisAdapter emojisAdapter;
private MergeRecyclerAdapter usersMergeAdapter;
private MergeRecyclerAdapter emojisMergeAdapter;
private Consumer<String> completionSelectedListener;
private AutocompleteListener completionSelectedListener;
public ComposeAutocompleteViewController(Activity activity, String accountID){
this.activity=activity;
@@ -77,7 +85,6 @@ public class ComposeAutocompleteViewController{
list=new UsableRecyclerView(activity);
list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
list.setItemAnimator(new BetterItemAnimator());
list.setVisibility(View.GONE);
list.setPadding(V.dp(16), V.dp(12), V.dp(16), V.dp(12));
list.setClipToPadding(false);
list.setSelector(null);
@@ -90,10 +97,15 @@ public class ComposeAutocompleteViewController{
});
contentView.addView(list, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
progress=new ProgressBar(activity);
FrameLayout.LayoutParams progressLP=new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.CENTER_HORIZONTAL|Gravity.TOP);
progressLP.topMargin=V.dp(16);
contentView.addView(progress, progressLP);
emptyButton=new FilterChipView(activity);
emptyButtonAdapter=new HideableSingleViewRecyclerAdapter(emptyButton);
emptyButton.setOnClickListener(v->{
if(mode==Mode.EMOJIS){
completionSelectedListener.onSetEmojiPanelOpen(true);
}else if(mode==Mode.USERS){
completionSelectedListener.onLaunchAccountSearch();
}
});
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
}
@@ -104,13 +116,15 @@ public class ComposeAutocompleteViewController{
}else if(mode==Mode.HASHTAGS){
list.removeCallbacks(hashtagsDebouncer);
}
if(text==null)
return;
Mode prevMode=mode;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(text==null){
reset();
return;
}
Mode prevMode=mode;
mode=switch(text.charAt(0)){
case '@' -> Mode.USERS;
case '#' -> Mode.HASHTAGS;
@@ -118,16 +132,33 @@ public class ComposeAutocompleteViewController{
default -> throw new IllegalStateException("Unexpected value: "+text.charAt(0));
};
if(prevMode!=mode){
if(mode==Mode.USERS){
isLoading=true;
emptyButtonAdapter.setVisible(false);
}
list.setAdapter(switch(mode){
case USERS -> {
if(usersAdapter==null)
if(usersAdapter==null){
usersAdapter=new UsersAdapter();
yield usersAdapter;
usersMergeAdapter=new MergeRecyclerAdapter();
usersMergeAdapter.addAdapter(emptyButtonAdapter);
usersMergeAdapter.addAdapter(usersAdapter);
}
emptyButton.setText(R.string.compose_autocomplete_users_empty);
emptyButton.setDrawableStartTinted(R.drawable.ic_search_20px);
yield usersMergeAdapter;
}
case EMOJIS -> {
if(emojisAdapter==null)
if(emojisAdapter==null){
emojisAdapter=new EmojisAdapter();
yield emojisAdapter;
emojisMergeAdapter=new MergeRecyclerAdapter();
emojisMergeAdapter.addAdapter(emptyButtonAdapter);
emojisMergeAdapter.addAdapter(emojisAdapter);
}
emptyButton.setText(R.string.compose_autocomplete_emoji_empty);
emptyButton.setDrawableStartTinted(R.drawable.ic_mood_20px);
yield emojisMergeAdapter;
}
case HASHTAGS -> {
if(hashtagsAdapter==null)
@@ -135,20 +166,18 @@ public class ComposeAutocompleteViewController{
yield hashtagsAdapter;
}
});
if(mode!=Mode.EMOJIS){
list.setVisibility(View.GONE);
progress.setVisibility(View.VISIBLE);
listIsHidden=true;
}else if(listIsHidden){
list.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
listIsHidden=false;
}
}
lastText=text;
if(mode==Mode.USERS){
list.postDelayed(usersDebouncer, 300);
}else if(mode==Mode.HASHTAGS){
List<Hashtag> oldList=hashtags;
hashtags=new ArrayList<>();
Hashtag tag=new Hashtag();
tag.name=lastText.substring(1);
hashtags.add(tag);
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
list.postDelayed(hashtagsDebouncer, 300);
}else if(mode==Mode.EMOJIS){
String _text=text.substring(1); // remove ':'
@@ -165,12 +194,14 @@ public class ComposeAutocompleteViewController{
.filter(e -> e.shortcode.toLowerCase().contains(_text.toLowerCase())))
.map(WrappedEmoji::new)
.collect(Collectors.toList());
emptyButtonAdapter.setVisible(emojis.isEmpty());
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
list.invalidateItemDecorations();
imgLoader.updateImages();
}
}
public void setCompletionSelectedListener(Consumer<String> completionSelectedListener){
public void setCompletionSelectedListener(AutocompleteListener completionSelectedListener){
this.completionSelectedListener=completionSelectedListener;
}
@@ -178,6 +209,17 @@ public class ComposeAutocompleteViewController{
return contentView;
}
public void reset(){
mode=null;
users.clear();
emojis.clear();
hashtags.clear();
}
public Mode getMode(){
return mode;
}
private void doSearchUsers(){
currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.ACCOUNTS, false)
.setCallback(new Callback<>(){
@@ -186,13 +228,22 @@ public class ComposeAutocompleteViewController{
currentRequest=null;
List<WrappedAccount> oldList=users;
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
imgLoader.updateImages();
if(listIsHidden){
listIsHidden=false;
V.setVisibilityAnimated(list, View.VISIBLE);
V.setVisibilityAnimated(progress, View.GONE);
if(isLoading){
isLoading=false;
if(users.size()>=LOADING_FAKE_USER_COUNT){
usersAdapter.notifyItemRangeChanged(0, LOADING_FAKE_USER_COUNT);
if(users.size()>LOADING_FAKE_USER_COUNT)
usersAdapter.notifyItemRangeInserted(LOADING_FAKE_USER_COUNT, users.size()-LOADING_FAKE_USER_COUNT);
}else{
usersAdapter.notifyItemRangeChanged(0, users.size());
usersAdapter.notifyItemRangeRemoved(users.size(), LOADING_FAKE_USER_COUNT-users.size());
}
}else{
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
}
list.invalidateItemDecorations();
emptyButtonAdapter.setVisible(users.isEmpty());
imgLoader.updateImages();
}
@Override
@@ -209,15 +260,12 @@ public class ComposeAutocompleteViewController{
@Override
public void onSuccess(SearchResults result){
currentRequest=null;
if(result.hashtags.isEmpty() || (result.hashtags.size()==1 && result.hashtags.get(0).name.equals(lastText.substring(1))))
return;
List<Hashtag> oldList=hashtags;
hashtags=result.hashtags;
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
imgLoader.updateImages();
if(listIsHidden){
listIsHidden=false;
V.setVisibilityAnimated(list, View.VISIBLE);
V.setVisibilityAnimated(progress, View.GONE);
}
list.invalidateItemDecorations();
}
@Override
@@ -236,23 +284,31 @@ public class ComposeAutocompleteViewController{
@NonNull
@Override
public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new UserViewHolder();
return switch(viewType){
case 0 -> new UserViewHolder();
case 1 -> new LoadingUserViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@Override
public int getItemCount(){
if(isLoading)
return LOADING_FAKE_USER_COUNT;
return users.size();
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position){
holder.bind(users.get(position));
super.onBindViewHolder(holder, position);
if(!isLoading){
holder.bind(users.get(position));
super.onBindViewHolder(holder, position);
}
}
@Override
public int getImageCountForItem(int position){
return 1/*+users.get(position).emojiHelper.getImageCount()*/;
return isLoading ? 0 : 1;
}
@Override
@@ -262,13 +318,18 @@ public class ComposeAutocompleteViewController{
return a.avaRequest;
return a.emojiHelper.getImageRequest(image-1);
}
@Override
public int getItemViewType(int position){
return isLoading ? 1 : 0;
}
}
private class UserViewHolder extends BindableViewHolder<WrappedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final ImageView ava;
private final TextView username;
protected final ImageView ava;
protected final TextView username;
private UserViewHolder(){
public UserViewHolder(){
super(activity, R.layout.item_autocomplete_user, list);
ava=findViewById(R.id.photo);
username=findViewById(R.id.username);
@@ -283,7 +344,7 @@ public class ComposeAutocompleteViewController{
@Override
public void onClick(){
completionSelectedListener.accept("@"+item.account.acct);
completionSelectedListener.onCompletionSelected("@"+item.account.acct);
}
@Override
@@ -297,7 +358,24 @@ public class ComposeAutocompleteViewController{
@Override
public void clearImage(int index){
setImage(index, null);
if(index==0)
ava.setImageResource(R.drawable.image_placeholder);
else
setImage(index, null);
}
}
private class LoadingUserViewHolder extends UserViewHolder implements UsableRecyclerView.DisableableClickable{
public LoadingUserViewHolder(){
int color=UiUtils.getThemeColor(activity, R.attr.colorM3OutlineVariant);
ava.setImageDrawable(new ColorDrawable(color));
username.setLayoutParams(new LinearLayout.LayoutParams(V.dp(64), V.dp(10)));
username.setBackgroundColor(color);
}
@Override
public boolean isEnabled(){
return false;
}
}
@@ -336,7 +414,7 @@ public class ComposeAutocompleteViewController{
@Override
public void onClick(){
completionSelectedListener.accept("#"+item.name);
completionSelectedListener.onCompletionSelected("#"+item.name);
}
}
@@ -401,7 +479,7 @@ public class ComposeAutocompleteViewController{
@Override
public void onClick(){
completionSelectedListener.accept(":"+item.emoji.shortcode+":");
completionSelectedListener.onCompletionSelected(":"+item.emoji.shortcode+":");
}
}
@@ -430,9 +508,15 @@ public class ComposeAutocompleteViewController{
}
}
private enum Mode{
public enum Mode{
USERS,
HASHTAGS,
EMOJIS
}
public interface AutocompleteListener{
void onCompletionSelected(String completion);
void onSetEmojiPanelOpen(boolean open);
void onLaunchAccountSearch();
}
}

View File

@@ -36,6 +36,7 @@ import org.parceler.Parcels;
import java.util.HashMap;
import java.util.Objects;
import java.util.function.Consumer;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -56,6 +57,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
private final Fragment fragment;
private final HashMap<String, Relationship> relationships;
private Consumer<AccountViewHolder> onClick;
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment.getActivity(), R.layout.item_account_list, list);
this.fragment=fragment;
@@ -140,6 +143,10 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
@Override
public void onClick(){
if(onClick!=null){
onClick.accept(this);
return;
}
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
@@ -253,4 +260,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
relationships.put(item.account.id, r);
bindRelationship();
}
public void setOnClickListener(Consumer<AccountViewHolder> listener){
onClick=listener;
}
}

View File

@@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi;
public class ComposeEditText extends EditText{
private SelectionListener selectionListener;
private InputConnection currentInputConnection;
public ComposeEditText(Context context){
super(context);
@@ -49,15 +50,19 @@ public class ComposeEditText extends EditText{
this.selectionListener=selectionListener;
}
public InputConnection getCurrentInputConnection(){
return currentInputConnection;
}
// Support receiving images from keyboards
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs){
final InputConnection ic=super.onCreateInputConnection(outAttrs);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1){
outAttrs.contentMimeTypes=selectionListener.onGetAllowedMediaMimeTypes();
return new MediaAcceptingInputConnection(ic);
return currentInputConnection=new MediaAcceptingInputConnection(ic);
}
return ic;
return currentInputConnection=ic;
}
// Support pasting images

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Button;
@@ -30,7 +29,6 @@ public class FilterChipView extends Button{
setBackgroundResource(R.drawable.bg_filter_chip);
setTextAppearance(R.style.m3_label_large);
setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface)));
updatePadding();
}
@@ -40,7 +38,9 @@ public class FilterChipView extends Button{
if(currentlySelected==isSelected())
return;
currentlySelected=isSelected();
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()) : null;
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()).mutate() : null;
if(start!=null)
start.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface));
Drawable end=getCompoundDrawablesRelative()[2];
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
updatePadding();
@@ -53,7 +53,18 @@ public class FilterChipView extends Button{
}
public void setDrawableEnd(@DrawableRes int drawable){
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, drawable, 0);
Drawable icon=getResources().getDrawable(drawable, getContext().getTheme()).mutate();
icon.setBounds(0, 0, V.dp(18), V.dp(18));
icon.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface));
setCompoundDrawablesRelativeWithIntrinsicBounds(getCompoundDrawablesRelative()[0], null, icon, null);
updatePadding();
}
public void setDrawableStartTinted(@DrawableRes int drawable){
Drawable icon=getResources().getDrawable(drawable, getContext().getTheme()).mutate();
icon.setBounds(0, 0, V.dp(18), V.dp(18));
icon.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
setCompoundDrawablesRelative(icon, null, getCompoundDrawablesRelative()[2], null);
updatePadding();
}
}