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.
This commit is contained in:
JosJuice 2021-08-11 14:49:20 +02:00
parent 215492152c
commit 47efd3317d
7 changed files with 156 additions and 13 deletions

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -20,25 +20,27 @@ import java.util.ArrayList;
public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
{
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<CheatItemViewHolder>
{
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<CheatItemViewHolder>
return getItemAt(position).getType();
}
private void addViewListeners(View view)
{
CheatsActivity.setOnFocusChangeListenerRecursively(view,
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
}
private CheatItem getItemAt(int position)
{
// Patches

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:nextFocusLeft="@id/checkbox">
<TextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Headline"
android:textSize="16sp"
tools:text="Hyrule Field Speed Hack"
android:layout_margin="@dimen/spacing_large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<CheckBox
android:id="@+id/checkbox"
android:layout_width="48dp"
android:layout_height="64dp"
app:layout_constraintStart_toEndOf="@id/text_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"
android:focusable="true"
android:nextFocusRight="@id/root" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -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">
<TextView
android:id="@+id/text_name"
@ -30,6 +31,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"
android:focusable="true" />
android:focusable="true"
android:nextFocusLeft="@id/root" />
</androidx.constraintlayout.widget.ConstraintLayout>