Search code examples
androidandroid-listviewandroid-viewpager2

ListFragment item selection doesn't always work when nested into ViewPager2


I have a ViewPager2, connected with a TabLayout, that creates multiple fragments, each composed of a ListFragment instance with a single item that can be selected (using android:choiceMode="singleChoice").

The problem is that when running on the Android Emulator (Pixel 2, API 28) after some clicking and scrolling the selection doesn't happen at all as if I didn't click. But if I attach the Android Studio Profiler I can see the red circle every time I click, even if the toast is not displayed and the item not marked as selected.

This is my full code, it's just a simple example.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <-- Tab layout sync-ed with ViewPager2 -->
        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs"
            style="@style/Widget.MaterialComponents.TabLayout.Colored"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </com.google.android.material.appbar.AppBarLayout>

    <!-- ViewPager2 to handle left/right scrolling too -->
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/tabviewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

l_v_page.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LVPageFragment">

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:choiceMode="singleChoice"
        android:background="@color/white">
    </ListView>
</FrameLayout>

l_v_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="?android:attr/activatedBackgroundIndicator" >

    <TextView
        android:id="@+id/row_num"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginRight="4dp"
        android:gravity="center"
        android:minHeight="?android:attr/listPreferredItemHeightLarge"
        android:textSize="30dp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/row_descr"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/row_num"
        android:layout_alignBottom="@id/row_num"
        android:layout_toRightOf="@id/row_num"
        android:gravity="center_vertical"
        android:textColorHighlight="#FFFFFF"
        android:textSize="16sp" />
</RelativeLayout>

MainActivity.class

package com.example.testui4;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import android.os.Bundle;

import com.example.testui4.databinding.ActivityMainBinding;
import com.google.android.material.tabs.TabLayoutMediator;

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());

        // Create adapter for ViewPager2
        binding.tabviewpager.setAdapter(new VPAdapter(this));
        // Connect tabs with the ViewPager2
        new TabLayoutMediator(binding.tabs, binding.tabviewpager, (tab, position) ->
        { tab.setText("List " + (position + 1)); }).attach();

        setContentView(binding.getRoot());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        binding = null;
    }

    // Inner class to handle ViewPager2 with 4 fixed pages
    private class VPAdapter extends FragmentStateAdapter {
        public VPAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); }
        public VPAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) { super(fragmentManager, lifecycle); }

        @NonNull
        @Override
        public Fragment createFragment(int position) {  return LVPageFragment.newInstance(position); }

        @Override
        public int getItemCount() { return 4; }
    }
}

LVAdapter.class

package com.example.testui4;

import android.content.Context;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class LVAdapter extends BaseAdapter {

    protected final Context context;
    protected SparseArray<String> mChoices = null;

    public LVAdapter(Context ctx) { this.context = ctx; }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public int getCount() {
        if (mChoices != null)
            return mChoices.size();
        else
            return 0;
    }

    @Override
    public Object getItem(int i) {
        if (mChoices != null)
            return mChoices.get(i);
        else
            return null;
    }

    @Override
    public long getItemId(int i) { return i; }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        RelativeLayout row = (RelativeLayout) convertView;
        if (row == null) {
            // The view is not recycled, must be re-created
            LayoutInflater inflater = (LayoutInflater) LayoutInflater.from(context);
            row = (RelativeLayout) inflater.inflate(R.layout.l_v_item, parent, false);
        }

        // Get objects
        TextView txtNum = (TextView) row.findViewById(R.id.row_num);
        TextView txtDescr = (TextView) row.findViewById(R.id.row_descr);

        // Set properties
        txtNum.setText(Integer.toString(mChoices.keyAt(position)));
        txtDescr.setText(mChoices.valueAt(position));

        // Return the row
        return row;
    }

    public void updateChoices(SparseArray<String> choices) {
        this.mChoices = choices;
        notifyDataSetChanged();
    }
}

LVPageFragment.class

package com.example.testui4;

import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;

import com.example.testui4.databinding.LVPageBinding;

public class LVPageFragment extends ListFragment {

    private static final String ARG_NUMBER = "number";
    private int mNumber = 1;
    private LVPageBinding binding;

    public LVPageFragment() { }

    public static LVPageFragment newInstance(int number) {
        LVPageFragment fragment = new LVPageFragment();
        Bundle args = new Bundle();
        args.putInt(ARG_NUMBER, number);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {  mNumber = getArguments().getInt(ARG_NUMBER); }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = LVPageBinding.inflate(inflater, container, false);

        // List contents (may change in future for each fragment instance)
        int intPage = mNumber + 1;
        SparseArray<String> placeholder = new SparseArray<>();
        placeholder.put(1, "ALPHA " + intPage);
        placeholder.put(2, "BRAVO " + intPage);
        placeholder.put(3, "CHARLIE " + intPage);
        placeholder.put(4, "DELTA " + intPage);
        placeholder.put(5, "FOXTROT " + intPage);
        placeholder.put(6, "GOLF " + intPage);
        placeholder.put(7, "HOTEL " + intPage);
        placeholder.put(8, "INDIA " + intPage);

        // Send data to the adapter and setup list fragment
        LVAdapter adapter = new LVAdapter(getContext());
        adapter.updateChoices(placeholder);
        setListAdapter(adapter);

        return binding.getRoot();
    }

    @Override
    public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
        Toast.makeText(requireActivity(),"You clicked #" + (position + 1) + " on page #" + (mNumber + 1),
                Toast.LENGTH_SHORT).show();
    }
}

Result:

The item doesn't select after some scrolling

Why does that happen and how can I fix it?

EDIT: I tried to test the same app in a real device and there are no issues, so maybe it is a problem of the emulator?


Solution

  • There is a bug with the ViewPager2 and ListFragment: https://issuetracker.google.com/issues/256270071

    I suppose it may or may not reproduce on real devices.

    If having some predefined number of fragments this could be fixed calling pager.setOffscreenPageLimit with a value ≥ number of fragments.