From 47efd3317d2cc6e317121a9ca10fb25808f85981 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 11 Aug 2021 14:49:20 +0200 Subject: [PATCH] Android: Fix CheatsActivity d-pad navigation Special shoutout to Android for not having RTL compatible variants of nextFocusRight and nextFocusLeft. Ideally we would have some way to block the user from using the d-pad to switch between the two panes when in portrait mode, or make the list pane act as if it's to the left of the details pane rather than the right when the details pane is open, but I don't know of a good way to do this. SlidingPaneLayout doesn't really seem to have been implemented with d-pad navigation in mind. Thankfully, landscape is the most important use case for gamepads. --- .../cheats/ui/CheatDetailsFragment.java | 25 ++++-- .../features/cheats/ui/CheatListFragment.java | 2 +- .../cheats/ui/CheatWarningFragment.java | 4 + .../features/cheats/ui/CheatsActivity.java | 76 +++++++++++++++++++ .../features/cheats/ui/CheatsAdapter.java | 19 ++++- .../main/res/layout-ldrtl/list_item_cheat.xml | 37 +++++++++ .../src/main/res/layout/list_item_cheat.xml | 6 +- 7 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 Source/Android/app/src/main/res/layout-ldrtl/list_item_cheat.xml diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java index 421b0aa094..2ab6bb31b0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatDetailsFragment.java @@ -73,13 +73,12 @@ public class CheatDetailsFragment extends Fragment mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated); mButtonDelete.setOnClickListener(this::onDeleteClicked); - mButtonEdit.setOnClickListener((v) -> mViewModel.setIsEditing(true)); - mButtonCancel.setOnClickListener((v) -> - { - mViewModel.setIsEditing(false); - onSelectedCheatUpdated(mCheat); - }); + mButtonEdit.setOnClickListener(this::onEditClicked); + mButtonCancel.setOnClickListener(this::onCancelClicked); mButtonOk.setOnClickListener(this::onOkClicked); + + CheatsActivity.setOnFocusChangeListenerRecursively(view, + (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus)); } private void clearEditErrors() @@ -98,6 +97,19 @@ public class CheatDetailsFragment extends Fragment builder.show(); } + private void onEditClicked(View view) + { + mViewModel.setIsEditing(true); + mButtonOk.requestFocus(); + } + + private void onCancelClicked(View view) + { + mViewModel.setIsEditing(false); + onSelectedCheatUpdated(mCheat); + mButtonDelete.requestFocus(); + } + private void onOkClicked(View view) { clearEditErrors(); @@ -118,6 +130,7 @@ public class CheatDetailsFragment extends Fragment mViewModel.notifySelectedCheatChanged(); mViewModel.setIsEditing(false); } + mButtonEdit.requestFocus(); break; case Cheat.TRY_SET_FAIL_NO_NAME: mEditName.setError(getString(R.string.cheats_error_no_name)); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java index 03f87f0c0c..efc446d50e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatListFragment.java @@ -36,7 +36,7 @@ public class CheatListFragment extends Fragment CheatsActivity activity = (CheatsActivity) requireActivity(); CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class); - recyclerView.setAdapter(new CheatsAdapter(getViewLifecycleOwner(), viewModel)); + recyclerView.setAdapter(new CheatsAdapter(activity, viewModel)); recyclerView.setLayoutManager(new LinearLayoutManager(activity)); recyclerView.addItemDecoration(new DividerItemDecoration(activity, null)); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java index aba0bb1602..df6d9d94b0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatWarningFragment.java @@ -37,6 +37,10 @@ public class CheatWarningFragment extends Fragment implements View.OnClickListen Button settingsButton = view.findViewById(R.id.button_settings); settingsButton.setOnClickListener(this); + + CheatsActivity activity = (CheatsActivity) requireActivity(); + CheatsActivity.setOnFocusChangeListenerRecursively(view, + (v, hasFocus) -> activity.onListViewFocusChange(hasFocus)); } @Override diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java index 4143457006..42300e2b8d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsActivity.java @@ -5,8 +5,12 @@ package org.dolphinemu.dolphinemu.features.cheats.ui; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.ViewCompat; import androidx.lifecycle.ViewModelProvider; import androidx.slidingpanelayout.widget.SlidingPaneLayout; @@ -18,6 +22,7 @@ import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback; import org.dolphinemu.dolphinemu.ui.main.MainPresenter; public class CheatsActivity extends AppCompatActivity + implements SlidingPaneLayout.PanelSlideListener { private static final String ARG_GAME_ID = "game_id"; private static final String ARG_REVISION = "revision"; @@ -29,6 +34,11 @@ public class CheatsActivity extends AppCompatActivity private CheatsViewModel mViewModel; private SlidingPaneLayout mSlidingPaneLayout; + private View mCheatList; + private View mCheatDetails; + + private View mCheatListLastFocus; + private View mCheatDetailsLastFocus; public static void launch(Context context, String gameId, int revision, boolean isWii) { @@ -59,6 +69,13 @@ public class CheatsActivity extends AppCompatActivity setContentView(R.layout.activity_cheats); mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout); + mCheatList = findViewById(R.id.cheat_list); + mCheatDetails = findViewById(R.id.cheat_details); + + mCheatListLastFocus = mCheatList; + mCheatDetailsLastFocus = mCheatDetails; + + mSlidingPaneLayout.addPanelSlideListener(this); getOnBackPressedDispatcher().addCallback(this, new TwoPaneOnBackPressedCallback(mSlidingPaneLayout)); @@ -77,6 +94,25 @@ public class CheatsActivity extends AppCompatActivity mViewModel.saveIfNeeded(mGameId, mRevision); } + @Override + public void onPanelSlide(@NonNull View panel, float slideOffset) + { + } + + @Override + public void onPanelOpened(@NonNull View panel) + { + boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL; + mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT); + } + + @Override + public void onPanelClosed(@NonNull View panel) + { + boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL; + mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT); + } + private void onSelectedCheatChanged(Cheat selectedCheat) { boolean cheatSelected = selectedCheat != null; @@ -88,6 +124,30 @@ public class CheatsActivity extends AppCompatActivity SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED); } + public void onListViewFocusChange(boolean hasFocus) + { + if (hasFocus) + { + mCheatListLastFocus = mCheatList.findFocus(); + if (mCheatListLastFocus == null) + throw new NullPointerException(); + + mSlidingPaneLayout.close(); + } + } + + public void onDetailsViewFocusChange(boolean hasFocus) + { + if (hasFocus) + { + mCheatDetailsLastFocus = mCheatDetails.findFocus(); + if (mCheatDetailsLastFocus == null) + throw new NullPointerException(); + + mSlidingPaneLayout.open(); + } + } + private void openDetailsView(boolean open) { if (open) @@ -100,4 +160,20 @@ public class CheatsActivity extends AppCompatActivity settings.loadSettings(null, mGameId, mRevision, mIsWii); return settings; } + + public static void setOnFocusChangeListenerRecursively(@NonNull View view, + View.OnFocusChangeListener listener) + { + view.setOnFocusChangeListener(listener); + + if (view instanceof ViewGroup) + { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) + { + View child = viewGroup.getChildAt(i); + setOnFocusChangeListenerRecursively(child, listener); + } + } + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java index c12cd58c9f..ed230f0513 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.java @@ -20,25 +20,27 @@ import java.util.ArrayList; public class CheatsAdapter extends RecyclerView.Adapter { + private final CheatsActivity mActivity; private final CheatsViewModel mViewModel; - public CheatsAdapter(LifecycleOwner owner, CheatsViewModel viewModel) + public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) { + mActivity = activity; mViewModel = viewModel; - mViewModel.getCheatAddedEvent().observe(owner, (position) -> + mViewModel.getCheatAddedEvent().observe(activity, (position) -> { if (position != null) notifyItemInserted(position); }); - mViewModel.getCheatChangedEvent().observe(owner, (position) -> + mViewModel.getCheatChangedEvent().observe(activity, (position) -> { if (position != null) notifyItemChanged(position); }); - mViewModel.getCheatDeletedEvent().observe(owner, (position) -> + mViewModel.getCheatDeletedEvent().observe(activity, (position) -> { if (position != null) notifyItemRemoved(position); @@ -55,12 +57,15 @@ public class CheatsAdapter extends RecyclerView.Adapter { case CheatItem.TYPE_CHEAT: View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false); + addViewListeners(cheatView); return new CheatViewHolder(cheatView); case CheatItem.TYPE_HEADER: View headerView = inflater.inflate(R.layout.list_item_header, parent, false); + addViewListeners(headerView); return new HeaderViewHolder(headerView); case CheatItem.TYPE_ACTION: View actionView = inflater.inflate(R.layout.list_item_submenu, parent, false); + addViewListeners(actionView); return new ActionViewHolder(actionView); default: throw new UnsupportedOperationException(); @@ -86,6 +91,12 @@ public class CheatsAdapter extends RecyclerView.Adapter return getItemAt(position).getType(); } + private void addViewListeners(View view) + { + CheatsActivity.setOnFocusChangeListenerRecursively(view, + (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus)); + } + private CheatItem getItemAt(int position) { // Patches diff --git a/Source/Android/app/src/main/res/layout-ldrtl/list_item_cheat.xml b/Source/Android/app/src/main/res/layout-ldrtl/list_item_cheat.xml new file mode 100644 index 0000000000..7f1c4d524c --- /dev/null +++ b/Source/Android/app/src/main/res/layout-ldrtl/list_item_cheat.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/Source/Android/app/src/main/res/layout/list_item_cheat.xml b/Source/Android/app/src/main/res/layout/list_item_cheat.xml index 5d79260b68..1b663780f7 100644 --- a/Source/Android/app/src/main/res/layout/list_item_cheat.xml +++ b/Source/Android/app/src/main/res/layout/list_item_cheat.xml @@ -6,7 +6,8 @@ android:id="@+id/root" android:layout_width="match_parent" android:layout_height="wrap_content" - android:focusable="true"> + android:focusable="true" + android:nextFocusRight="@id/checkbox"> + android:focusable="true" + android:nextFocusLeft="@id/root" />