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);
}
}