Add support for predictive back navigation

This commit is contained in:
Grishka 2024-06-09 21:17:01 +03:00
parent 767e414c94
commit 513b29f57d
17 changed files with 144 additions and 133 deletions

View File

@ -90,7 +90,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.17'
implementation 'me.grishka.appkit:appkit:1.3.0'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@ -33,7 +33,8 @@
android:supportsRtl="true"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:largeHeap="true">
android:largeHeap="true"
android:enableOnBackInvokedCallback="true">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"

View File

@ -102,13 +102,12 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{
public class ComposeFragment extends MastodonToolbarFragment implements ComposeEditText.SelectionListener, CustomTransitionsFragment{
private static final int MEDIA_RESULT=717;
public static final int IMAGE_DESCRIPTION_RESULT=363;
@ -173,6 +172,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private BackgroundColorSpan overLimitBG;
private ForegroundColorSpan overLimitFG;
private Runnable emojiKeyboardHider;
private Runnable sendingBackButtonBlocker;
private Runnable discardConfirmationCallback=this::confirmDiscardDraftAndFinish;
private boolean prevHadDraft;
public ComposeFragment(){
super(R.layout.toolbar_fragment_with_progressbar);
}
@ -249,6 +253,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
});
emojiKeyboardHider=emojiKeyboard::hide;
View view=inflater.inflate(R.layout.fragment_compose, container, false);
mainLayout=view.findViewById(R.id.compose_main_ll);
@ -305,6 +310,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onIconChanged(int icon){
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
updateNavigationBarColor(icon!=PopupKeyboard.ICON_HIDDEN);
if(icon!=PopupKeyboard.ICON_HIDDEN)
addBackCallback(emojiKeyboardHider);
else
removeBackCallback(emojiKeyboardHider);
if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
if(icon==PopupKeyboard.ICON_HIDDEN)
@ -480,6 +489,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
updateCharCounter();
updateDraftState();
}
});
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
@ -621,6 +631,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(publishButton==null)
return;
publishButton.setEnabled((trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
updateDraftState();
}
private void onCustomEmojiClick(Emoji emoji){
@ -696,6 +707,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
overlayParams.token=mainEditText.getWindowToken();
wm.addView(sendingOverlay, overlayParams);
addBackCallback(sendingBackButtonBlocker);
publishButton.setEnabled(false);
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
@ -737,6 +749,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onSuccess(Status result){
wm.removeView(sendingOverlay);
sendingOverlay=null;
removeBackCallback(sendingBackButtonBlocker);
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
@ -769,6 +782,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void handlePublishError(ErrorResponse error){
wm.removeView(sendingOverlay);
sendingOverlay=null;
removeBackCallback(sendingBackButtonBlocker);
V.setVisibilityAnimated(sendProgress, View.GONE);
publishButton.setEnabled(true);
if(error instanceof MastodonErrorResponse me){
@ -796,19 +810,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !mediaViewController.isEmpty() || pollFieldsHaveContent;
}
@Override
public boolean onBackPressed(){
if(emojiKeyboard.isVisible()){
emojiKeyboard.hide();
return true;
private void updateDraftState(){
boolean hasDraft=hasDraft();
if(hasDraft!=prevHadDraft){
prevHadDraft=hasDraft;
if(hasDraft){
addBackCallback(discardConfirmationCallback);
}else{
removeBackCallback(discardConfirmationCallback);
}
}
if(hasDraft()){
confirmDiscardDraftAndFinish();
return true;
}
if(sendingOverlay!=null)
return true;
return false;
}
@Override
@ -842,7 +853,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
.setPositiveButton(R.string.discard, (dialog, which)->{
removeBackCallback(discardConfirmationCallback);
Nav.finish(this);
})
.setNegativeButton(R.string.cancel, null)
.show();
}

View File

@ -32,12 +32,11 @@ import java.util.Collections;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
private static final String TAG="ComposeImageDescription";
private String accountID, attachmentID;
@ -138,9 +137,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
deliverResult();
return false;
}
@Override

View File

@ -42,13 +42,11 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
public class CreateListAddMembersFragment extends BaseAccountListFragment implements AddNewListMembersFragment.Listener{
private FollowList followList;
private Button nextButton;
private View buttonBar;
@ -59,6 +57,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
private WindowInsets lastInsets;
private boolean dismissingSearchFragment;
private HashSet<String> accountIDsInList=new HashSet<>();
private Runnable searchFragmentDismisser=this::dismissSearchFragment;
@Override
public void onCreate(Bundle savedInstanceState){
@ -156,6 +155,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
addBackCallback(searchFragmentDismisser);
return true;
}
@ -183,6 +183,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
removeBackCallback(searchFragmentDismisser);
dismissingSearchFragment=true;
rootView.setVisibility(View.VISIBLE);
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
@ -201,15 +202,6 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
@Override
public boolean isAccountInList(AccountViewModel account){
return accountIDsInList.contains(account.account.id);

View File

@ -30,8 +30,8 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ObjectIdComparator;
@ -48,13 +48,12 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
public class HomeFragment extends AppKitFragment{
private FragmentRootLinearLayout content;
private HomeTimelineFragment homeTimelineFragment;
private NotificationsListFragment notificationsFragment;
@ -272,15 +271,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
return false;
}
@Override
public boolean onBackPressed(){
if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed();
if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed();
return false;
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);

View File

@ -44,12 +44,11 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener{
private ImageButton fab;
private FollowList followList;
private boolean inSelectionMode;
@ -63,6 +62,8 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
private WindowInsets lastInsets;
private HashSet<String> accountIDsInList=new HashSet<>();
private boolean dismissingSearchFragment;
private Runnable searchFragmentDismisser=this::dismissSearchFragment;;
private Runnable actionModeDismisser=()->actionMode.finish();
public ListMembersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@ -214,6 +215,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
addBackCallback(searchFragmentDismisser);
}
private void onItemClick(AccountViewHolder holder){
@ -293,9 +295,11 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
selectedAccounts.clear();
updateItemsForSelectionModeTransition();
V.setVisibilityAnimated(fab, View.VISIBLE);
removeBackCallback(actionModeDismisser);
}
});
updateActionModeTitle();
addBackCallback(actionModeDismisser);
}
private void updateActionModeTitle(){
@ -371,15 +375,6 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
removeAccounts(Set.of(account.account.id), onDone);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
@ -393,6 +388,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
searchFragment=null;
dismissingSearchFragment=false;
}).start();
removeBackCallback(searchFragmentDismisser);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}

View File

@ -100,14 +100,13 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@ -158,6 +157,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private Animator tabBarColorAnim;
private MenuItem editSaveMenuItem;
private boolean savingEdits;
private Runnable editModeBackCallback=this::onEditModeBackCallback;
@Override
public void onCreate(Bundle savedInstanceState){
@ -983,12 +983,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
refreshLayout.setEnabled(false);
editDirty=false;
V.setVisibilityAnimated(fab, View.GONE);
addBackCallback(editModeBackCallback);
}
private void exitEditMode(){
if(!isInEditMode)
throw new IllegalStateException();
isInEditMode=false;
removeBackCallback(editModeBackCallback);
invalidateOptionsMenu();
actionButton.setText(R.string.edit_profile);
@ -1098,23 +1100,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
}
@Override
public boolean onBackPressed(){
if(isInEditMode){
if(savingEdits)
return true;
if(editDirty || aboutFragment.isEditDirty()){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
.setNegativeButton(R.string.cancel, null)
.show();
}else{
exitEditMode();
}
return true;
private void onEditModeBackCallback(){
if(savingEdits)
return;
if(editDirty || aboutFragment.isEditDirty()){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
.setNegativeButton(R.string.cancel, null)
.show();
}else{
exitEditMode();
}
return false;
}
private List<Attachment> createFakeAttachments(String url, Drawable drawable){

View File

@ -53,6 +53,8 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
@ -144,12 +146,16 @@ public class ProfileQrCodeFragment extends AppKitFragment{
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
dlg.setOnKeyListener((dialog, keyCode, event)->{
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
dismiss();
}
return true;
});
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
dlg.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this::dismiss);
}else{
dlg.setOnKeyListener((dialog, keyCode, event)->{
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
dismiss();
}
return true;
});
}
}
@Override

View File

@ -36,10 +36,9 @@ import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
private static final int QUERY_RESULT=937;
private static final int SCAN_RESULT=456;
@ -62,6 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private String accountID;
private String currentQuery;
private Intent scannerIntent;
private Runnable searchExitCallback=this::exitSearch;
@Override
public void onCreate(Bundle savedInstanceState){
@ -232,6 +232,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
addBackCallback(searchExitCallback);
}
}
@ -248,6 +249,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
tabsDivider.setVisibility(View.VISIBLE);
currentQuery=null;
removeBackCallback(searchExitCallback);
}
private Fragment getFragmentForPage(int page){
@ -260,15 +262,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
};
}
@Override
public boolean onBackPressed(){
if(searchActive){
exitSearch();
return true;
}
return false;
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==QUERY_RESULT && success){

View File

@ -57,14 +57,13 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment{
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
@ -371,6 +370,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
if(!enter){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
}
return set;
}
@ -437,14 +441,6 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;

View File

@ -58,12 +58,11 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
private View topBar;
private List<String> languages=Collections.emptyList();
@ -84,6 +83,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
private Runnable exitQueryModeCallback=()->setSearchQueryMode(false);
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@ -582,19 +583,13 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
@Override
public boolean onBackPressed(){
if(searchQueryMode){
setSearchQueryMode(false);
return true;
}
return false;
}
private void setSearchQueryMode(boolean enabled){
if(searchQueryMode==enabled)
return;
searchQueryMode=enabled;
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams();
if(searchQueryMode){
addBackCallback(exitQueryModeCallback);
filtersScroll.setVisibility(View.GONE);
lp.removeRule(RelativeLayout.END_OF);
backBtn.setScaleX(0.83333333f);
@ -602,6 +597,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
backBtn.setTranslationX(V.dp(8));
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0));
}else{
removeBackCallback(exitQueryModeCallback);
filtersScroll.setVisibility(View.VISIBLE);
focusThing.requestFocus();
searchEdit.setText("");

View File

@ -24,6 +24,7 @@ import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
@ -44,11 +45,10 @@ import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
public class EditFilterFragment extends BaseSettingsFragment<Void>{
private static final int WORDS_RESULT=370;
private static final int CONTEXT_RESULT=651;
@ -63,6 +63,13 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
private ArrayList<String> deletedWordIDs=new ArrayList<>();
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
private boolean dirty;
private boolean wasDirty;
private Runnable confirmCallback=()->{
if(isDirty()){
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
}
};
@Override
public void onCreate(Bundle savedInstanceState){
@ -101,6 +108,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
titleEditLayout.updateHint();
if(filter!=null)
titleEdit.setText(filter.title);
titleEdit.addTextChangedListener(new SimpleTextWatcher(e->updateBackCallback()));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
@ -158,6 +166,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
a.dismiss();
}
updateBackCallback();
})
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
@ -309,6 +318,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
}
updateBackCallback();
}
}
@ -317,11 +327,19 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
}
@Override
public boolean onBackPressed(){
if(isDirty()){
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
return true;
protected void toggleCheckableItem(ListItem<?> item){
super.toggleCheckableItem(item);
updateBackCallback();
}
private void updateBackCallback(){
boolean dirty=isDirty();
if(dirty!=wasDirty){
wasDirty=dirty;
if(dirty)
addBackCallback(confirmCallback);
else
removeBackCallback(confirmCallback);
}
return false;
}
}

View File

@ -11,9 +11,7 @@ import java.util.Arrays;
import java.util.EnumSet;
import java.util.stream.Collectors;
import me.grishka.appkit.fragments.OnBackPressedListener;
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
public class FilterContextFragment extends BaseSettingsFragment<FilterContext>{
private EnumSet<FilterContext> context;
@Override
@ -33,7 +31,8 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
protected void doLoadData(int offset, int count){}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
context=EnumSet.noneOf(FilterContext.class);
for(ListItem<FilterContext> item:data){
if(((CheckableListItem<FilterContext>) item).checked)
@ -42,6 +41,5 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
Bundle args=new Bundle();
args.putSerializable("context", context);
setResult(true, args);
return false;
}
}

View File

@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
@ -11,11 +10,9 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.FilterKeyword;
@ -33,15 +30,15 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword>{
private Button fab;
private ActionMode actionMode;
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
private ArrayList<String> deletedItemIDs=new ArrayList<>();
private MenuItem deleteItem;
private Runnable actionModeDismisser=()->actionMode.finish();
public FilterWordsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
@ -80,12 +77,12 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
}
@Override
public boolean onBackPressed(){
public void onStop(){
super.onStop();
Bundle result=new Bundle();
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
result.putStringArrayList("deleted", deletedItemIDs);
setResult(true, result);
return false;
}
@Override
@ -259,6 +256,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
updateActionModeTitle();
addBackCallback(actionModeDismisser);
}
private void leaveSelectionMode(boolean fromActionMode){
@ -280,6 +278,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
data.set(i, newItem);
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
removeBackCallback(actionModeDismisser);
}
private void updateActionModeTitle(){

View File

@ -54,6 +54,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import android.window.OnBackInvokedDispatcher;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
@ -169,7 +170,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
windowView=new FrameLayout(activity){
@Override
public boolean dispatchKeyEvent(KeyEvent event){
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
onStartSwipeToDismissTransition(0f);
}
@ -257,6 +258,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
wm.addView(windowView, wlp);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
// TODO make use of the progress callback for nicer animation
windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->onStartSwipeToDismissTransition(0));
}
windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override

View File

@ -10,6 +10,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Build;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
@ -19,6 +20,7 @@ import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toolbar;
import android.window.OnBackInvokedDispatcher;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.OutlineProviders;
@ -83,6 +85,15 @@ public class ToolbarDropdownMenuController{
.withLayer()
.start();
controllerStack.add(initialSubmenu);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->{
if(controllerStack.size()>1)
popSubmenuController();
else
dismiss();
});
}
}
public void dismiss(){
@ -243,7 +254,7 @@ public class ToolbarDropdownMenuController{
@Override
public boolean dispatchKeyEvent(KeyEvent event){
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
if(controllerStack.size()>1)
popSubmenuController();