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:
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?
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.