androidsamsung-mobilestylus-pen

How can I work around strange S Pen bugs with the spinner?


When using a Spinner with all default settings, I found some weird bugs when using the S Pen:

  1. If you open the spinner and hover the S Pen at the bottom of the list, so it scrolls to the bottom, and then lift the S Pen up (so that the "hover circle" disappears), the list jumps back to the top
  2. If you open the spinner and hover the S Pen at the bottom of the list, so it scrolls to the bottom, and then select an option, sometimes (intermittently - maybe about 1 in 4 times) it ignores the selected option and just jumps back to the top

I am testing on a tablet device in landscape orientation, the device is running Android 9 but I have done some tests on other devices and seems to be the same.

For comparison I tried using an app I use regularly which makes heavy use of spinners - "Packing List" by dotnetideas. This app was last updated in 2019 and has target SDK 27. You can easily test the spinners in the app by going into the settings and adding multiple items of luggage, then trying to edit the luggage on any packing list item. I found the S Pen works fine with this app, and it doesn't have the bugs described above, so there must be some way to work around it. That's not an open source app so I can't get ideas from their source code. I tried changing my target SDK to 27, but didn't make any difference.

Here is my code for what seems to me to be a completely vanilla implementation of spinner with all default settings - and still has these same "S Pen bugs" which are not reproducible in Packing List.

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.penpoc">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SPenControlExperiment">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.container, MainFragment.newInstance())
                    .commitNow();
        }
    }
}

main_activity.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />

MainFragment.java:

public class MainFragment extends Fragment {

    public static MainFragment newInstance() {
        return new MainFragment();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.main_fragment, container, false);

        Spinner spDefault = root.findViewById(R.id.spDefault);

        ArrayAdapter<String> defaultSpinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item);
        defaultSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spDefault.setAdapter(defaultSpinnerAdapter);

        defaultSpinnerAdapter.addAll(getExampleList());
        defaultSpinnerAdapter.notifyDataSetChanged();

        return root;
    }

    private List<String> getExampleList() {
        List<String> list = new ArrayList<>();
        list.add("Alpha");
        list.add("Bravo");
        list.add("Charlie");
        list.add("Delta");
        list.add("Echo");
        list.add("Foxtrot");
        list.add("Golf");
        list.add("Hotel");
        list.add("India");
        list.add("Juliet");
        list.add("Kilo");
        list.add("Lima");
        list.add("Mike");
        list.add("November");
        list.add("Oscar");
        list.add("Papa");
        list.add("Quebec");
        list.add("Romeo");
        return list;
    }
}

main_fragment.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainFragment">

    <!-- Add some text views to push the spinner further down the page, this is not strictly necessary -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="one" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="two" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="three" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="four" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="five" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="six" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="seven" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="eight" />

    <Spinner
        android:layout_width="350dp"
        android:layout_height="wrap_content"
        android:id="@+id/spDefault" />

</LinearLayout>

Gradle deps:

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'

Solution

  • I exactly faced the same problem with the S Pen. For an old app, API 25 Spinners were working great, but not any more with API 30. The only way I can resolve it was to replace Spinners by AutoCompleteTextViews. The drop down it generates seems to work correctly with the S Pen.

    A few adaptations are needed to make it look like a Spinner:

    1. Make AutoCompleteTextView not editable

    From the documentation, you can make it by adding android:inputType="none". But, as said in MaterialAutoCompleteTextView code:

    // Due to a framework bug, setting android:inputType="none" on xml has no effect. Therefore,
    // we check it here in case the autoCompleteTextView should be non-editable.
    

    So you have two options:

    2. Handle clicks to show the drop down list

    The default behavior with AutoCompleteTextView is to show suggestions by typing a few characters. So you have to force the drop down show when view gets focused.

    autoComplete.setOnFocusChangeListener(new View.OnFocusChangeListener() {
      @Override
      void onFocusChange(View v, boolean hasFocus) {
        if (hasFocus) {
          autoComplete.showDropDown();
        }
      }
    });
    
    autoComplete.setOnDismissListener(new AutoCompleteTextView.OnDismissListener() {
      @Override
      void onDismiss() {
        // Force focus change after selection
        autoComplete.clearFocus()
      }
    });
    

    3. Style the drop down list

    Contrary to Spinner calling both getView() and getDropDownView() from Adapter, AutoCompleteTextView only calls getView(). So, to provide the correct view, you need to check the parent type in getView() call:

    @Override
    public @NonNull View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
      // Workaround for AutoCompleteTextView using only this method
      if (parent instanceof ListView) {
        return getDropDownView(position, convertView, parent);
      } else {
        return super.getView(position, convertView, parent);
      }
    }
    

    4. Style the AutoCompleteTextView

    One last thing is to add the little caret at the end of the AutoCompleteTextView to make it look like a Spinner. You can use the android:drawableEnd attribute in your XML layout file to handle this.

    5. Other tips