/* * Copyright (C) 2013 Simon Vig Therkildsen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.simonvt.cathode.util; import android.annotation.SuppressLint; import android.app.Activity; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import timber.log.Timber; import static net.simonvt.cathode.api.util.Preconditions.checkNotNull; /** A class that manages a stack of {@link Fragment}s in a single container. */ public final class FragmentStack { public interface Callback { void onStackChanged(int stackSize, Fragment topFragment); } public static class StackEntry implements Parcelable { Class fragment; String tag; Bundle args; public StackEntry(Class fragment, String tag) { this.fragment = fragment; this.tag = tag; } public StackEntry(Class fragment, String tag, Bundle args) { this.fragment = fragment; this.tag = tag; this.args = args; } public StackEntry(Parcel in) { fragment = (Class) in.readValue(getClass().getClassLoader()); tag = in.readString(); args = in.readBundle(getClass().getClassLoader()); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeValue(fragment); dest.writeString(tag); dest.writeBundle(args); } public static final Parcelable.Creator<StackEntry> CREATOR = new Parcelable.Creator<StackEntry>() { @Override public StackEntry createFromParcel(Parcel in) { return new StackEntry(in); } @Override public StackEntry[] newArray(int size) { return new StackEntry[size]; } }; } /** Create an instance for a specific container. */ public static FragmentStack forContainer(FragmentActivity activity, int containerId) { return forContainer(activity, containerId, null); } /** Create an instance for a specific container. */ public static FragmentStack forContainer(FragmentActivity activity, int containerId, Callback callback) { return new FragmentStack(activity, containerId, callback); } private static final String STATE_STACK = "net.simonvt.util.FragmentStack.stack"; private LinkedList<Fragment> stack = new LinkedList<>(); private Set<String> topLevelTags = new HashSet<>(); private Activity activity; private FragmentManager fragmentManager; private FragmentTransaction fragmentTransaction; private int containerId; private Callback callback; private int enterAnimation; private int exitAnimation; private int popStackEnterAnimation; private int popStackExitAnimation; private boolean paused; private FragmentStack(FragmentActivity activity, int containerId, Callback callback) { this.activity = activity; fragmentManager = activity.getSupportFragmentManager(); this.containerId = containerId; this.callback = callback; } public void pause() { paused = true; } public void resume() { paused = false; commit(); } private boolean allowTransactions() { if (paused || fragmentManager.isDestroyed()) { return false; } return true; } public int positionInstack(Fragment fragment) { return stack.indexOf(fragment); } /** Removes all added fragments and clears the stack. */ public void destroy() { commit(); ensureTransaction(); fragmentTransaction.setCustomAnimations(enterAnimation, exitAnimation); final Fragment topFragment = stack.peekFirst(); for (Fragment f : stack) { if (f != topFragment) removeFragment(f); } stack.clear(); for (String tag : topLevelTags) { removeFragment(fragmentManager.findFragmentByTag(tag)); } fragmentTransaction.commitNow(); fragmentTransaction = null; } public Bundle saveState() { commit(); final int stackSize = stack.size(); String[] stackTags = new String[stackSize]; int i = 0; for (Fragment f : stack) { String tag = f.getTag(); checkNotNull(tag, "Null tag for Fragment %s", f.getClass().getName()); stackTags[i++] = tag; } Bundle outState = new Bundle(); outState.putStringArray(STATE_STACK, stackTags); return outState; } public void restoreState(Bundle inState) { String[] stackTags = inState.getStringArray(STATE_STACK); for (String tag : stackTags) { Fragment f = fragmentManager.findFragmentByTag(tag); stack.add(f); } dispatchOnStackChangedEvent(); } public int size() { return stack.size(); } public Fragment peek() { return stack.peekLast(); } public Fragment peekFirst() { return stack.peekFirst(); } /** Replaces the entire stack with this fragment. */ public void replace(Class fragment, String tag) { replace(fragment, tag, null); } /** * Replaces the entire stack with this fragment. * * @param args Arguments to be set on the fragment using {@link Fragment#setArguments(android.os.Bundle)}. */ public void replace(Class fragment, String tag, Bundle args) { checkNotNull(tag, "Passed null tag for Fragment %s", fragment.getClass().getName()); if (!allowTransactions()) { return; } Fragment first = stack.peekFirst(); if (first != null && tag.equals(first.getTag())) { if (stack.size() > 1) { ensureTransaction(); fragmentTransaction.setCustomAnimations(popStackEnterAnimation, popStackExitAnimation); while (stack.size() > 1) { removeFragment(stack.pollLast()); } attachFragment(stack.peek(), tag); } commit(); return; } Fragment f = fragmentManager.findFragmentByTag(tag); if (f == null) { f = Fragment.instantiate(activity, fragment.getName(), args); } ensureTransaction(); fragmentTransaction.setCustomAnimations(enterAnimation, exitAnimation); clear(); attachFragment(f, tag); stack.add(f); topLevelTags.add(tag); commit(); } public void replaceStack(List<StackEntry> stackEntries) { if (!allowTransactions()) { return; } ensureTransaction(); fragmentTransaction.setCustomAnimations(popStackEnterAnimation, popStackExitAnimation); final int stackSize = stackEntries.size(); if (stackSize > 1) { while (stack.size() > 1) { removeFragment(stack.pollLast()); } } StackEntry topLevel = stackEntries.get(0); Fragment firstFragment = fragmentManager.findFragmentByTag(topLevel.tag); Fragment first = stack.peekFirst(); if (firstFragment == null) { Fragment f = Fragment.instantiate(activity, topLevel.fragment.getName(), topLevel.args); attachFragment(f, topLevel.tag); commit(); stack.clear(); stack.add(f); } if (stackEntries.size() == 1) { if (firstFragment != null) { ensureTransaction(); attachFragment(firstFragment, topLevel.tag); commit(); stack.clear(); stack.add(firstFragment); } } else { ensureTransaction(); if (firstFragment == first) { detachFragment(first); } else { detachFragment(first); detachFragment(firstFragment); } commit(); if (firstFragment != null) { stack.clear(); stack.add(firstFragment); } for (int i = 1; i < stackSize; i++) { StackEntry entry = stackEntries.get(i); Fragment f = Fragment.instantiate(activity, entry.fragment.getName(), entry.args); ensureTransaction(); attachFragment(f, entry.tag); commit(); stack.add(f); if (i + 1 < stackSize) { ensureTransaction(); detachFragment(f); commit(); } } } } public void push(Class fragment, String tag) { push(fragment, tag, null); } /** Adds a new fragment to the stack and displays it. */ public void push(Class fragment, String tag, Bundle args) { checkNotNull(tag, "Passed null tag for Fragment %s", fragment.getClass().getName()); if (!allowTransactions()) { return; } ensureTransaction(); fragmentTransaction.setCustomAnimations(enterAnimation, exitAnimation); detachTop(); Fragment f = fragmentManager.findFragmentByTag(tag); if (f == null) { f = Fragment.instantiate(activity, fragment.getName(), args); } attachFragment(f, tag); stack.add(f); commit(); } /** * Removes the fragment at the top of the stack and displays the previous one. This will not do * anything if there is only one fragment in the stack. * * @return Whether a transaction was committed. */ public boolean pop() { if (!allowTransactions()) { return false; } if (stack.size() > 1) { ensureTransaction(); fragmentTransaction.setCustomAnimations(popStackEnterAnimation, popStackExitAnimation); removeFragment(stack.pollLast()); Fragment f = stack.peekLast(); attachFragment(f, f.getTag()); commit(); return true; } return false; } /** * Removes the top fragment if there are more than one in the stack. * * @return Whether a transaction was committed. */ public boolean removeTop() { if (!allowTransactions()) { return false; } if (stack.size() > 1) { ensureTransaction(); fragmentTransaction.setCustomAnimations(popStackEnterAnimation, popStackExitAnimation); removeFragment(stack.pollLast()); commit(); return true; } return false; } /** * Adds a fragment to the top of the stack and attaches it. */ public void putFragment(Class fragment, String tag, Bundle args) { checkNotNull(tag, "Passed null tag for Fragment %s", fragment.getClass().getName()); if (!allowTransactions()) { return; } ensureTransaction(); fragmentTransaction.setCustomAnimations(enterAnimation, exitAnimation); if (fragmentManager.findFragmentByTag(tag) != null) { throw new IllegalStateException("Fragment with tag " + tag + " already exists"); } Fragment f = Fragment.instantiate(activity, fragment.getName(), args); attachFragment(f, tag); stack.add(f); commit(); } public void attachTop() { Fragment f = stack.peekLast(); attachFragment(f, f.getTag()); } private void detachTop() { Fragment f = stack.peekLast(); detachFragment(f); } private void clear() { Fragment first = stack.peekFirst(); for (Fragment f : stack) { if (f == first) { detachFragment(f); } else { removeFragment(f); } } stack.clear(); } private void dispatchOnStackChangedEvent() { if (callback != null && stack.size() > 0) { callback.onStackChanged(stack.size(), stack.peekLast()); } } @SuppressLint("CommitTransaction") private FragmentTransaction ensureTransaction() { if (fragmentTransaction == null) { fragmentTransaction = fragmentManager.beginTransaction(); } return fragmentTransaction; } private void attachFragment(Fragment fragment, String tag) { checkNotNull(tag, "Passed null tag for Fragment %s", fragment.getClass().getName()); Timber.d("Attaching fragment: %s", tag); if (fragment.isDetached()) { ensureTransaction(); fragmentTransaction.attach(fragment); } else if (!fragment.isAdded()) { ensureTransaction(); fragmentTransaction.add(containerId, fragment, tag); } } private void detachFragment(Fragment fragment) { if (fragment != null && !fragment.isDetached()) { Timber.d("Detaching fragment: %s", fragment.getTag()); ensureTransaction(); fragmentTransaction.detach(fragment); } } private void removeFragment(Fragment fragment) { if (fragment != null && (fragment.isAdded() || fragment.isDetached())) { Timber.d("Removing fragment: %s", fragment.getTag()); ensureTransaction(); fragmentTransaction.remove(fragment); } } public void setDefaultAnimation(int enter, int exit, int popEnter, int popExit) { enterAnimation = enter; exitAnimation = exit; popStackEnterAnimation = popEnter; popStackExitAnimation = popExit; } public void commit() { if (!allowTransactions()) { return; } if (fragmentTransaction != null && !fragmentTransaction.isEmpty()) { fragmentTransaction.commitNow(); } fragmentTransaction = null; } }