FragmentPagerAdapter неправильно удаляет элементы (фрагменты) - PullRequest
43 голосов
/ 30 января 2012

Я реализовал FragmentPagerAdapter и использую List<Fragment> для хранения всех фрагментов для моего ViewPager для отображения. На addItem() я просто добавляю экземпляр Fragment и затем вызываю notifyDataSetChanged(). Я не уверен, если это необходимо или нет.

Моя проблема просто ... начать с фрагмента 1

[Fragment 1] 

добавить новый фрагмент 2

[Fragment 1] [Fragment 2]

удалить Фрагмент 2

[Fragment 1]

добавить новый фрагмент 3

[Fragment 1] [Fragment 2]

При добавлении новых фрагментов все кажется великолепным. Как только я удаляю фрагмент, а затем добавляю новый экземпляр, старый фрагмент все еще отображается. Когда я иду .getClass.getName(), он дает мне имя Фрагмента 3, но я все еще вижу Фрагмент 2.

Я полагаю, что это может быть проблемой с instantiateItem() или чем-то подобным, но я подумал, что адаптер должен был справиться с этим для нас. Любые предложения будут великолепны.

код адаптера ...

public class MyPagerAdapter extends FragmentPagerAdapter {
public final ArrayList<Fragment> screens2 = new ArrayList<Fragment>();


private Context context;

public MyPagerAdapter(FragmentManager fm, Context context) {
    super(fm);
    this.context = context;
}

public void removeType(String name){
    for(Fragment f: screens2){
        if(f.getClass().getName().contains(name)){ screens2.remove(f); return; }
    }
    this.notifyDataSetChanged();
}

public boolean addSt(String tag, Class<?> clss, Bundle args){
    if(clss==null) return false;
    if(!clss.getName().contains("St")) return false; 
    if(!args.containsKey("cId")) return false;
    boolean has = false;
    boolean hasAlready = false;
    for(Fragment tab: screens2){
        if(tab.getClass().getName().contains("St")){
            has = true;
            if(tab.getArguments().containsKey("cId"))
                if(tab.getArguments().getLong("cId") == args.getLong("cId")){
                    hasAlready = true;
                }
            if(!hasAlready){
                // exists but is different so replace
                screens2.remove(tab);
                this.addScreen(tag, clss, args, C.PAGE_ST);
                // if returned true then it notifies dataset
                return true;
            }
        }
        hasAlready = false;
    }

    if(!has){ 
        // no st yet exist in adapter
        this.addScreen(tag, clss, args, C.PAGE_ST);
        return true;
    }

    return false;
}

public boolean removeCCreate(){
    this.removeType("Create");  
    return false;
}

@Override
public int getItemPosition(Object object) {

   return POSITION_NONE; //To make notifyDataSetChanged() do something
  }

public void addCCreate(){
    this.removeCCreate();
    Log.w("addding c", " ");
    this.addScreen("Create C",  CreateCFragment.class, null, C.PAGE_CREATE_C);
}

public void addScreen(String tag, Class<?> clss, Bundle args, int type){
    if(clss!=null){
        screens2.add(Fragment.instantiate(context, clss.getName(), args));
    }
}

@Override
public int getCount() {
    return screens2.size();
}


@Override
public Fragment getItem(int position) {
    return screens2.get(position); 
}

}

Я понимаю, что код использует некоторые "гетто" средства определения типа фрагмента, однако я написал этот код строго для тестирования функциональности. Любая помощь или идеи были бы великолепны, так как кажется, что не многие люди рискнули в мир FragmentPagerAdapter s.

Ответы [ 8 ]

34 голосов
/ 24 апреля 2012

У меня та же проблема, и мое решение было переопределить метод "destroyItem" следующим образом.

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    FragmentManager manager = ((Fragment)object).getFragmentManager();
    FragmentTransaction trans = manager.beginTransaction();
    trans.remove((Fragment)object);
    trans.commit();
}

Это работа для меня, у кого-нибудь есть другие решения?

Обновлен:

Я обнаружил, что этот код сделал удаление фрагмента ненужным, поэтому я добавил условие, чтобы его избежать.

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (position >= getCount()) {
        FragmentManager manager = ((Fragment) object).getFragmentManager();
        FragmentTransaction trans = manager.beginTransaction();
        trans.remove((Fragment) object);
        trans.commit();
    }
}
6 голосов
/ 01 февраля 2012

Обновил этот пост и включил мое решение (если кто-то может улучшить, дайте мне знать)

Хорошо, теперь я решил свою проблему хакерским способом, но да, это работает;).Если кто-то может улучшить мое решение, пожалуйста, дайте мне знать.Для моего нового решения я теперь использую CustomFragmentStatePagerAdapter, но он не сохраняет состояние, как должно, и сохраняет все фрагменты в списке.Это может вызвать проблемы с памятью, если у пользователя более 50 фрагментов, как у обычного FragmentPagerAdapter.Было бы здорово, если бы кто-то мог добавить State-thing обратно в мое решение, не удаляя мои исправления.Спасибо.

Так вот мой CustomFragmentStatePagerAdapter.java

package com.tundem.webLab.Adapter;

import java.util.ArrayList;

import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentStatePagerAdapter";
    private static final boolean DEBUG = false;

    private final FragmentManager mFragmentManager;
    private FragmentTransaction mCurTransaction = null;

    public ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
    public ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
    private Fragment mCurrentPrimaryItem = null;

    public CustomFragmentStatePagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
    }

    /**
     * Return the Fragment associated with a specified position.
     */
    public abstract Fragment getItem(int position);

    @Override
    public void startUpdate(ViewGroup container) {}

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do. This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.

        // DONE Remove of the add process of the old stuff
        /* if (mFragments.size() > position) { Fragment f = mFragments.get(position); if (f != null) { return f; } } */

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG)
            Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                try // DONE: Try Catch
                {
                    fragment.setInitialSavedState(fss);
                } catch (Exception ex) {
                    // Schon aktiv (kA was das heißt xD)
                }
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.remove(fragment);

        /*if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + " v=" + ((Fragment)
         * object).getView()); while (mSavedState.size() <= position) { mSavedState.add(null); } mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
         * mFragments.set(position, null); mCurTransaction.remove(fragment); */
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(true);
            }
            mCurrentPrimaryItem = fragment;
        }
    }

    @Override
    public void finishUpdate(ViewGroup container) {
        if (mCurTransaction != null) {
            mCurTransaction.commitAllowingStateLoss();
            mCurTransaction = null;
            mFragmentManager.executePendingTransactions();
        }
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return ((Fragment) object).getView() == view;
    }

    @Override
    public Parcelable saveState() {
        Bundle state = null;
        if (mSavedState.size() > 0) {
            state = new Bundle();
            Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
            mSavedState.toArray(fss);
            state.putParcelableArray("states", fss);
        }
        for (int i = 0; i < mFragments.size(); i++) {
            Fragment f = mFragments.get(i);
            if (f != null) {
                if (state == null) {
                    state = new Bundle();
                }
                String key = "f" + i;
                mFragmentManager.putFragment(state, key, f);
            }
        }
        return state;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle) state;
            bundle.setClassLoader(loader);
            Parcelable[] fss = bundle.getParcelableArray("states");
            mSavedState.clear();
            mFragments.clear();
            if (fss != null) {
                for (int i = 0; i < fss.length; i++) {
                    mSavedState.add((Fragment.SavedState) fss[i]);
                }
            }
            Iterable<String> keys = bundle.keySet();
            for (String key : keys) {
                if (key.startsWith("f")) {
                    int index = Integer.parseInt(key.substring(1));
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        while (mFragments.size() <= index) {
                            mFragments.add(null);
                        }
                        f.setMenuVisibility(false);
                        mFragments.set(index, f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }
}

Вот мой обычный FragmentAdapter.java

package com.tundem.webLab.Adapter;

import java.util.LinkedList;
import java.util.List;

import android.support.v4.app.FragmentManager;

import com.tundem.webLab.fragments.BaseFragment;
import com.viewpagerindicator.TitleProvider;

public class FragmentAdapter extends CustomFragmentStatePagerAdapter implements TitleProvider {
    public List<BaseFragment> fragments = new LinkedList<BaseFragment>();

    private int actPage;

    public FragmentAdapter(FragmentManager fm) {
        super(fm);
    }

    public void setActPage(int actPage) {
        this.actPage = actPage;
    }

    public void addItem(BaseFragment fragment) {
        // TODO if exists don't open / change to that tab
        fragments.add(fragment);
    }

    public BaseFragment getActFragment() {
        return getItem(getActPage());
    }

    public int getActPage() {
        return actPage;
    }

    @Override
    public BaseFragment getItem(int position) {
        if (position < getCount()) {
            return fragments.get(position);
        } else
            return null;
    }

    @Override
    public int getCount() {
        return fragments.size();
    }

    @Override
    public String getTitle(int position) {
        return fragments.get(position).getTitle();
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }
}

И так я удаляю фрагмент.(Я знаю, что это немного больше, чем только .remove ()).Будьте свободны в улучшении моего решения, вы также можете добавить этот код где-нибудь в адаптере, так что да.Это зависит от пользователя, который пытается реализовать это.Я использую это в моем TabHelper.java (Класс, который обрабатывает все операции вкладки, такие как удаление, добавление, ...)

    int act = Cfg.mPager.getCurrentItem();
    Cfg.mPager.removeAllViews();
    Cfg.mAdapter.mFragments.remove(act);
    try {
        Cfg.mAdapter.mSavedState.remove(act);
    } catch (Exception ex) {/* Already removed */}
    try {
        Cfg.mAdapter.fragments.remove(act);
    } catch (Exception ex) {/* Already removed */}

    Cfg.mAdapter.notifyDataSetChanged();
    Cfg.mIndicator.notifyDataSetChanged();

Описание Cfg.вещь.Я сохраняю ссылку на эти объекты в классе cfg, так что я всегда могу использовать их без необходимости специального Factory.java ...

Да.Я надеюсь, что смог помочь.Не стесняйтесь улучшать это, но дайте мне знать, чтобы я тоже мог улучшить свой код.

Спасибо.

Если я пропустил какой-либо код, дайте мне знать.


Мой старый ответ также работает, но только если у вас есть разные фрагменты.FileFragment, WebFragment, ... Нет, если вы используете один из этих типов фрагмента дважды.

У меня пока есть псевдо-работа.Это действительно грязное решение, и я все еще ищу лучшее.Пожалуйста помоги.

Я изменил код, где я удаляю вкладку:

   public static void deleteActTab()
        {   
            //We set this on the indicator, NOT the pager
            int act = Cfg.mPager.getCurrentItem();
            Cfg.mAdapter.removeItem(act);
            List<BaseFragment> frags = new LinkedList<BaseFragment>();
            frags = Cfg.mAdapter.fragments;

            Cfg.mPager = (ViewPager)Cfg.act.findViewById(R.id.pager);
            Cfg.mPager.setAdapter(Cfg.mAdapter);
            Cfg.mIndicator.setViewPager(Cfg.mPager);

            Cfg.mAdapter.fragments = frags;

            if(act > 0)
            {
                Cfg.mPager.setCurrentItem(act-1);
                Cfg.mIndicator.setCurrentItem(act-1);
            }

            Cfg.mIndicator.notifyDataSetChanged();
        }

Если кто-то может улучшить этот код, дайте мне знать.Если кто-то может сказать нам реальный ответ на эту проблему.пожалуйста, добавьте это здесь.Есть много людей, которые сталкиваются с этой проблемой.Я добавил репутацию 50 для того, кто ее решает.Я также могу сделать пожертвование для того, кто ее решит.

Спасибо

3 голосов
/ 27 апреля 2016

Может быть, этот ответ поможет вам.

Использование FragmentStatePagerAdapter вместо FragmentPagerAdapter .

Поскольку FragmentPagerAdapter не уничтожает представления.Для получения дополнительной информации прочитайте этот ответ .

3 голосов
/ 12 марта 2015

Взяв «лучшее из обоих миров» (я имею в виду ответы @Tericky Shih и @mikepenz), мы получаем его кратко и просто:

public class MyPagerAdapter extends FragmentPagerAdapter {

    public ArrayList<Fragment> fragments = new ArrayList<Fragment>();    

    ...

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        super.destroyItem(container, position, object);
        if (position >= getCount()) fm.beginTransaction().remove((Fragment) object).commit();
    }

    @Override
    public int getItemPosition(Object object) {
        if (fragments.contains(object)) return fragments.indexOf(object);
        else return POSITION_NONE;
    }
}

Основное отличие состоит в том, что если какой-то фрагмент не изменился, вам не нужно уничтожать его вид и не возвращать POSITION_NONE для него. В то же время я столкнулся с ситуацией, когда ViewPager содержал ссылку на элемент, который уже был уничтожен, поэтому проверка if (fragments.contains(object)) помогает определить, больше не нужен ли этот элемент.

1 голос
/ 15 марта 2015

У меня была ситуация, похожая на вашу. Недавно мне нужно было добавить и удалить фрагменты из ViewPager. В первом режиме у меня есть Фрагменты 0, 1 и 2, а во втором режиме у меня есть Фрагменты 0 и 3. Я хочу, чтобы Фрагмент 0 был одинаковым для обоих режимов и удерживал информацию.

Все, что мне нужно было сделать, это переопределить FragmentPagerAdapter.getItemId, чтобы убедиться, что я вернул уникальный номер для каждого отдельного фрагмента - по умолчанию возвращается «позиция». Мне также пришлось снова установить адаптер в ViewPager - новый экземпляр будет работать, но я снова установил его на тот же экземпляр. Установка адаптера приводит к тому, что ViewPager удаляет все виды и пытается добавить их снова.

Однако хитрость в том, что адаптер вызывает getItem только тогда, когда он хочет создать экземпляр фрагмента, а не каждый раз, когда он показывает его. Это потому, что они кэшируются и ищут их по "позиции", возвращаемой getItemId.

Представьте, что у вас есть три фрагмента (0, 1 и 2), и вы хотите удалить «1». Если вы вернете «положение» для getItemId, то удаление Фрагмента 1 не будет работать, потому что, когда вы попытаетесь показать Фрагмент 2 после удаления Фрагмента 1, пейджер / адаптер будет думать, что он уже получил Фрагмент для этой «позиции» и продолжит отображать Фрагмент 1 .

К вашему сведению: я попытался notifyDataSetChanged вместо настройки адаптера, но у меня это не сработало.

Во-первых, пример переопределения getItemId и то, что я сделал для своего getItem:

public class SectionsPagerAdapter extends FragmentPagerAdapter
{
    ...

    @Override
    public long getItemId(int position)
    {
        // Mode 1 uses Fragments 0, 1 and 2. Mode 2 uses Fragments 0 and 3
        if ( mode == 2 && position == 1 )
            return 3;
        return position;
    }

    @Override
    public Fragment getItem(int position)
    {
        if ( mode == 1 )
        {
            switch (position)
            {
                case 0:
                    return <<fragment 0>>;
                case 1:
                    return <<fragment 1>>;
                case 2:
                    return <<fragment 2>>;
            }
        }
        else    // Mode 2
        {
            switch (position)
            {
                case 0:
                    return <<fragment 0>>;
                case 1:
                    return <<fragment 3>>;
            }
        }
        return null;
    }
}

Теперь смена режима:

private void modeChanged(int newMode)
{
    if ( newMode == mode )
        return;

    mode = newMode;

    // Calling mSectionsPagerAdapter.notifyDataSetChanged() is not enough here
    mViewPager.setAdapter(mSectionsPagerAdapter);
}
0 голосов
/ 31 декабря 2016

Настоящая проблема заключается в том, что FragmentPagerAdapter использует положение фрагмента в вашем списке в качестве идентификатора. Поэтому, если вы добавите новый список или просто удалите элементы, элемент «instantiateItem» найдет разные фрагменты для новых элементов в списке ...

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    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));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

и

  private static String makeFragmentName(int viewId, long id) {
    return "android:switcher:" + viewId + ":" + id;
}

и

     * Return a unique identifier for the item at the given position.
 * <p>
 * <p>The default implementation returns the given position.
 * Subclasses should override this method if the positions of items can change.</p>
 *
 * @param position Position within this adapter
 * @return Unique identifier for the item at position
 */
public long getItemId(int position) {
    return position;
}
0 голосов
/ 23 октября 2016

После долгих попыток я заставил его работать так, чтобы он правильно удалял или прикреплял третий фрагмент в конечной позиции.

Object fragments[] = new Object[3];
int mItems = 2;
MyAdapter mAdapter;
ViewPager mPager;


public void addFragment(boolean bool) {

    mAdapter.startUpdate(mPager);

    if (!bool) {
        mAdapter.destroyItem(mPager, 2, fragments[2]);
        mItems = 2;
        fNach = false;
    }
    else if (bool && !fNach){
        mItems = 3;
        mAdapter.instantiateItem(mPager,2);
        fNach = true;
    }
    mAdapter.finishUpdate(mPager);
    mAdapter.notifyDataSetChanged();

}

public class MyAdapter extends FragmentPagerAdapter {
    MyAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public int getCount() {
        return mItems;
    }

    @Override
    public CharSequence getPageTitle(int position) {
    ... (code for the PagerTitleStrip)
    }

    @Override
    public Fragment getItem(int position) {
        Fragment f = null;
        switch (position) {
            case 0:
                f = new Fragment1();
                break;
            case 1:
                f = new Fragment2();
                break;
            case 2:
                f = new Fragment3();
                break;
        }
        return f;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object o = super.instantiateItem(container,position);
        fragments[position] = o;
        return o;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        super.destroyItem(container, position, object);
        System.out.println("Destroy item " + position);
        if (position >= getCount()) {
                FragmentManager manager = ((Fragment) object).getFragmentManager();
                FragmentTransaction ft = manager.beginTransaction();
                ft.remove((Fragment) object);
                ft.commit();
        }

    }
}

Некоторые пояснения: чтобы получить ссылку на объект для вызова destroyItem,я хранил объекты, возвращенные из instantiateItem в массиве.Когда вы добавляете или удаляете фрагменты, вы должны объявить об этом с помощью startUpdate, finishUpdate и notifyDataSetChanged.Количество элементов должно быть изменено вручную, для добавления вы увеличиваете его и создаете его экземпляр, затем getItem создает его.Для удаления вы вызываете destroyItem, и в этом коде важно положение> = mItems, потому что destroyItem также вызывается, если фрагмент выходит из кэша.Вы не хотите удалять это тогда.Единственное, что не работает, - это смахивание анимации.После удаления последней страницы анимация «невозможно провести пальцем влево» не будет правильно восстановлена ​​на новой последней странице.Если вы проведете по экрану, отобразится пустая страница, которая отскочит назад.

0 голосов
/ 28 ноября 2014

Не сработало для меня.Мое решение было помещено FragmentStatePagerAdapter.java в мой проект, переименованный в FragmentStatePagerAdapter2.java.В destroyItem () я немного изменился на основе журналов ошибок.От

// mFragments.set(position, null);

до

if (position < mFragments.size())mFragments.remove(position);

Возможно, у вас нет такой же проблемы, просто проверьте журнал. Надеюсь, это кому-нибудь поможет!

...