Eliminar página de fragmentos de ViewPager en Android
Estoy intentando agregar y eliminar fragmentos dinámicamente de un ViewPager; agregarlos funciona sin ningún problema, pero eliminarlos no funciona como se esperaba.
Cada vez que quiero eliminar el elemento actual, se elimina el último.
También intenté usar un FragmentStatePagerAdapter o devolver POSITION_NONE en el método getItemPosition del adaptador.
¿Qué estoy haciendo mal?
Aquí hay un ejemplo básico:
Actividad principal.java
public class MainActivity extends FragmentActivity implements TextProvider {
private Button mAdd;
private Button mRemove;
private ViewPager mPager;
private MyPagerAdapter mAdapter;
private ArrayList<String> mEntries = new ArrayList<String>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mEntries.add("pos 1");
mEntries.add("pos 2");
mEntries.add("pos 3");
mEntries.add("pos 4");
mEntries.add("pos 5");
mAdd = (Button) findViewById(R.id.add);
mRemove = (Button) findViewById(R.id.remove);
mPager = (ViewPager) findViewById(R.id.pager);
mAdd.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
addNewItem();
}
});
mRemove.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
removeCurrentItem();
}
});
mAdapter = new MyPagerAdapter(this.getSupportFragmentManager(), this);
mPager.setAdapter(mAdapter);
}
private void addNewItem() {
mEntries.add("new item");
mAdapter.notifyDataSetChanged();
}
private void removeCurrentItem() {
int position = mPager.getCurrentItem();
mEntries.remove(position);
mAdapter.notifyDataSetChanged();
}
@Override
public String getTextForPosition(int position) {
return mEntries.get(position);
}
@Override
public int getCount() {
return mEntries.size();
}
private class MyPagerAdapter extends FragmentPagerAdapter {
private TextProvider mProvider;
public MyPagerAdapter(FragmentManager fm, TextProvider provider) {
super(fm);
this.mProvider = provider;
}
@Override
public Fragment getItem(int position) {
return MyFragment.newInstance(mProvider.getTextForPosition(position));
}
@Override
public int getCount() {
return mProvider.getCount();
}
}
}
Proveedor de texto.java
public interface TextProvider {
public String getTextForPosition(int position);
public int getCount();
}
MiFragmento.java
public class MyFragment extends Fragment {
private String mText;
public static MyFragment newInstance(String text) {
MyFragment f = new MyFragment(text);
return f;
}
public MyFragment() {
}
public MyFragment(String text) {
this.mText = text;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment, container, false);
((TextView) root.findViewById(R.id.position)).setText(mText);
return root;
}
}
actividad_principal.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/add"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="add new item" />
<Button
android:id="@+id/remove"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="remove current item" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1" />
</LinearLayout>
fragmento.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/position"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="35sp" />
</LinearLayout>
ViewPager no elimina sus fragmentos con el código anterior porque carga varias vistas (o fragmentos en su caso) en la memoria. Además de la vista visible, también carga la vista a ambos lados de la visible. Esto proporciona un desplazamiento suave de una vista a otra que hace que ViewPager sea tan atractivo.
Para lograr el efecto que desea, debe hacer un par de cosas.
Cambie FragmentPagerAdapter a FragmentStatePagerAdapter. La razón de esto es que FragmentPagerAdapter mantendrá todas las vistas que cargue en la memoria para siempre. Donde FragmentStatePagerAdapter elimina las vistas que quedan fuera de las vistas actuales y transitables.
Anule el método del adaptador getItemPosition (que se muestra a continuación). Cuando llamamos,
mAdapter.notifyDataSetChanged();
ViewPager interroga al adaptador para determinar qué ha cambiado en términos de posicionamiento. Usamos este método para decir que todo ha cambiado, así que reprocese todas las posiciones de su vista.
Y aquí está el código...
private class MyPagerAdapter extends FragmentStatePagerAdapter {
//... your existing code
@Override
public int getItemPosition(Object object){
return PagerAdapter.POSITION_NONE;
}
}
La solución de Louth no fue suficiente para que todo funcionara para mí, ya que los fragmentos existentes no se destruían. Motivado por esta respuesta , descubrí que la solución es anular el getItemId(int position)
método para FragmentPagerAdapter
proporcionar una nueva ID única cada vez que haya un cambio en la posición esperada de un Fragmento.
Código fuente:
private class MyPagerAdapter extends FragmentPagerAdapter {
private TextProvider mProvider;
private long baseId = 0;
public MyPagerAdapter(FragmentManager fm, TextProvider provider) {
super(fm);
this.mProvider = provider;
}
@Override
public Fragment getItem(int position) {
return MyFragment.newInstance(mProvider.getTextForPosition(position));
}
@Override
public int getCount() {
return mProvider.getCount();
}
//this is called when notifyDataSetChanged() is called
@Override
public int getItemPosition(Object object) {
// refresh all fragments when data set changed
return PagerAdapter.POSITION_NONE;
}
@Override
public long getItemId(int position) {
// give an ID different from position when position has been changed
return baseId + position;
}
/**
* Notify that the position of a fragment has been changed.
* Create a new ID for each position to force recreation of the fragment
* @param n number of items which have been changed
*/
public void notifyChangeInPosition(int n) {
// shift the ID returned by getItemId outside the range of all previous fragments
baseId += getCount() + n;
}
}
Ahora, por ejemplo, si elimina una sola pestaña o realiza algún cambio en el orden, debe llamar notifyChangeInPosition(1)
antes de llamar notifyDataSetChanged()
, lo que garantizará que se recrearán todos los Fragmentos.
Por qué funciona esta solución
Anulando getItemPosition():
Cuando notifyDataSetChanged()
se llama, el adaptador llama al notifyChanged()
método al ViewPager
que está conectado. Luego ViewPager
verifica el valor devuelto por el adaptador getItemPosition()
para cada elemento, eliminando los elementos que devuelven POSITION_NONE
(consulte el código fuente ) y luego repoblando.
Anulando getItemId():
Esto es necesario para evitar que el adaptador recargue el fragmento antiguo cuando se ViewPager
está repoblando. Puedes entender fácilmente por qué esto funciona mirando el código fuente de instantiateItem() en FragmentPagerAdapter
.
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
Como puede ver, el getItem()
método solo se llama si el administrador de fragmentos no encuentra fragmentos existentes con el mismo ID. A mí me parece un error que los fragmentos antiguos todavía estén adjuntos incluso después de notifyDataSetChanged()
llamarlos, pero la documentación ViewPager
indica claramente que:
Tenga en cuenta que esta clase se encuentra actualmente en fase inicial de diseño y desarrollo. Es probable que la API cambie en actualizaciones posteriores de la biblioteca de compatibilidad, lo que requerirá cambios en el código fuente de las aplicaciones cuando se compilan con la versión más reciente.
Es de esperar que la solución que se proporciona aquí no sea necesaria en una versión futura de la biblioteca de soporte.
mi solución de trabajo para eliminar la página fragmentada de la vista del buscapersonas
public class MyFragmentAdapter extends FragmentStatePagerAdapter {
private ArrayList<ItemFragment> pages;
public MyFragmentAdapter(FragmentManager fragmentManager, ArrayList<ItemFragment> pages) {
super(fragmentManager);
this.pages = pages;
}
@Override
public Fragment getItem(int index) {
return pages.get(index);
}
@Override
public int getCount() {
return pages.size();
}
@Override
public int getItemPosition(Object object) {
int index = pages.indexOf (object);
if (index == -1)
return POSITION_NONE;
else
return index;
}
}
Y cuando necesito eliminar alguna página por índice hago esto
pages.remove(position); // ArrayList<ItemFragment>
adapter.notifyDataSetChanged(); // MyFragmentAdapter
Aquí está la inicialización de mi adaptador.
MyFragmentAdapter adapter = new MyFragmentAdapter(getSupportFragmentManager(), pages);
viewPager.setAdapter(adapter);