Sostituisci frammento all'interno di un ViewPager


268

Sto cercando di usare Fragment con un ViewPagerutilizzo di FragmentPagerAdapter. Quello che sto cercando di ottenere è sostituire un frammento, posizionato sulla prima pagina diViewPager , con un altro.

Il cercapersone è composto da due pagine. Il primo è il FirstPagerFragment, il secondo è il SecondPagerFragment. Facendo clic su un pulsante della prima pagina. Vorrei sostituire ilFirstPagerFragment con NextFragment.

C'è il mio codice qui sotto.

public class FragmentPagerActivity extends FragmentActivity {

    static final int NUM_ITEMS = 2;

    MyAdapter mAdapter;
    ViewPager mPager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.fragment_pager);

        mAdapter = new MyAdapter(getSupportFragmentManager());

        mPager = (ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(mAdapter);

    }


    /**
     * Pager Adapter
     */
    public static class MyAdapter extends FragmentPagerAdapter {
        public MyAdapter(FragmentManager fm) {
            super(fm);
        }

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

        @Override
        public Fragment getItem(int position) {

            if(position == 0) {
                return FirstPageFragment.newInstance();
            } else {
                return SecondPageFragment.newInstance();
            }

        }
    }


    /**
     * Second Page FRAGMENT
     */
    public static class SecondPageFragment extends Fragment {

        public static SecondPageFragment newInstance() {
            SecondPageFragment f = new SecondPageFragment();
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            //Log.d("DEBUG", "onCreateView");
            return inflater.inflate(R.layout.second, container, false);

        }
    }

    /**
     * FIRST PAGE FRAGMENT
     */
    public static class FirstPageFragment extends Fragment {

        Button button;

        public static FirstPageFragment newInstance() {
            FirstPageFragment f = new FirstPageFragment();
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            //Log.d("DEBUG", "onCreateView");
            View root = inflater.inflate(R.layout.first, container, false);
            button = (Button) root.findViewById(R.id.button);
            button.setOnClickListener(new OnClickListener() {

                @Override
                public void onClick(View v) {
                    FragmentTransaction trans = getFragmentManager().beginTransaction();
                                    trans.replace(R.id.first_fragment_root_id, NextFragment.newInstance());
                    trans.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
                    trans.addToBackStack(null);
                    trans.commit();

                }

            });

            return root;
        }

        /**
     * Next Page FRAGMENT in the First Page
     */
    public static class NextFragment extends Fragment {

        public static NextFragment newInstance() {
            NextFragment f = new NextFragment();
            return f;
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            //Log.d("DEBUG", "onCreateView");
            return inflater.inflate(R.layout.next, container, false);

        }
    }
}

... e qui i file xml

fragment_pager.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:padding="4dip"
        android:gravity="center_horizontal"
        android:layout_width="match_parent" android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1">
    </android.support.v4.view.ViewPager>

</LinearLayout>

first.xml

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

  <Button android:id="@+id/button"
     android:layout_width="wrap_content" android:layout_height="wrap_content"
     android:text="to next"/>

</LinearLayout>

Ora il problema ... in quale ID dovrei usare

trans.replace(R.id.first_fragment_root_id, NextFragment.newInstance());

?

Se io uso R.id.first_fragment_root_id , la sostituzione funziona, ma il Gerarchia visualizzatore mostra uno strano comportamento, come di seguito.

All'inizio la situazione è

dopo la sostituzione la situazione è

Come puoi vedere c'è qualcosa che non va, mi aspetto di trovare lo stesso stato mostrato nella prima immagine dopo aver sostituito il frammento.


Hai generato quel diagramma di layout in qualche modo o l'hai fatto manualmente?
Jeroen Vannevel,

@Noodles: devo fare lo stesso. La tua domanda ha generato un lungo thread con molte risposte diverse. Cosa ha funzionato meglio alla fine? Grazie molto!
narb,

1
@JeroenVannevel potresti averlo già capito, ma quel diagramma di layout è developer.android.com/tools/performance/hierarchy-viewer/…
Ankit Popli

ho avuto qualche problema hai bisogno del tuo aiuto stackoverflow.com/questions/40149039/…
albert

Risposte:


162

C'è un'altra soluzione che non ha bisogno di modificare il codice sorgente di ViewPagere FragmentStatePagerAdapter, e funziona con la FragmentPagerAdapterclasse di base utilizzata dall'autore.

Vorrei iniziare rispondendo alla domanda dell'autore su quale ID dovrebbe usare; è l'ID del contenitore, ovvero l'ID del cercapersone della vista stessa. Tuttavia, come probabilmente hai notato te stesso, l'utilizzo di tale ID nel tuo codice non provoca nulla. Spiegherò perché:

Prima di tutto, per far ViewPagerripopolare le pagine, devi chiamare notifyDataSetChanged()che risiede nella classe base del tuo adattatore.

In secondo luogo, ViewPagerutilizza il getItemPosition()metodo astratto per verificare quali pagine devono essere distrutte e quali devono essere conservate. Viene sempre restituita l'implementazione predefinita di questa funzione POSITION_UNCHANGED, che causa il ViewPagermantenimento di tutte le pagine correnti e, di conseguenza, non allega la nuova pagina. Pertanto, per far funzionare la sostituzione dei frammenti, getItemPosition()deve essere sovrascritto nell'adattatore e deve essere restituito POSITION_NONEquando viene chiamato con un frammento vecchio, per essere nascosto, come argomento.

Ciò significa anche che l'adattatore deve sempre essere consapevole di quale frammento deve essere visualizzato in posizione 0 FirstPageFragmento NextFragment. Un modo per farlo è fornire un ascoltatore durante la creazione FirstPageFragment, che verrà chiamato quando è il momento di cambiare i frammenti. Penso che questa sia una buona cosa, però, per consentire all'adattatore per frammenti di gestire tutti gli switch e le chiamate di frammenti verso ViewPagere FragmentManager.

Terzo, FragmentPagerAdaptermemorizza nella cache i frammenti usati con un nome che deriva dalla posizione, quindi se nella posizione 0 c'era un frammento, non verrà sostituito anche se la classe è nuova. Esistono due soluzioni, ma la più semplice è utilizzare la remove()funzione di FragmentTransaction, che rimuoverà anche il suo tag.

Era un sacco di testo, ecco il codice che dovrebbe funzionare nel tuo caso:

public class MyAdapter extends FragmentPagerAdapter
{
    static final int NUM_ITEMS = 2;
    private final FragmentManager mFragmentManager;
    private Fragment mFragmentAtPos0;

    public MyAdapter(FragmentManager fm)
    {
        super(fm);
        mFragmentManager = fm;
    }

    @Override
    public Fragment getItem(int position)
    {
        if (position == 0)
        {
            if (mFragmentAtPos0 == null)
            {
                mFragmentAtPos0 = FirstPageFragment.newInstance(new FirstPageFragmentListener()
                {
                    public void onSwitchToNextFragment()
                    {
                        mFragmentManager.beginTransaction().remove(mFragmentAtPos0).commit();
                        mFragmentAtPos0 = NextFragment.newInstance();
                        notifyDataSetChanged();
                    }
                });
            }
            return mFragmentAtPos0;
        }
        else
            return SecondPageFragment.newInstance();
    }

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

    @Override
    public int getItemPosition(Object object)
    {
        if (object instanceof FirstPageFragment && mFragmentAtPos0 instanceof NextFragment)
            return POSITION_NONE;
        return POSITION_UNCHANGED;
    }
}

public interface FirstPageFragmentListener
{
    void onSwitchToNextFragment();
}

Spero che questo aiuti chiunque!


24
funziona anche per me, ma come implementare il pulsante Indietro per mostrare il primo frammento?
user1159819

6
Grande spiegazione ma potresti pubblicare il codice sorgente completo di questo, in particolare le classi FirstPageFragment e SecondPageFragment.
georgiecasey,

6
Come si aggiunge FirstPageFragmentListener al pacchetto in newInstance ()?
miguel,

4
Puoi aggiungere il codice che gestisci con il FirstPageFragment.newInstance()parametro listener?
MiguelHincapieC,

6
Questo funziona solo per me se chiamo commitNow () invece di commit () quando rimuovo il frammento. Non sono sicuro del perché il mio caso d'uso sia diverso, ma spero che questo salverà qualcuno un po 'di tempo.
Ian Lovejoy,

37

A partire dal 13 novembre 2012, la sostituzione dei frammenti in un ViewPager sembra essere diventata molto più semplice. Google ha rilasciato Android 4.2 con supporto per frammenti nidificati ed è supportato anche nella nuova libreria di supporto Android v11, quindi funzionerà fino alla 1.6

È molto simile al modo normale di sostituire un frammento, tranne per il fatto che usi getChildFragmentManager. Sembra funzionare tranne per il fatto che il backstack del frammento nidificato non viene visualizzato quando l'utente fa clic sul pulsante Indietro. Come per la soluzione in quella domanda collegata, è necessario chiamare manualmente popBackStackImmediate () sul gestore figlio del frammento. Quindi devi sovrascrivere onBackPressed () dell'attività ViewPager dove otterrai il frammento corrente di ViewPager e chiamerai getChildFragmentManager (). PopBackStackImmediate () su di esso.

Anche ottenere il frammento attualmente visualizzato è un po 'confuso, ho usato questa sporca soluzione "android: switcher: VIEWPAGER_ID: INDEX" ma puoi anche tenere traccia di tutti i frammenti del ViewPager come spiegato nella seconda soluzione in questa pagina .

Quindi ecco il mio codice per un ViewPager con 4 ListView con una vista di dettaglio mostrata nel ViewPager quando l'utente fa clic su una riga e con il pulsante Indietro funzionante. Ho cercato di includere solo il codice pertinente per brevità, quindi lascia un commento se vuoi che l'app completa sia caricata su GitHub.

HomeActivity.java

 public class HomeActivity extends SherlockFragmentActivity {
FragmentAdapter mAdapter;
ViewPager mPager;
TabPageIndicator mIndicator;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    mAdapter = new FragmentAdapter(getSupportFragmentManager());
    mPager = (ViewPager)findViewById(R.id.pager);
    mPager.setAdapter(mAdapter);
    mIndicator = (TabPageIndicator)findViewById(R.id.indicator);
    mIndicator.setViewPager(mPager);
}

// This the important bit to make sure the back button works when you're nesting fragments. Very hacky, all it takes is some Google engineer to change that ViewPager view tag to break this in a future Android update.
@Override
public void onBackPressed() {
    Fragment fragment = (Fragment) getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.pager + ":"+mPager.getCurrentItem());
    if (fragment != null) // could be null if not instantiated yet
    {
        if (fragment.getView() != null) {
            // Pop the backstack on the ChildManager if there is any. If not, close this activity as normal.
            if (!fragment.getChildFragmentManager().popBackStackImmediate()) {
                finish();
            }
        }
    }
}

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

    @Override
    public Fragment getItem(int position) {
        switch (position) {
        case 0:
            return ListProductsFragment.newInstance();
        case 1:
            return ListActiveSubstancesFragment.newInstance();
        case 2:
            return ListProductFunctionsFragment.newInstance();
        case 3:
            return ListCropsFragment.newInstance();
        default:
            return null;
        }
    }

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

 }
}

ListProductsFragment.java

public class ListProductsFragment extends SherlockFragment {
private ListView list;

public static ListProductsFragment newInstance() {
    ListProductsFragment f = new ListProductsFragment();
    return f;
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View V = inflater.inflate(R.layout.list, container, false);
    list = (ListView)V.findViewById(android.R.id.list);
    list.setOnItemClickListener(new OnItemClickListener() {
        public void onItemClick(AdapterView<?> parent, View view,
            int position, long id) {
          // This is important bit
          Fragment productDetailFragment = FragmentProductDetail.newInstance();
          FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
          transaction.addToBackStack(null);
          transaction.replace(R.id.products_list_linear, productDetailFragment).commit();
        }
      });       
    return V;
}
}

7
Sarà possibile caricare il codice su github? Grazie.
Gautham,

4
@georgiecasey: ho scaricato API 17, Android 4.2 che mi ha permesso di iniziare a utilizzare getChildFragmentManager (). Ma mi manca qualcosa nella tua soluzione, poiché ora sto sovrapponendo vecchi + nuovi frammenti sullo schermo. Nota: sto cercando di utilizzare la tua soluzione di concerto con il codice TabsAdapter di esempio di Google da developer.android.com/reference/android/support/v4/view/… . Grazie per eventuali suggerimenti e / o codice di riferimento.
gcl1,

27
A cosa si riferisce R.id.products_list_linear? Si prega di collegarsi al codice come è stato offerto.
howettl

1
@howettl i riferimenti ID al membro esterno del frammento. La chiave per risolvere questo problema è usare frammenti nidificati (in questo modo non è necessario manipolare l'adattatore viewpager).
Indrek Kõue,

2
Questo è mai arrivato a Github?
Baruch,

32

Sulla base della risposta di @wize, che ho trovato utile ed elegante, ho potuto raggiungere parzialmente quello che volevo, perché volevo che la possibilità di tornare al primo frammento una volta sostituita. L'ho raggiunto modificando un po 'il suo codice.

Questo sarebbe il FragmentPagerAdapter:

public static class MyAdapter extends FragmentPagerAdapter {
    private final class CalendarPageListener implements
            CalendarPageFragmentListener {
        public void onSwitchToNextFragment() {
            mFragmentManager.beginTransaction().remove(mFragmentAtPos0)
                    .commit();
            if (mFragmentAtPos0 instanceof FirstFragment){
                mFragmentAtPos0 = NextFragment.newInstance(listener);
            }else{ // Instance of NextFragment
                mFragmentAtPos0 = FirstFragment.newInstance(listener);
            }
            notifyDataSetChanged();
        }
    }

    CalendarPageListener listener = new CalendarPageListener();;
    private Fragment mFragmentAtPos0;
    private FragmentManager mFragmentManager;

    public MyAdapter(FragmentManager fm) {
        super(fm);
        mFragmentManager = fm;
    }

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

    @Override
    public int getItemPosition(Object object) {
        if (object instanceof FirstFragment && mFragmentAtPos0 instanceof NextFragment)
            return POSITION_NONE;
        if (object instanceof NextFragment && mFragmentAtPos0 instanceof FirstFragment)
            return POSITION_NONE;
        return POSITION_UNCHANGED;
    }

    @Override
    public Fragment getItem(int position) {
        if (position == 0)
            return Portada.newInstance();
        if (position == 1) { // Position where you want to replace fragments
            if (mFragmentAtPos0 == null) {
                mFragmentAtPos0 = FirstFragment.newInstance(listener);
            }
            return mFragmentAtPos0;
        }
        if (position == 2)
            return Clasificacion.newInstance();
        if (position == 3)
            return Informacion.newInstance();

        return null;
    }
}

public interface CalendarPageFragmentListener {
    void onSwitchToNextFragment();
}

Per eseguire la sostituzione, è sufficiente definire un campo statico, del tipo CalendarPageFragmentListenere inizializzato attraverso i newInstancemetodi dei frammenti corrispondenti e chiamare FirstFragment.pageListener.onSwitchToNextFragment()o NextFragment.pageListener.onSwitchToNextFragment()rispettivamente.


Come si esegue la sostituzione? non è possibile inizializzare il listener senza sovrascrivere il metodo onSwitchToNext.
Leandros,

1
non perdi lo stato salvato in mFragmentAtPos0 se ruoti il ​​dispositivo?
seb

@seb, in realtà ho migliorato questa soluzione al fine di salvare i mFragmentAtPos0riferimenti durante il salvataggio dello stato di attività. Non è la soluzione più elegante, ma funziona.
mdelolmo,

Puoi vedere la mia risposta . Sostituisce i frammenti e ritorna al primo.
Lyd,

@mdelolmo Potresti dare un'occhiata a questa domanda su ViewPager? Grazie stackoverflow.com/questions/27937250/…
Hoa Vu

21

Ho implementato una soluzione per:

  • Sostituzione di frammenti dinamici all'interno della scheda
  • Manutenzione della cronologia per scheda
  • Lavorare con i cambiamenti di orientamento

I trucchi per raggiungere questo obiettivo sono i seguenti:

  • Utilizzare il metodo notifyDataSetChanged () per applicare la sostituzione del frammento
  • Utilizzare il gestore frammenti solo per il back stage e no per la sostituzione dei frammenti
  • Mantieni la cronologia usando il modello ricordo (pila)

Il codice dell'adattatore è il seguente:

public class TabsAdapter extends FragmentStatePagerAdapter implements ActionBar.TabListener, ViewPager.OnPageChangeListener {

/** The sherlock fragment activity. */
private final SherlockFragmentActivity mActivity;

/** The action bar. */
private final ActionBar mActionBar;

/** The pager. */
private final ViewPager mPager;

/** The tabs. */
private List<TabInfo> mTabs = new LinkedList<TabInfo>();

/** The total number of tabs. */
private int TOTAL_TABS;

private Map<Integer, Stack<TabInfo>> history = new HashMap<Integer, Stack<TabInfo>>();

/**
 * Creates a new instance.
 *
 * @param activity the activity
 * @param pager    the pager
 */
public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
    super(activity.getSupportFragmentManager());
    activity.getSupportFragmentManager();
    this.mActivity = activity;
    this.mActionBar = activity.getSupportActionBar();
    this.mPager = pager;
    mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
}

/**
 * Adds the tab.
 *
 * @param image         the image
 * @param fragmentClass the class
 * @param args          the arguments
 */
public void addTab(final Drawable image, final Class fragmentClass, final Bundle args) {
    final TabInfo tabInfo = new TabInfo(fragmentClass, args);
    final ActionBar.Tab tab = mActionBar.newTab();
    tab.setTabListener(this);
    tab.setTag(tabInfo);
    tab.setIcon(image);

    mTabs.add(tabInfo);
    mActionBar.addTab(tab);

    notifyDataSetChanged();
}

@Override
public Fragment getItem(final int position) {
    final TabInfo tabInfo = mTabs.get(position);
    return Fragment.instantiate(mActivity, tabInfo.fragmentClass.getName(), tabInfo.args);
}

@Override
public int getItemPosition(final Object object) {
    /* Get the current position. */
    int position = mActionBar.getSelectedTab().getPosition();

    /* The default value. */
    int pos = POSITION_NONE;
    if (history.get(position).isEmpty()) {
        return POSITION_NONE;
    }

    /* Checks if the object exists in current history. */
    for (Stack<TabInfo> stack : history.values()) {
        TabInfo c = stack.peek();
        if (c.fragmentClass.getName().equals(object.getClass().getName())) {
            pos = POSITION_UNCHANGED;
            break;
        }
    }
    return pos;
}

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

@Override
public void onPageScrollStateChanged(int arg0) {
}

@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}

@Override
public void onPageSelected(int position) {
    mActionBar.setSelectedNavigationItem(position);
}

@Override
public void onTabSelected(final ActionBar.Tab tab, final FragmentTransaction ft) {
    TabInfo tabInfo = (TabInfo) tab.getTag();
    for (int i = 0; i < mTabs.size(); i++) {
        if (mTabs.get(i).equals(tabInfo)) {
            mPager.setCurrentItem(i);
        }
    }
}

@Override
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
}

@Override
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
}

public void replace(final int position, final Class fragmentClass, final Bundle args) {
    /* Save the fragment to the history. */
    mActivity.getSupportFragmentManager().beginTransaction().addToBackStack(null).commit();

    /* Update the tabs. */
    updateTabs(new TabInfo(fragmentClass, args), position);

    /* Updates the history. */
    history.get(position).push(new TabInfo(mTabs.get(position).fragmentClass, mTabs.get(position).args));

    notifyDataSetChanged();
}

/**
 * Updates the tabs.
 *
 * @param tabInfo
 *          the new tab info
 * @param position
 *          the position
 */
private void updateTabs(final TabInfo tabInfo, final int position) {
    mTabs.remove(position);
    mTabs.add(position, tabInfo);
    mActionBar.getTabAt(position).setTag(tabInfo);
}

/**
 * Creates the history using the current state.
 */
public void createHistory() {
    int position = 0;
    TOTAL_TABS = mTabs.size();
    for (TabInfo mTab : mTabs) {
        if (history.get(position) == null) {
            history.put(position, new Stack<TabInfo>());
        }
        history.get(position).push(new TabInfo(mTab.fragmentClass, mTab.args));
        position++;
    }
}

/**
 * Called on back
 */
public void back() {
    int position = mActionBar.getSelectedTab().getPosition();
    if (!historyIsEmpty(position)) {
        /* In case there is not any other item in the history, then finalize the activity. */
        if (isLastItemInHistory(position)) {
            mActivity.finish();
        }
        final TabInfo currentTabInfo = getPrevious(position);
        mTabs.clear();
        for (int i = 0; i < TOTAL_TABS; i++) {
            if (i == position) {
                mTabs.add(new TabInfo(currentTabInfo.fragmentClass, currentTabInfo.args));
            } else {
                TabInfo otherTabInfo = history.get(i).peek();
                mTabs.add(new TabInfo(otherTabInfo.fragmentClass, otherTabInfo.args));
            }
        }
    }
    mActionBar.selectTab(mActionBar.getTabAt(position));
    notifyDataSetChanged();
}

/**
 * Returns if the history is empty.
 *
 * @param position
 *          the position
 * @return  the flag if empty
 */
private boolean historyIsEmpty(final int position) {
    return history == null || history.isEmpty() || history.get(position).isEmpty();
}

private boolean isLastItemInHistory(final int position) {
    return history.get(position).size() == 1;
}

/**
 * Returns the previous state by the position provided.
 *
 * @param position
 *          the position
 * @return  the tab info
 */
private TabInfo getPrevious(final int position) {
    TabInfo currentTabInfo = history.get(position).pop();
    if (!history.get(position).isEmpty()) {
        currentTabInfo = history.get(position).peek();
    }
    return currentTabInfo;
}

/** The tab info class */
private static class TabInfo {

    /** The fragment class. */
    public Class fragmentClass;

    /** The args.*/
    public Bundle args;

    /**
     * Creates a new instance.
     *
     * @param fragmentClass
     *          the fragment class
     * @param args
     *          the args
     */
    public TabInfo(Class fragmentClass, Bundle args) {
        this.fragmentClass = fragmentClass;
        this.args = args;
    }

    @Override
    public boolean equals(final Object o) {
        return this.fragmentClass.getName().equals(o.getClass().getName());
    }

    @Override
    public int hashCode() {
        return fragmentClass.getName() != null ? fragmentClass.getName().hashCode() : 0;
    }

    @Override
    public String toString() {
        return "TabInfo{" +
                "fragmentClass=" + fragmentClass +
                '}';
    }
}

La prima volta che aggiungi tutte le schede, è necessario chiamare il metodo createHistory () per creare la cronologia iniziale

public void createHistory() {
    int position = 0;
    TOTAL_TABS = mTabs.size();
    for (TabInfo mTab : mTabs) {
        if (history.get(position) == null) {
            history.put(position, new Stack<TabInfo>());
        }
        history.get(position).push(new TabInfo(mTab.fragmentClass, mTab.args));
        position++;
    }
}

Ogni volta che vuoi sostituire un frammento in una scheda specifica che chiami: sostituisci (posizione int finale, classe frammento classe finale, argomenti bundle finale)

/* Save the fragment to the history. */
    mActivity.getSupportFragmentManager().beginTransaction().addToBackStack(null).commit();

    /* Update the tabs. */
    updateTabs(new TabInfo(fragmentClass, args), position);

    /* Updates the history. */
    history.get(position).push(new TabInfo(mTabs.get(position).fragmentClass, mTabs.get(position).args));

    notifyDataSetChanged();

Premendo indietro è necessario chiamare il metodo back ():

public void back() {
    int position = mActionBar.getSelectedTab().getPosition();
    if (!historyIsEmpty(position)) {
        /* In case there is not any other item in the history, then finalize the activity. */
        if (isLastItemInHistory(position)) {
            mActivity.finish();
        }
        final TabInfo currentTabInfo = getPrevious(position);
        mTabs.clear();
        for (int i = 0; i < TOTAL_TABS; i++) {
            if (i == position) {
                mTabs.add(new TabInfo(currentTabInfo.fragmentClass, currentTabInfo.args));
            } else {
                TabInfo otherTabInfo = history.get(i).peek();
                mTabs.add(new TabInfo(otherTabInfo.fragmentClass, otherTabInfo.args));
            }
        }
    }
    mActionBar.selectTab(mActionBar.getTabAt(position));
    notifyDataSetChanged();
}

La soluzione funziona con la barra delle azioni di Sherlock e con il gesto di scorrimento.


è ottimo! potresti metterlo sul github?
Nicramus,

Questa implementazione è la più pulita di tutte quelle fornite qui e risolve ogni singolo problema. Dovrebbe essere la risposta accettata.
Dazedviper,

Qualcuno ha avuto problemi con questa implementazione? Ne sto avendo 2. 1. il primo back non si registra - devo registrarmi due volte per registrarmi. 2. Dopo aver fatto clic indietro (due volte) sulla scheda a e vado alla scheda b, quando torno alla scheda a mostra solo il contenuto della scheda b
yaronbic

7

tl; dr: usa un frammento di host che è responsabile della sostituzione del suo contenuto ospitato e tiene traccia di una cronologia di navigazione indietro (come in un browser).

Dato che il tuo caso d'uso è costituito da un numero fisso di schede, la mia soluzione funziona bene: l'idea è di riempire ViewPager con istanze di una classe personalizzata HostFragment, in grado di sostituire il contenuto ospitato e mantenere la propria cronologia di navigazione. Per sostituire il frammento ospitato si effettua una chiamata al metodo hostfragment.replaceFragment():

public void replaceFragment(Fragment fragment, boolean addToBackstack) {
    if (addToBackstack) {
        getChildFragmentManager().beginTransaction().replace(R.id.hosted_fragment, fragment).addToBackStack(null).commit();
    } else {
        getChildFragmentManager().beginTransaction().replace(R.id.hosted_fragment, fragment).commit();
    }
}

Tutto quel metodo fa è sostituire il layout del frame con l'id R.id.hosted_fragmentcon il frammento fornito al metodo.

Controlla il mio tutorial su questo argomento per ulteriori dettagli e un esempio di lavoro completo su GitHub!


Anche se questo sembra più maldestro, penso che in realtà sia il modo migliore (in termini di struttura e facilità di manutenzione) per farlo.
Sira Lam,

Finisco con la testa di un NPE. : / Impossibile trovare la vista da sostituire
Fariz Fakkel,

6

Alcune delle soluzioni presentate mi hanno aiutato molto a risolvere parzialmente il problema, ma manca ancora una cosa importante nelle soluzioni che ha prodotto eccezioni impreviste e contenuto della pagina nera invece del frammento del contenuto in alcuni casi.

Il fatto è quello classe FragmentPagerAdapter sta usando l'ID oggetto per memorizzare i frammenti memorizzati nella cache in FragmentManager . Per questo motivo, è necessario sovrascrivere anche il metodo getItemId (int position) in modo che restituisca, ad esempio, la posizione per le pagine di livello superiore e la posizione 100 + per le pagine dei dettagli. In caso contrario, il frammento di livello superiore creato in precedenza verrebbe restituito dalla cache anziché dal frammento di livello dettaglio.

Inoltre, sto condividendo qui un esempio completo di come implementare attività simili a schede con pagine Fragment usando ViewPager e pulsanti tab usando RadioGroup che consente la sostituzione di pagine di livello superiore con pagine dettagliate e supporta anche il pulsante Indietro. Questa implementazione supporta solo un livello di back stacking (elenco articoli - dettagli articolo) ma l'implementazione multi-livello di back stacking è semplice. Questo esempio funziona abbastanza bene nei casi normali, tranne per il fatto che genera una NullPointerException nel caso in cui passi ad esempio alla seconda pagina, cambi il frammento della prima pagina (mentre non è visibile) e ritorni alla prima pagina. Pubblicherò una soluzione a questo problema una volta capito:

public class TabsActivity extends FragmentActivity {

  public static final int PAGE_COUNT = 3;
  public static final int FIRST_PAGE = 0;
  public static final int SECOND_PAGE = 1;
  public static final int THIRD_PAGE = 2;

  /**
   * Opens a new inferior page at specified tab position and adds the current page into back
   * stack.
   */
  public void startPage(int position, Fragment content) {
    // Replace page adapter fragment at position.
    mPagerAdapter.start(position, content);
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Initialize basic layout.
    this.setContentView(R.layout.tabs_activity);

    // Add tab fragments to view pager.
    {
      // Create fragments adapter.
      mPagerAdapter = new PagerAdapter(pager);
      ViewPager pager = (ViewPager) super.findViewById(R.id.tabs_view_pager);
      pager.setAdapter(mPagerAdapter);

      // Update active tab in tab bar when page changes.
      pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int index, float value, int nextIndex) {
          // Not used.
        }

        @Override
        public void onPageSelected(int index) {
          RadioGroup tabs_radio_group = (RadioGroup) TabsActivity.this.findViewById(
            R.id.tabs_radio_group);
          switch (index) {
            case 0: {
              tabs_radio_group.check(R.id.first_radio_button);
            }
            break;
            case 1: {
              tabs_radio_group.check(R.id.second_radio_button);
            }
            break;
            case 2: {
              tabs_radio_group.check(R.id.third_radio_button);
            }
            break;
          }
        }

        @Override
        public void onPageScrollStateChanged(int index) {
          // Not used.
        }
      });
    }

    // Set "tabs" radio group on checked change listener that changes the displayed page.
    RadioGroup radio_group = (RadioGroup) this.findViewById(R.id.tabs_radio_group);
    radio_group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
      @Override
      public void onCheckedChanged(RadioGroup radioGroup, int id) {
        // Get view pager representing tabs.
        ViewPager view_pager = (ViewPager) TabsActivity.this.findViewById(R.id.tabs_view_pager);
        if (view_pager == null) {
          return;
        }

        // Change the active page.
        switch (id) {
          case R.id.first_radio_button: {
            view_pager.setCurrentItem(FIRST_PAGE);
          }
          break;
          case R.id.second_radio_button: {
            view_pager.setCurrentItem(SECOND_PAGE);
          }
          break;
          case R.id.third_radio_button: {
            view_pager.setCurrentItem(THIRD_PAGE);
          }
          break;
        }
      });
    }
  }

  @Override
  public void onBackPressed() {
    if (!mPagerAdapter.back()) {
      super.onBackPressed();
    }
  }

  /**
   * Serves the fragments when paging.
   */
  private class PagerAdapter extends FragmentPagerAdapter {

    public PagerAdapter(ViewPager container) {
      super(TabsActivity.this.getSupportFragmentManager());

      mContainer = container;
      mFragmentManager = TabsActivity.this.getSupportFragmentManager();

      // Prepare "empty" list of fragments.
      mFragments = new ArrayList<Fragment>(){};
      mBackFragments = new ArrayList<Fragment>(){};
      for (int i = 0; i < PAGE_COUNT; i++) {
        mFragments.add(null);
        mBackFragments.add(null);
      }
    }

    /**
     * Replaces the view pager fragment at specified position.
     */
    public void replace(int position, Fragment fragment) {
      // Get currently active fragment.
      Fragment old_fragment = mFragments.get(position);
      if (old_fragment == null) {
        return;
      }

      // Replace the fragment using transaction and in underlaying array list.
      // NOTE .addToBackStack(null) doesn't work
      this.startUpdate(mContainer);
      mFragmentManager.beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
        .remove(old_fragment).add(mContainer.getId(), fragment)
        .commit();
      mFragments.set(position, fragment);
      this.notifyDataSetChanged();
      this.finishUpdate(mContainer);
    }

    /**
     * Replaces the fragment at specified position and stores the current fragment to back stack
     * so it can be restored by #back().
     */
    public void start(int position, Fragment fragment) {
      // Remember current fragment.
      mBackFragments.set(position, mFragments.get(position));

      // Replace the displayed fragment.
      this.replace(position, fragment);
    }

    /**
     * Replaces the current fragment by fragment stored in back stack. Does nothing and returns
     * false if no fragment is back-stacked.
     */
    public boolean back() {
      int position = mContainer.getCurrentItem();
      Fragment fragment = mBackFragments.get(position);
      if (fragment == null) {
        // Nothing to go back.
        return false;
      }

      // Restore the remembered fragment and remove it from back fragments.
      this.replace(position, fragment);
      mBackFragments.set(position, null);
      return true;
    }

    /**
     * Returns fragment of a page at specified position.
     */
    @Override
    public Fragment getItem(int position) {
      // If fragment not yet initialized, create its instance.
      if (mFragments.get(position) == null) {
        switch (position) {
          case FIRST_PAGE: {
            mFragments.set(FIRST_PAGE, new DefaultFirstFragment());
          }
          break;
          case SECOND_PAGE: {
            mFragments.set(SECOND_PAGE, new DefaultSecondFragment());
          }
          break;
          case THIRD_PAGE: {
            mFragments.set(THIRD_PAGE, new DefaultThirdFragment());
          }
          break;
        }
      }

      // Return fragment instance at requested position.
      return mFragments.get(position);
    }

    /**
     * Custom item ID resolution. Needed for proper page fragment caching.
     * @see FragmentPagerAdapter#getItemId(int).
     */
    @Override
    public long getItemId(int position) {
      // Fragments from second level page hierarchy have their ID raised above 100. This is
      // important to FragmentPagerAdapter because it is caching fragments to FragmentManager with
      // this item ID key.
      Fragment item = mFragments.get(position);
      if (item != null) {
        if ((item instanceof NewFirstFragment) || (item instanceof NewSecondFragment) ||
          (item instanceof NewThirdFragment)) {
          return 100 + position;
        }
      }

      return position;
    }

    /**
     * Returns number of pages.
     */
    @Override
    public int getCount() {
      return mFragments.size();
    }

    @Override
    public int getItemPosition(Object object)
    {
      int position = POSITION_UNCHANGED;
      if ((object instanceof DefaultFirstFragment) || (object instanceof NewFirstFragment)) {
        if (object.getClass() != mFragments.get(FIRST_PAGE).getClass()) {
          position = POSITION_NONE;
        }
      }
      if ((object instanceof DefaultSecondragment) || (object instanceof NewSecondFragment)) {
        if (object.getClass() != mFragments.get(SECOND_PAGE).getClass()) {
          position = POSITION_NONE;
        }
      }
      if ((object instanceof DefaultThirdFragment) || (object instanceof NewThirdFragment)) {
        if (object.getClass() != mFragments.get(THIRD_PAGE).getClass()) {
          position = POSITION_NONE;
        }
      }
      return position;
    }

    private ViewPager mContainer;
    private FragmentManager mFragmentManager;

    /**
     * List of page fragments.
     */
    private List<Fragment> mFragments;

    /**
     * List of page fragments to return to in onBack();
     */
    private List<Fragment> mBackFragments;
  }

  /**
   * Tab fragments adapter.
   */
  private PagerAdapter mPagerAdapter;
}

Hmm, c'è un altro problema con questo codice. Quando voglio iniziare un'attività dal frammento che è stato rimosso e aggiunto aggain, ricevo un errore: "Stato di salvataggio errore: attivo NewFirstFragment {405478a0} ha cancellato l'indice: -1" (traccia dello stack completo: nopaste.info/547a23dfea.html ). Guardando alle fonti Android, ho capito che ha qualcosa a che fare con lo zaino ma non so come risolverlo finora. Qualcuno ha un indizio?
Blackhex,

2
Bene, usando <code> mFragmentManager.beginTransaction (). Detach (old_fragment) .attach (fragment) .commit (); </code> invece di <pre> mFragmentManager.beginTransaction (). SetTransition (FragmentTransaction.TRANSIT_FRAGMENT_OPEN) .remove ( old_fragment) .add (mContainer.getId (), fragment) .commit (); </pre> nell'esempio precedente sembra risolvere entrambi i problemi :-).
Blackhex,

1
Se usi questo metodo, assicurati di non consentire ad Android di ricreare la tua attività quando ruoti il ​​dispositivo. Altrimenti, PagerAdapter verrà ricreato e perderai i tuoi mBackFragments. Ciò confonderà seriamente anche il FragmentManager. L'uso del BackStack evita questo problema, a spese di un'implementazione più complicata di PagerAdapter (cioè non puoi ereditare da FragmentPagerAdapter.)
Norman

6

Ho creato un ViewPager con 3 elementi e 2 elementi secondari per gli indici 2 e 3 e qui cosa volevo fare ..

inserisci qui la descrizione dell'immagine

Ho implementato questo con l'aiuto delle precedenti domande e risposte di StackOverFlow ed ecco il link.

ViewPagerChildFragments


Il progetto ha esito negativo se si ruota il dispositivo.
Dory,

6

Per sostituire un frammento all'interno di una ViewPagerè possibile spostare i codici sorgente dei ViewPager, PagerAdaptereFragmentStatePagerAdapter le classi nel vostro progetto e aggiungere il codice seguente.

in ViewPager:

public void notifyItemChanged(Object oldItem, Object newItem) {
    if (mItems != null) {
            for (ItemInfo itemInfo : mItems) {
                        if (itemInfo.object.equals(oldItem)) {
                                itemInfo.object = newItem;
                        }
                    }
       }
       invalidate();
    }

in FragmentStatePagerAdapter:

public void replaceFragmetns(ViewPager container, Fragment oldFragment, Fragment newFragment) {
       startUpdate(container);

       // remove old fragment

       if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
       int position = getFragmentPosition(oldFragment);
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, null);
        mFragments.set(position, null);

        mCurTransaction.remove(oldFragment);

        // add new fragment

        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        mFragments.set(position, newFragment);
        mCurTransaction.add(container.getId(), newFragment);

       finishUpdate(container);

       // ensure getItem returns newFragemtn after calling handleGetItemInbalidated()
       handleGetItemInbalidated(container, oldFragment, newFragment);

       container.notifyItemChanged(oldFragment, newFragment);
    }

protected abstract void handleGetItemInbalidated(View container, Fragment oldFragment, Fragment newFragment);
protected abstract int  getFragmentPosition(Fragment fragment);

handleGetItemInvalidated()assicura che dopo la prossima chiamata getItem()restituisca newFragment getFragmentPosition() restituisce la posizione del frammento nell'adattatore.

Ora, per sostituire i frammenti chiama

mAdapter.replaceFragmetns(mViewPager, oldFragment, newFragment);

Se sei interessato a un progetto di esempio, chiedimi le fonti.


Come dovrebbero essere i metodi getFragmentPosition () e handleGetItemInbalidated ()? Non riesco ad aggiornare getItem () per restituire NewFragment .. Per favore

Ciao! Puoi trovare il progetto di prova su questo link files.mail.ru/eng?back=%2FEZU6G4
AndroidTeam At Mail.Ru

1
Il progetto di test ha esito negativo se si ruota il dispositivo.
seb

1
@ AndroidTeamAtMail.Ru il link sembra essere scaduto. Ti invitiamo a condividere il codice
Sunny

Puoi vedere la mia risposta . Sostituisce i frammenti e ritorna al primo.
Lyd,

4

Funziona alla grande con la soluzione di AndroidTeam, tuttavia ho scoperto che avevo bisogno della possibilità di tornare molto indietro, FrgmentTransaction.addToBackStack(null) ma semplicemente aggiungendo questo, il frammento verrà sostituito solo senza avvisare ViewPager. La combinazione della soluzione fornita con questo piccolo miglioramento consentirà di tornare allo stato precedente semplicemente ignorando il onBackPressed()metodo dell'attività . Lo svantaggio maggiore è che tornerà indietro di uno solo alla volta, il che potrebbe comportare più clic

private ArrayList<Fragment> bFragments = new ArrayList<Fragment>();
private ArrayList<Integer> bPosition = new ArrayList<Integer>();

public void replaceFragmentsWithBackOut(ViewPager container, Fragment oldFragment, Fragment newFragment) {
    startUpdate(container);

    // remove old fragment

    if (mCurTransaction == null) {
         mCurTransaction = mFragmentManager.beginTransaction();
     }
    int position = getFragmentPosition(oldFragment);
     while (mSavedState.size() <= position) {
         mSavedState.add(null);
     }

     //Add Fragment to Back List
     bFragments.add(oldFragment);

     //Add Pager Position to Back List
     bPosition.add(position);

     mSavedState.set(position, null);
     mFragments.set(position, null);

     mCurTransaction.remove(oldFragment);

     // add new fragment

     while (mFragments.size() <= position) {
         mFragments.add(null);
     }
     mFragments.set(position, newFragment);
     mCurTransaction.add(container.getId(), newFragment);

    finishUpdate(container);

    // ensure getItem returns newFragemtn after calling handleGetItemInbalidated()
    handleGetItemInvalidated(container, oldFragment, newFragment);

    container.notifyItemChanged(oldFragment, newFragment);
 }


public boolean popBackImmediate(ViewPager container){
    int bFragSize = bFragments.size();
    int bPosSize = bPosition.size();

    if(bFragSize>0 && bPosSize>0){
        if(bFragSize==bPosSize){
            int last = bFragSize-1;
            int position = bPosition.get(last);

            //Returns Fragment Currently at this position
            Fragment replacedFragment = mFragments.get(position);               
            Fragment originalFragment = bFragments.get(last);

            this.replaceFragments(container, replacedFragment, originalFragment);

            bPosition.remove(last);
            bFragments.remove(last);

            return true;
        }
    }

    return false;       
}

Spero che questo aiuti qualcuno.

Anche per quanto riguarda getFragmentPosition()è praticamente getItem()al contrario. Sai quali frammenti vanno dove, assicurati solo di restituire la posizione corretta in cui si troveranno. Ecco un esempio:

    @Override
    protected int getFragmentPosition(Fragment fragment) {
            if(fragment.equals(originalFragment1)){
                return 0;
            }
            if(fragment.equals(replacementFragment1)){
                return 0;
            }
            if(fragment.equals(Fragment2)){
                return 1;
            }
        return -1;
    }

3

Nel tuo onCreateViewmetodo, in containerrealtà è ViewPagerun'istanza.

Quindi, sto solo chiamando

ViewPager vpViewPager = (ViewPager) container;
vpViewPager.setCurrentItem(1);

cambierà il frammento attuale nel tuo ViewPager.


1
Da ore di ricerca, era questa riga di codice che mi serviva per far funzionare correttamente tutte le mie schede. vpViewPager.setCurrentItem(1);. Ho esaminato ogni esempio di persone, senza che succedesse nulla ogni volta fino a quando non sono finalmente arrivato al tuo. Grazie.
Brandon,

1
strabiliato ... è fantastico !! Non mi rendevo conto che la variabile container era il viewpager stesso. Questa risposta deve essere esplorata prima di tutte le altre lunghe. Inoltre, non è possibile trasferire le variabili di listener al costruttore di frammenti, il che semplifica tutta l'implementazione quando Android ricrea automaticamente i frammenti utilizzando il costruttore no-arg predefinito quando la configurazione cambia.
Kevin Lee,

3
non è solo passare a una scheda esistente? come si collega alla sostituzione dei frammenti?
Behelit,

2
Concordo con @behelit, questo non risponde alla domanda, si sposterà semplicemente ViewPagersu un'altra pagina, non sostituirà alcun frammento.
Yoann Hercouet,

2

Ecco la mia soluzione relativamente semplice a questo problema. Le chiavi di questa soluzione devono essere utilizzate al FragmentStatePagerAdapterposto di FragmentPagerAdaptercome la prima rimuoverà i frammenti inutilizzati per te mentre la seconda conserva ancora le loro istanze. Il secondo è l'uso di POSITION_NONEin getItem (). Ho usato un semplice elenco per tenere traccia dei miei frammenti. Il mio requisito era sostituire l'intero elenco di frammenti contemporaneamente con un nuovo elenco, ma il seguente potrebbe essere facilmente modificato per sostituire singoli frammenti:

public class MyFragmentAdapter extends FragmentStatePagerAdapter {
    private List<Fragment> fragmentList = new ArrayList<Fragment>();
    private List<String> tabTitleList = new ArrayList<String>();

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

    public void addFragments(List<Fragment> fragments, List<String> titles) {
        fragmentList.clear();
        tabTitleList.clear();
        fragmentList.addAll(fragments);
        tabTitleList.addAll(titles);
        notifyDataSetChanged();
    }

    @Override
    public int getItemPosition(Object object) {
        if (fragmentList.contains(object)) {
            return POSITION_UNCHANGED;
        }
        return POSITION_NONE;
    }

    @Override
    public Fragment getItem(int item) {
        if (item >= fragmentList.size()) {
            return null;
        }
        return fragmentList.get(item);
    }

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

    @Override
    public CharSequence getPageTitle(int position) {
        return tabTitleList.get(position);
    }
}

2

Ho anche creato una soluzione, che sta lavorando con Stacks . È un approccio più modulare , quindi non è necessario specificare ogni frammento e frammento di dettaglio nel proprio FragmentPagerAdapter. È basato sull'esempio di ActionbarSherlock che deriva se ho ragione dall'app Demo di Google.

/**
 * This is a helper class that implements the management of tabs and all
 * details of connecting a ViewPager with associated TabHost.  It relies on a
 * trick.  Normally a tab host has a simple API for supplying a View or
 * Intent that each tab will show.  This is not sufficient for switching
 * between pages.  So instead we make the content part of the tab host
 * 0dp high (it is not shown) and the TabsAdapter supplies its own dummy
 * view to show as the tab content.  It listens to changes in tabs, and takes
 * care of switch to the correct paged in the ViewPager whenever the selected
 * tab changes.
 * 
 * Changed to support more Layers of fragments on each Tab.
 * by sebnapi (2012)
 * 
 */
public class TabsAdapter extends FragmentPagerAdapter
        implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
    private final Context mContext;
    private final TabHost mTabHost;
    private final ViewPager mViewPager;

    private ArrayList<String> mTabTags = new ArrayList<String>();
    private HashMap<String, Stack<TabInfo>> mTabStackMap = new HashMap<String, Stack<TabInfo>>();

    static final class TabInfo {
        public final String tag;
        public final Class<?> clss;
        public Bundle args;

        TabInfo(String _tag, Class<?> _class, Bundle _args) {
            tag = _tag;
            clss = _class;
            args = _args;
        }
    }

    static class DummyTabFactory implements TabHost.TabContentFactory {
        private final Context mContext;

        public DummyTabFactory(Context context) {
            mContext = context;
        }

        @Override
        public View createTabContent(String tag) {
            View v = new View(mContext);
            v.setMinimumWidth(0);
            v.setMinimumHeight(0);
            return v;
        }
    }

    public interface SaveStateBundle{
        public Bundle onRemoveFragment(Bundle outState);
    }

    public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager pager) {
        super(activity.getSupportFragmentManager());
        mContext = activity;
        mTabHost = tabHost;
        mViewPager = pager;
        mTabHost.setOnTabChangedListener(this);
        mViewPager.setAdapter(this);
        mViewPager.setOnPageChangeListener(this);
    }

    /**
     * Add a Tab which will have Fragment Stack. Add Fragments on this Stack by using
     * addFragment(FragmentManager fm, String _tag, Class<?> _class, Bundle _args)
     * The Stack will hold always the default Fragment u add here.
     * 
     * DON'T ADD Tabs with same tag, it's not beeing checked and results in unexpected
     * beahvior.
     * 
     * @param tabSpec
     * @param clss
     * @param args
     */
    public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args){
        Stack<TabInfo> tabStack = new Stack<TabInfo>();

        tabSpec.setContent(new DummyTabFactory(mContext));
        mTabHost.addTab(tabSpec);
        String tag = tabSpec.getTag();
        TabInfo info = new TabInfo(tag, clss, args);

        mTabTags.add(tag);                  // to know the position of the tab tag 
        tabStack.add(info);
        mTabStackMap.put(tag, tabStack);
        notifyDataSetChanged();
    }

    /**
     * Will add the Fragment to Tab with the Tag _tag. Provide the Class of the Fragment
     * it will be instantiated by this object. Proivde _args for your Fragment.
     * 
     * @param fm
     * @param _tag
     * @param _class
     * @param _args
     */
    public void addFragment(FragmentManager fm, String _tag, Class<?> _class, Bundle _args){
        TabInfo info = new TabInfo(_tag, _class, _args);
        Stack<TabInfo> tabStack = mTabStackMap.get(_tag);   
        Fragment frag = fm.findFragmentByTag("android:switcher:" + mViewPager.getId() + ":" + mTabTags.indexOf(_tag));
        if(frag instanceof SaveStateBundle){
            Bundle b = new Bundle();
            ((SaveStateBundle) frag).onRemoveFragment(b);
            tabStack.peek().args = b;
        }
        tabStack.add(info);
        FragmentTransaction ft = fm.beginTransaction();
        ft.remove(frag).commit();
        notifyDataSetChanged();
    }

    /**
     * Will pop the Fragment added to the Tab with the Tag _tag
     * 
     * @param fm
     * @param _tag
     * @return
     */
    public boolean popFragment(FragmentManager fm, String _tag){
        Stack<TabInfo> tabStack = mTabStackMap.get(_tag);   
        if(tabStack.size()>1){
            tabStack.pop();
            Fragment frag = fm.findFragmentByTag("android:switcher:" + mViewPager.getId() + ":" + mTabTags.indexOf(_tag));
            FragmentTransaction ft = fm.beginTransaction();
            ft.remove(frag).commit();
            notifyDataSetChanged();
            return true;
        }
        return false;
    }

    public boolean back(FragmentManager fm) {
        int position = mViewPager.getCurrentItem();
        return popFragment(fm, mTabTags.get(position));
    }

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

    @Override
    public int getItemPosition(Object object) {
        ArrayList<Class<?>> positionNoneHack = new ArrayList<Class<?>>();
        for(Stack<TabInfo> tabStack: mTabStackMap.values()){
            positionNoneHack.add(tabStack.peek().clss);
        }   // if the object class lies on top of our stacks, we return default
        if(positionNoneHack.contains(object.getClass())){
            return POSITION_UNCHANGED;
        }
        return POSITION_NONE;
    }

    @Override
    public Fragment getItem(int position) {
        Stack<TabInfo> tabStack = mTabStackMap.get(mTabTags.get(position));
        TabInfo info = tabStack.peek();
        return Fragment.instantiate(mContext, info.clss.getName(), info.args);
    }

    @Override
    public void onTabChanged(String tabId) {
        int position = mTabHost.getCurrentTab();
        mViewPager.setCurrentItem(position);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
        // Unfortunately when TabHost changes the current tab, it kindly
        // also takes care of putting focus on it when not in touch mode.
        // The jerk.
        // This hack tries to prevent this from pulling focus out of our
        // ViewPager.
        TabWidget widget = mTabHost.getTabWidget();
        int oldFocusability = widget.getDescendantFocusability();
        widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        mTabHost.setCurrentTab(position);
        widget.setDescendantFocusability(oldFocusability);
    }

    @Override
    public void onPageScrollStateChanged(int state) {
    }

}

Aggiungi questo per la funzionalità del pulsante Indietro in MainActivity:

@Override
public void onBackPressed() {
  if (!mTabsAdapter.back(getSupportFragmentManager())) {
    super.onBackPressed();
  }
}

Se ti piace salvare lo stato del frammento quando viene rimosso. Consenti al tuo frammento di implementare l'interfaccia SaveStateBundlerestituendo nella funzione un pacchetto con il tuo stato di salvataggio. Ottieni il pacchetto dopo l'istanza dithis.getArguments() .

È possibile creare un'istanza di una scheda come questa:

mTabsAdapter.addTab(mTabHost.newTabSpec("firstTabTag").setIndicator("First Tab Title"),
                FirstFragmentActivity.FirstFragmentFragment.class, null);

funziona in modo simile se si desidera aggiungere un frammento sopra una pila di schede. Importante : penso che non funzionerà se vuoi avere 2 istanze della stessa classe su due schede. Ho fatto questa soluzione rapidamente insieme, quindi posso solo condividerla senza fornire alcuna esperienza con essa.


Ottengo un'eccezione nullpointer quando premo il pulsante Indietro e anche il mio frammento sostituito non è visibile. Quindi forse sto facendo qualcosa di sbagliato: quale dovrebbe essere la chiamata corretta per cambiare correttamente i frammenti?
Jeroen,

Aggiornamento: ho risolto la maggior parte delle cose, tuttavia, se premo un pulsante Indietro dopo aver aggiunto un nuovo frammento e poi vado in un'altra scheda e indietro, ottengo un'eccezione nullpointer. Quindi ho 2 domande: 1: come posso rimuovere la cronologia di una scheda e assicurarmi che sia ancora presente solo l'ultimo frammento (parte superiore dello stack)? 2: come posso eliminare l'eccezione nullpointer?
Jeroen,

@Jeroen 1: puoi ottenere lo stack giusto da mTabStackMap, quindi rimuovere tutti gli oggetti TabInfo che si trovano sotto la parte superiore dello stack, 2: molto difficile da dire, fai una domanda con lo stacktrace, quindi potrei provare a scoprirlo, sono sei sicuro che non sia un errore nel tuo codice? Questo succede anche se passi a una scheda (e poi indietro) che si trova nelle immediate vicinanze (destra o sinistra)?
SEB

Ok, ho riscontrato un nuovo problema: se ruoto lo schermo, lo stack sembra essere di nuovo vuoto? Sembra perdere traccia degli elementi precedenti ...
Jeroen,

1
@seb grazie. Stavo usando questo codice ma ora sono possibili frammenti nidificati, quindi non è più un problema. :)
Garcon,

2

La sostituzione di frammenti in un viewpager è piuttosto complicata ma è molto possibile e può sembrare super liscia. Innanzitutto, devi consentire al viewpager stesso di gestire la rimozione e l'aggiunta dei frammenti. Quello che sta succedendo è quando sostituisci il frammento all'interno di SearchFragment, il tuo viewpager mantiene le sue viste frammento. Quindi si finisce con una pagina vuota perché SearchFragment viene rimosso quando si tenta di sostituirlo.

La soluzione è creare un listener all'interno del viewpager che gestirà le modifiche apportate al di fuori di esso, quindi prima aggiungi questo codice nella parte inferiore dell'adattatore.

public interface nextFragmentListener {
    public void fragment0Changed(String newFragmentIdentification);
}

Quindi devi creare una classe privata nel tuo viewpager che diventa un ascoltatore per quando vuoi cambiare il tuo frammento. Ad esempio potresti aggiungere qualcosa del genere. Si noti che implementa l'interfaccia appena creata. Quindi ogni volta che chiami questo metodo, eseguirà il codice all'interno della classe seguente.

private final class fragmentChangeListener implements nextFragmentListener {


    @Override
    public void fragment0Changed(String fragment) {
        //I will explain the purpose of fragment0 in a moment
        fragment0 = fragment;
        manager.beginTransaction().remove(fragAt0).commit();

        switch (fragment){
            case "searchFragment":
                fragAt0 = SearchFragment.newInstance(listener);
                break;
            case "searchResultFragment":
                fragAt0 = Fragment_Table.newInstance(listener);
                break;
        }

        notifyDataSetChanged();
    }

Ci sono due cose principali da sottolineare qui:

  1. fragAt0 è un frammento "flessibile". Può assumere qualunque tipo di frammento tu gli dia. Questo gli permette di diventare il tuo migliore amico nel cambiare il frammento in posizione 0 nel frammento che desideri.
  2. Notare i listener inseriti nel costruttore 'newInstance (listener). Ecco come chiameraiframment0Changed (String newFragmentIdentification) `. Il codice seguente mostra come si crea l'ascoltatore all'interno del frammento.

    static nextFragmentListener listenerSearch;

        public static Fragment_Journals newInstance(nextFragmentListener listener){
                listenerSearch = listener;
                return new Fragment_Journals();
            }

Potresti chiamare il cambiamento all'interno del tuo onPostExecute

private class SearchAsyncTask extends AsyncTask<Void, Void, Void>{

    protected Void doInBackground(Void... params){
        .
        .//some more operation
        .
    }
    protected void onPostExecute(Void param){

        listenerSearch.fragment0Changed("searchResultFragment");
    }

}

Ciò attiverebbe il codice all'interno del viewpager per spostare il frammento in posizione zero fragAt0 per diventare un nuovo searchResultFragment. Ci sono altri due piccoli pezzi che dovresti aggiungere al viewpager prima che diventasse funzionale.

Uno sarebbe nel metodo di sostituzione getItem del viewpager.

@Override
public Fragment getItem(int index) {

    switch (index) {
    case 0:
        //this is where it will "remember" which fragment you have just selected. the key is to set a static String fragment at the top of your page that will hold the position that you had just selected.  

        if(fragAt0 == null){

            switch(fragment0){
            case "searchFragment":
                fragAt0 = FragmentSearch.newInstance(listener);
                break;
            case "searchResultsFragment":
                fragAt0 = FragmentSearchResults.newInstance(listener);
                break;
            }
        }
        return fragAt0;
    case 1:
        // Games fragment activity
        return new CreateFragment();

    }

Ora senza questo pezzo finale otterrai comunque una pagina vuota. Tipo di zoppo, ma è una parte essenziale di viewPager. È necessario sovrascrivere il metodo getItemPosition del viewpager. Normalmente questo metodo restituisce POSITION_UNCHANGED che dice al viewpager di mantenere tutto uguale e quindi getItem non verrà mai chiamato per posizionare il nuovo frammento nella pagina. Ecco un esempio di qualcosa che potresti fare

public int getItemPosition(Object object)
{
    //object is the current fragment displayed at position 0.  
    if(object instanceof SearchFragment && fragAt0 instanceof SearchResultFragment){
        return POSITION_NONE;
    //this condition is for when you press back
    }else if{(object instanceof SearchResultFragment && fragAt0 instanceof SearchFragment){
        return POSITION_NONE;
    }
        return POSITION_UNCHANGED
}

Come ho detto, il codice è molto coinvolto, ma in pratica devi creare un adattatore personalizzato per la tua situazione. Le cose che ho citato renderanno possibile cambiare il frammento. Probabilmente ci vorrà molto tempo per immergere tutto in modo da essere paziente, ma tutto avrà un senso. Vale la pena prenderti il ​​tempo perché può rendere un'applicazione davvero liscia.

Ecco il nugget per la gestione del pulsante Indietro. Lo metti nella tua MainActivity

 public void onBackPressed() {
    if(mViewPager.getCurrentItem() == 0) {
        if(pagerAdapter.getItem(0) instanceof FragmentSearchResults){
            ((Fragment_Table) pagerAdapter.getItem(0)).backPressed();
        }else if (pagerAdapter.getItem(0) instanceof FragmentSearch) {
            finish();
        }
    }

Dovrai creare un metodo chiamato backPressed () all'interno di FragmentSearchResults che chiama fragment0changed. Questo in combinazione con il codice che ho mostrato prima gestirà la pressione del pulsante Indietro. Buona fortuna con il tuo codice per cambiare il viewpager. Ci vuole molto lavoro e, per quanto ho scoperto, non ci sono adattamenti rapidi. Come ho detto, fondamentalmente stai creando un adattatore viewpager personalizzato e gli consenti di gestire tutte le modifiche necessarie utilizzando i listener


Grazie per questa soluzione Ma hai avuto problemi dopo aver ruotato lo schermo. La mia app si arresta in modo anomalo quando ruoto lo schermo e provo a ricaricare un altro frammento nella stessa scheda.
AnteGemini,

È passato un po 'di tempo da quando ho sviluppato Android, ma la mia ipotesi è che ci sia un pezzo di dati su cui la tua app non conta più. Quando si ruota, l'intera attività si riavvia e può causare arresti anomali se si tenta di ricaricare lo stato precedente senza tutti i dati necessari (ad esempio dal pacchetto in onPause, onResume)
user3331142

2

Questo è il mio modo per raggiungerlo.

Prima di tutto aggiungi la scheda Root_fragmentinterna viewPagerin cui vuoi implementare l' fragmentevento click pulsante . Esempio;

@Override
public Fragment getItem(int position) {
  if(position==0)
      return RootTabFragment.newInstance();
  else
      return SecondPagerFragment.newInstance();
}

Prima di tutto, RootTabFragmentdovrebbe essere incluso FragmentLayoutper il cambio di frammento.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/root_frame"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
</FrameLayout>

Quindi, all'interno RootTabFragment onCreateView, implementa fragmentChangeper il tuoFirstPagerFragment

getChildFragmentManager().beginTransaction().replace(R.id.root_frame, FirstPagerFragment.newInstance()).commit();

Successivamente, implementa l' onClickevento per il tuo pulsante all'interno FirstPagerFragmente modifica nuovamente il frammento in questo modo.

getChildFragmentManager().beginTransaction().replace(R.id.root_frame, NextFragment.newInstance()).commit();

Spero che questo ti possa aiutare.


1

Ho trovato una soluzione semplice, che funziona benissimo anche se vuoi aggiungere nuovi frammenti nel mezzo o sostituire il frammento corrente. Nella mia soluzione dovresti scavalcaregetItemId() che dovrebbe restituire un ID univoco per ciascun frammento. Non posizionare come predefinito.

Eccolo:

public class DynamicPagerAdapter extends FragmentPagerAdapter {

private ArrayList<Page> mPages = new ArrayList<Page>();
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();

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

public void replacePage(int position, Page page) {
    mPages.set(position, page);
    notifyDataSetChanged();
}

public void setPages(ArrayList<Page> pages) {
    mPages = pages;
    notifyDataSetChanged();
}

@Override
public Fragment getItem(int position) {
    if (mPages.get(position).mPageType == PageType.FIRST) {
        return FirstFragment.newInstance(mPages.get(position));
    } else {
        return SecondFragment.newInstance(mPages.get(position));
    }
}

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

@Override
public long getItemId(int position) {
    // return unique id
    return mPages.get(position).getId();
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    Fragment fragment = (Fragment) super.instantiateItem(container, position);
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    mFragments.set(position, fragment);
    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    super.destroyItem(container, position, object);
    mFragments.set(position, null);
}

@Override
public int getItemPosition(Object object) {
    PagerFragment pagerFragment = (PagerFragment) object;
    Page page = pagerFragment.getPage();
    int position = mFragments.indexOf(pagerFragment);
    if (page.equals(mPages.get(position))) {
        return POSITION_UNCHANGED;
    } else {
        return POSITION_NONE;
    }
}
}

Avviso: in questo esempio FirstFragmente SecondFragmentestende la classe astratta PageFragment, che ha metodo getPage().


1

Sto facendo qualcosa di simile a Wize, ma nella mia risposta puoi cambiare tra i due frammenti ogni volta che vuoi. E con la risposta intelligente ho alcuni problemi quando si cambia l'orientamento dello schermo e cose del genere. Questo è il PagerAdapter simile a:

    public class MyAdapter extends FragmentPagerAdapter
{
    static final int NUM_ITEMS = 2;
    private final FragmentManager mFragmentManager;
    private Fragment mFragmentAtPos0;
     private Map<Integer, String> mFragmentTags;
     private boolean isNextFragment=false;

    public MyAdapter(FragmentManager fm)
    {
        super(fm);
        mFragmentManager = fm;
         mFragmentTags = new HashMap<Integer, String>();
    }

    @Override
    public Fragment getItem(int position)
    {
        if (position == 0)
        {


            if (isPager) {
                mFragmentAtPos0 = new FirstPageFragment();
            } else {
                mFragmentAtPos0 = new NextFragment();
            }
            return mFragmentAtPos0;
        }
        else
            return SecondPageFragment.newInstance();
    }

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


 @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Object obj = super.instantiateItem(container, position);
        if (obj instanceof Fragment) {
            // record the fragment tag here.
            Fragment f = (Fragment) obj;
            String tag = f.getTag();
            mFragmentTags.put(position, tag);
        }
        return obj;
    }


    public void onChange(boolean isNextFragment) {

        if (mFragmentAtPos0 == null)
            mFragmentAtPos0 = getFragment(0);
        if (mFragmentAtPos0 != null)
            mFragmentManager.beginTransaction().remove(mFragmentAtPos0).commit();


        if (!isNextFragment) {
            mFragmentAtFlashcards = new FirstPageFragment();
        } else {
            mFragmentAtFlashcards = new NextFragment();
        }

        notifyDataSetChanged();


    }


    @Override
    public int getItemPosition(Object object)
    {
        if (object instanceof FirstPageFragment && mFragmentAtPos0 instanceof NextFragment)
            return POSITION_NONE;
         if (object instanceof NextFragment && mFragmentAtPos0 instanceof FirstPageFragment)
            return POSITION_NONE;
        return POSITION_UNCHANGED;
    }


    public Fragment getFragment(int position) {
        String tag = mFragmentTags.get(position);
        if (tag == null)
            return null;
        return mFragmentManager.findFragmentByTag(tag);
    }
}

L'ascoltatore che ho implementato nell'attività del contenitore dell'adattatore per inserirlo nel frammento durante il collegamento, questa è l'attività:

    public class PagerContainerActivity extends AppCompatActivity implements ChangeFragmentListener {

//...

  @Override
    public void onChange(boolean isNextFragment) {
        if (pagerAdapter != null)
            pagerAdapter.onChange(isNextFragment);


    }

//...
}

Quindi nel frammento mettendo l'ascoltatore quando allego una chiamata:

public class FirstPageFragment extends Fragment{


private ChangeFragmentListener changeFragmentListener;


//...
 @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        changeFragmentListener = ((PagerContainerActivity) activity);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        changeFragmentListener = null;
    }
//...
//in the on click to change the fragment
changeFragmentListener.onChange(true);
//...
}

E infine l'ascoltatore:

public interface changeFragmentListener {

    void onChange(boolean isNextFragment);

}

1

Ho seguito le risposte di @wize e @mdelolmo e ho ottenuto la soluzione. Grazie tonnellate. Ma ho ottimizzato un po 'queste soluzioni per migliorare il consumo di memoria.

Problemi che ho osservato:

Salvano l'istanza di Fragmentcui viene sostituita. Nel mio caso, è un frammento che tiene MapViewe ho pensato che fosse costoso. Quindi, sto mantenendo il FragmentPagerPositionChanged (POSITION_NONE or POSITION_UNCHANGED)invece di Fragmentse stesso.

Ecco la mia implementazione.

  public static class DemoCollectionPagerAdapter extends FragmentStatePagerAdapter {

    private SwitchFragListener mSwitchFragListener;
    private Switch mToggle;
    private int pagerAdapterPosChanged = POSITION_UNCHANGED;
    private static final int TOGGLE_ENABLE_POS = 2;


    public DemoCollectionPagerAdapter(FragmentManager fm, Switch toggle) {
        super(fm);
        mToggle = toggle;

        mSwitchFragListener = new SwitchFragListener();
        mToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                mSwitchFragListener.onSwitchToNextFragment();
            }
        });
    }

    @Override
    public Fragment getItem(int i) {
        switch (i)
        {
            case TOGGLE_ENABLE_POS:
                if(mToggle.isChecked())
                {
                    return TabReplaceFragment.getInstance();
                }else
                {
                    return DemoTab2Fragment.getInstance(i);
                }

            default:
                return DemoTabFragment.getInstance(i);
        }
    }

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

    @Override
    public CharSequence getPageTitle(int position) {
        return "Tab " + (position + 1);
    }

    @Override
    public int getItemPosition(Object object) {

        //  This check make sures getItem() is called only for the required Fragment
        if (object instanceof TabReplaceFragment
                ||  object instanceof DemoTab2Fragment)
            return pagerAdapterPosChanged;

        return POSITION_UNCHANGED;
    }

    /**
     * Switch fragments Interface implementation
     */
    private final class SwitchFragListener implements
            SwitchFragInterface {

        SwitchFragListener() {}

        public void onSwitchToNextFragment() {

            pagerAdapterPosChanged = POSITION_NONE;
            notifyDataSetChanged();
        }
    }

    /**
     * Interface to switch frags
     */
    private interface SwitchFragInterface{
        void onSwitchToNextFragment();
    }
}

Demo link qui .. https://youtu.be/l_62uhKkLyM

A scopo dimostrativo, usato 2 frammenti TabReplaceFragmente DemoTab2Fragmentnella posizione due. In tutti gli altri casi sto usando DemoTabFragmentistanze.

Spiegazione:

Sto passando Switchdall'attività al DemoCollectionPagerAdapter. In base allo stato di questo interruttore visualizzeremo il frammento corretto. Quando il controllo interruttore è cambiato, chiamo la SwitchFragListener's onSwitchToNextFragmentmetodo, dove sto cambiando il valore della pagerAdapterPosChangedvariabile POSITION_NONE. Scopri di più su POSITION_NONE . Questo invaliderà getItem e ho logiche per istanziare il frammento giusto laggiù. Scusa, se la spiegazione è un po 'confusa.

Ancora una volta grande grazie a @wize e @mdelolmo per l'idea originale.

Spero sia utile. :)

Fammi sapere se questa implementazione ha qualche difetto. Questo sarà di grande aiuto per il mio progetto.


0

dopo la ricerca ho trovato una soluzione con un codice breve. prima di tutto crea un'istanza pubblica sul frammento e rimuovi il frammento su onSaveInstanceState se il frammento non viene ricreato al cambio di orientamento.

 @Override
public void onSaveInstanceState(Bundle outState) {
    if (null != mCalFragment) {
        FragmentTransaction bt = getChildFragmentManager().beginTransaction();
        bt.remove(mFragment);
        bt.commit();
    }
    super.onSaveInstanceState(outState);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.