Last active
April 10, 2019 08:55
-
-
Save SerhatSurguvec/59ad3cb5ce362dcda6d953810d1a515d to your computer and use it in GitHub Desktop.
ViewPager2 Individual Options Menu Support for FragmentStateAdapter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Similar in behavior to {@link FragmentStatePagerAdapter} | |
* <p> | |
* Lifecycle within {@link RecyclerView}: | |
* <ul> | |
* <li>{@link RecyclerView.ViewHolder} initially an empty {@link FrameLayout}, serves as a | |
* re-usable container for a {@link Fragment} in later stages. | |
* <li>{@link RecyclerView.Adapter#onBindViewHolder} we ask for a {@link Fragment} for the | |
* position. If we already have the fragment, or have previously saved its state, we use those. | |
* <li>{@link RecyclerView.Adapter#onAttachedToWindow} we attach the {@link Fragment} to a | |
* container. | |
* <li>{@link RecyclerView.Adapter#onViewRecycled} and | |
* {@link RecyclerView.Adapter#onFailedToRecycleView} we remove, save state, destroy the | |
* {@link Fragment}. | |
* </ul> | |
*/ | |
public abstract class FragmentStateAdapter extends | |
RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter { | |
// State saving config | |
private static final String KEY_PREFIX_FRAGMENT = "f#"; | |
private static final String KEY_PREFIX_STATE = "s#"; | |
// Fragment GC config | |
private static final long GRACE_WINDOW_TIME_MS = 10_000; // 10 seconds | |
private final FragmentManager mFragmentManager; | |
private final Lifecycle mLifecycle; | |
// Fragment bookkeeping | |
private final LongSparseArray<Fragment> mFragments = new LongSparseArray<>(); | |
private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>(); | |
private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>(); | |
// Fragment GC | |
@SuppressWarnings("WeakerAccess") // to avoid creation of a synthetic accessor | |
boolean mIsInGracePeriod = false; | |
private boolean mHasStaleFragments = false; | |
/** | |
* @param fragmentActivity if the {@link ViewPager2} lives directly in a | |
* {@link FragmentActivity} subclass. | |
* @see FragmentStateAdapter#FragmentStateAdapter(Fragment) | |
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle) | |
*/ | |
public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity) { | |
this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle()); | |
} | |
/** | |
* @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass. | |
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity) | |
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle) | |
*/ | |
public FragmentStateAdapter(@NonNull Fragment fragment) { | |
this(fragment.getChildFragmentManager(), fragment.getLifecycle()); | |
} | |
/** | |
* @param fragmentManager of {@link ViewPager2}'s host | |
* @param lifecycle of {@link ViewPager2}'s host | |
* @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity) | |
* @see FragmentStateAdapter#FragmentStateAdapter(Fragment) | |
*/ | |
public FragmentStateAdapter(@NonNull FragmentManager fragmentManager, | |
@NonNull Lifecycle lifecycle) { | |
mFragmentManager = fragmentManager; | |
mLifecycle = lifecycle; | |
super.setHasStableIds(true); | |
} | |
/** | |
* Provide a Fragment associated with the specified position. | |
*/ | |
public abstract @NonNull | |
Fragment getItem(int position); | |
@NonNull | |
@Override | |
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { | |
return FragmentViewHolder.create(parent); | |
} | |
@Override | |
public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) { | |
final long itemId = holder.getItemId(); | |
final int viewHolderId = holder.getContainer().getId(); | |
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH | |
if (boundItemId != null && boundItemId != itemId) { | |
removeFragment(boundItemId); | |
mItemIdToViewHolder.remove(boundItemId); | |
} | |
mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry | |
ensureFragment(position); | |
/** Special case when {@link RecyclerView} decides to keep the {@link container} | |
* attached to the window, but not to the view hierarchy (i.e. parent is null) */ | |
final FrameLayout container = holder.getContainer(); | |
if (ViewCompat.isAttachedToWindow(container)) { | |
if (container.getParent() != null) { | |
throw new IllegalStateException("Design assumption violated."); | |
} | |
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { | |
@Override | |
public void onLayoutChange(View v, int left, int top, int right, int bottom, | |
int oldLeft, int oldTop, int oldRight, int oldBottom) { | |
if (container.getParent() != null) { | |
container.removeOnLayoutChangeListener(this); | |
placeFragmentInViewHolder(holder); | |
} | |
} | |
}); | |
} | |
gcFragments(); | |
} | |
@SuppressWarnings("WeakerAccess") | |
// to avoid creation of a synthetic accessor | |
void gcFragments() { | |
if (!mHasStaleFragments || shouldDelayFragmentTransactions()) { | |
return; | |
} | |
// Remove Fragments for items that are no longer part of the data-set | |
Set<Long> toRemove = new ArraySet<>(); | |
for (int ix = 0; ix < mFragments.size(); ix++) { | |
long itemId = mFragments.keyAt(ix); | |
if (!containsItem(itemId)) { | |
toRemove.add(itemId); | |
mItemIdToViewHolder.remove(itemId); // in case they're still bound | |
} | |
} | |
// Remove Fragments that are not bound anywhere -- pending a grace period | |
if (!mIsInGracePeriod) { | |
mHasStaleFragments = false; // we've executed all GC checks | |
for (int ix = 0; ix < mFragments.size(); ix++) { | |
long itemId = mFragments.keyAt(ix); | |
if (!mItemIdToViewHolder.containsKey(itemId)) { | |
toRemove.add(itemId); | |
} | |
} | |
} | |
for (Long itemId : toRemove) { | |
removeFragment(itemId); | |
} | |
} | |
private Long itemForViewHolder(int viewHolderId) { | |
Long boundItemId = null; | |
for (int ix = 0; ix < mItemIdToViewHolder.size(); ix++) { | |
if (mItemIdToViewHolder.valueAt(ix) == viewHolderId) { | |
if (boundItemId != null) { | |
throw new IllegalStateException("Design assumption violated: " | |
+ "a ViewHolder can only be bound to one item at a time."); | |
} | |
boundItemId = mItemIdToViewHolder.keyAt(ix); | |
} | |
} | |
return boundItemId; | |
} | |
private void ensureFragment(int position) { | |
long itemId = getItemId(position); | |
if (!mFragments.containsKey(itemId)) { | |
Fragment newFragment = getItem(position); | |
newFragment.setInitialSavedState(mSavedStates.get(itemId)); | |
mFragments.put(itemId, newFragment); | |
} | |
} | |
@Override | |
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) { | |
placeFragmentInViewHolder(holder); | |
gcFragments(); | |
} | |
/** | |
* @param holder that has been bound to a Fragment in the {@link #onBindViewHolder} stage. | |
*/ | |
@SuppressWarnings("WeakerAccess") | |
// to avoid creation of a synthetic accessor | |
void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) { | |
Fragment fragment = mFragments.get(holder.getItemId()); | |
if (fragment == null) { | |
throw new IllegalStateException("Design assumption violated."); | |
} | |
FrameLayout container = holder.getContainer(); | |
View view = fragment.getView(); | |
/* | |
possible states: | |
- fragment: { added, notAdded } | |
- view: { created, notCreated } | |
- view: { attached, notAttached } | |
combinations: | |
- { f:added, v:created, v:attached } -> check if attached to the right container | |
- { f:added, v:created, v:notAttached} -> attach view to container | |
- { f:added, v:notCreated, v:attached } -> impossible | |
- { f:added, v:notCreated, v:notAttached} -> schedule callback for when created | |
- { f:notAdded, v:created, v:attached } -> illegal state | |
- { f:notAdded, v:created, v:notAttached } -> illegal state | |
- { f:notAdded, v:notCreated, v:attached } -> impossible | |
- { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach | |
*/ | |
// { f:notAdded, v:created, v:attached } -> illegal state | |
// { f:notAdded, v:created, v:notAttached } -> illegal state | |
if (!fragment.isAdded() && view != null) { | |
throw new IllegalStateException("Design assumption violated."); | |
} | |
// { f:added, v:notCreated, v:notAttached} -> schedule callback for when created | |
if (fragment.isAdded() && view == null) { | |
scheduleViewAttach(fragment, container); | |
return; | |
} | |
// { f:added, v:created, v:attached } -> check if attached to the right container | |
if (fragment.isAdded() && view.getParent() != null) { | |
if (view.getParent() != container) { | |
addViewToContainer(view, container); | |
} | |
//Set menu visibility of All Fragments to false | |
setMenuVisibilityOfAllFragments(false); | |
//Set menu visibility of primary item | |
fragment.setMenuVisibility(true); | |
return; | |
} | |
// { f:added, v:created, v:notAttached} -> attach view to container | |
if (fragment.isAdded()) { | |
addViewToContainer(view, container); | |
return; | |
} | |
// { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach | |
if (!shouldDelayFragmentTransactions()) { | |
scheduleViewAttach(fragment, container); | |
//Set menu visibility of All Fragments to false | |
setMenuVisibilityOfAllFragments(false); | |
//Set menu visibility of primary item | |
fragment.setMenuVisibility(true); | |
mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow(); | |
} else { | |
if (mFragmentManager.isDestroyed()) { | |
return; // nothing we can do | |
} | |
mLifecycle.addObserver(new LifecycleEventObserver() { | |
@Override | |
public void onStateChanged(@NonNull LifecycleOwner source, | |
@NonNull Lifecycle.Event event) { | |
if (shouldDelayFragmentTransactions()) { | |
return; | |
} | |
source.getLifecycle().removeObserver(this); | |
if (ViewCompat.isAttachedToWindow(holder.getContainer())) { | |
placeFragmentInViewHolder(holder); | |
} | |
} | |
}); | |
} | |
} | |
/** | |
* Sets visibility of all existing fragments with visibility param | |
* @param visibility | |
*/ | |
private void setMenuVisibilityOfAllFragments(Boolean visibility) { | |
for (int i = 0; i < mFragments.size(); i++) { | |
long key = mFragments.keyAt(i); | |
// get the object by the key. | |
Fragment obj = mFragments.get(key); | |
if (obj != null) { | |
obj.setMenuVisibility(visibility); | |
} | |
} | |
} | |
private void scheduleViewAttach(final Fragment fragment, final FrameLayout container) { | |
// After a config change, Fragments that were in FragmentManager will be recreated. Since | |
// ViewHolder container ids are dynamically generated, we opted to manually handle | |
// attaching Fragment views to containers. For consistency, we use the same mechanism for | |
// all Fragment views. | |
mFragmentManager.registerFragmentLifecycleCallbacks( | |
new FragmentManager.FragmentLifecycleCallbacks() { | |
@Override | |
public void onFragmentViewCreated(@NonNull FragmentManager fm, | |
@NonNull Fragment f, @NonNull View v, | |
@Nullable Bundle savedInstanceState) { | |
if (f == fragment) { | |
fm.unregisterFragmentLifecycleCallbacks(this); | |
addViewToContainer(v, container); | |
} | |
} | |
}, false); | |
} | |
@SuppressWarnings("WeakerAccess") | |
// to avoid creation of a synthetic accessor | |
void addViewToContainer(@NonNull View v, FrameLayout container) { | |
if (container.getChildCount() > 1) { | |
throw new IllegalStateException("Design assumption violated."); | |
} | |
if (v.getParent() == container) { | |
return; | |
} | |
if (container.getChildCount() > 0) { | |
container.removeAllViews(); | |
} | |
if (v.getParent() != null) { | |
((ViewGroup) v.getParent()).removeView(v); | |
} | |
container.addView(v); | |
} | |
@Override | |
public final void onViewRecycled(@NonNull FragmentViewHolder holder) { | |
final int viewHolderId = holder.getContainer().getId(); | |
final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH | |
if (boundItemId != null) { | |
removeFragment(boundItemId); | |
mItemIdToViewHolder.remove(boundItemId); | |
} | |
} | |
@Override | |
public final boolean onFailedToRecycleView(@NonNull FragmentViewHolder holder) { | |
// This happens when a ViewHolder is in a transient state (e.g. during custom | |
// animation). We don't have sufficient information on how to clear up what lead to | |
// the transient state, so we are throwing away the ViewHolder to stay on the | |
// conservative side. | |
onViewRecycled(holder); // the same clean-up steps as when recycling a ViewHolder | |
return false; // don't recycle the view | |
} | |
private void removeFragment(long itemId) { | |
Fragment fragment = mFragments.get(itemId); | |
if (fragment == null) { | |
return; | |
} | |
if (fragment.getView() != null) { | |
ViewParent viewParent = fragment.getView().getParent(); | |
if (viewParent != null) { | |
((FrameLayout) viewParent).removeAllViews(); | |
} | |
} | |
if (!containsItem(itemId)) { | |
mSavedStates.remove(itemId); | |
} | |
if (!fragment.isAdded()) { | |
mFragments.remove(itemId); | |
return; | |
} | |
if (shouldDelayFragmentTransactions()) { | |
mHasStaleFragments = true; | |
return; | |
} | |
if (fragment.isAdded() && containsItem(itemId)) { | |
mSavedStates.put(itemId, mFragmentManager.saveFragmentInstanceState(fragment)); | |
} | |
mFragmentManager.beginTransaction().remove(fragment).commitNow(); | |
mFragments.remove(itemId); | |
} | |
@SuppressWarnings("WeakerAccess") | |
// to avoid creation of a synthetic accessor | |
boolean shouldDelayFragmentTransactions() { | |
return mFragmentManager.isStateSaved(); | |
} | |
/** | |
* Default implementation works for collections that don't add, move, remove items. | |
* <p> | |
* TODO(b/122670460): add lint rule | |
* When overriding, also override {@link #containsItem(long)}. | |
* <p> | |
* If the item is not a part of the collection, return {@link RecyclerView#NO_ID}. | |
* | |
* @param position Adapter position | |
* @return stable item id {@link RecyclerView.Adapter#hasStableIds()} | |
*/ | |
@Override | |
public long getItemId(int position) { | |
return position; | |
} | |
/** | |
* Default implementation works for collections that don't add, move, remove items. | |
* <p> | |
* TODO(b/122670460): add lint rule | |
* When overriding, also override {@link #getItemId(int)} | |
*/ | |
public boolean containsItem(long itemId) { | |
return itemId >= 0 && itemId < getItemCount(); | |
} | |
@Override | |
public final void setHasStableIds(boolean hasStableIds) { | |
throw new UnsupportedOperationException( | |
"Stable Ids are required for the adapter to function properly, and the adapter " | |
+ "takes care of setting the flag."); | |
} | |
@Override | |
public @NonNull | |
Parcelable saveState() { | |
/** TODO(b/122670461): use custom {@link Parcelable} instead of Bundle to save space */ | |
Bundle savedState = new Bundle(mFragments.size() + mSavedStates.size()); | |
/** save references to active fragments */ | |
for (int ix = 0; ix < mFragments.size(); ix++) { | |
long itemId = mFragments.keyAt(ix); | |
Fragment fragment = mFragments.get(itemId); | |
if (fragment != null && fragment.isAdded()) { | |
String key = createKey(KEY_PREFIX_FRAGMENT, itemId); | |
mFragmentManager.putFragment(savedState, key, fragment); | |
} | |
} | |
/** Write {@link mSavedStates) into a {@link Parcelable} */ | |
for (int ix = 0; ix < mSavedStates.size(); ix++) { | |
long itemId = mSavedStates.keyAt(ix); | |
if (containsItem(itemId)) { | |
String key = createKey(KEY_PREFIX_STATE, itemId); | |
savedState.putParcelable(key, mSavedStates.get(itemId)); | |
} | |
} | |
return savedState; | |
} | |
@Override | |
public void restoreState(@NonNull Parcelable savedState) { | |
if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) { | |
throw new IllegalStateException( | |
"Expected the adapter to be 'fresh' while restoring state."); | |
} | |
Bundle bundle = (Bundle) savedState; | |
for (String key : bundle.keySet()) { | |
if (isValidKey(key, KEY_PREFIX_FRAGMENT)) { | |
long itemId = parseIdFromKey(key, KEY_PREFIX_FRAGMENT); | |
Fragment fragment = mFragmentManager.getFragment(bundle, key); | |
mFragments.put(itemId, fragment); | |
continue; | |
} | |
if (isValidKey(key, KEY_PREFIX_STATE)) { | |
long itemId = parseIdFromKey(key, KEY_PREFIX_STATE); | |
Fragment.SavedState state = bundle.getParcelable(key); | |
if (containsItem(itemId)) { | |
mSavedStates.put(itemId, state); | |
} | |
continue; | |
} | |
throw new IllegalArgumentException("Unexpected key in savedState: " + key); | |
} | |
if (!mFragments.isEmpty()) { | |
mHasStaleFragments = true; | |
mIsInGracePeriod = true; | |
gcFragments(); | |
scheduleGracePeriodEnd(); | |
} | |
} | |
private void scheduleGracePeriodEnd() { | |
final Handler handler = new Handler(Looper.getMainLooper()); | |
final Runnable runnable = new Runnable() { | |
@Override | |
public void run() { | |
mIsInGracePeriod = false; | |
gcFragments(); // good opportunity to GC | |
} | |
}; | |
mLifecycle.addObserver(new LifecycleEventObserver() { | |
@Override | |
public void onStateChanged(@NonNull LifecycleOwner source, | |
@NonNull Lifecycle.Event event) { | |
if (event == Lifecycle.Event.ON_DESTROY) { | |
handler.removeCallbacks(runnable); | |
source.getLifecycle().removeObserver(this); | |
} | |
} | |
}); | |
handler.postDelayed(runnable, GRACE_WINDOW_TIME_MS); | |
} | |
// Helper function for dealing with save / restore state | |
private static @NonNull | |
String createKey(@NonNull String prefix, long id) { | |
return prefix + id; | |
} | |
// Helper function for dealing with save / restore state | |
private static boolean isValidKey(@NonNull String key, @NonNull String prefix) { | |
return key.startsWith(prefix) && key.length() > prefix.length(); | |
} | |
// Helper function for dealing with save / restore state | |
private static long parseIdFromKey(@NonNull String key, @NonNull String prefix) { | |
return Long.parseLong(key.substring(prefix.length())); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* {@link ViewHolder} implementation for handling {@link Fragment}s. Used in | |
* {@link FragmentStateAdapter}. | |
*/ | |
public final class FragmentViewHolder extends RecyclerView.ViewHolder { | |
private FragmentViewHolder(FrameLayout container) { | |
super(container); | |
} | |
static FragmentViewHolder create(ViewGroup parent) { | |
FrameLayout container = new FrameLayout(parent.getContext()); | |
container.setLayoutParams( | |
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, | |
ViewGroup.LayoutParams.MATCH_PARENT)); | |
container.setId(ViewCompat.generateViewId()); | |
container.setSaveEnabled(false); | |
return new FragmentViewHolder(container); | |
} | |
FrameLayout getContainer() { | |
return (FrameLayout) itemView; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
inner class ViewPagerAdapter : FragmentStateAdapter(this) { | |
override fun getItem(position: Int): Fragment { | |
return YourFragment.newInstance() | |
} | |
override fun getItemCount(): Int { | |
return yourList.size | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Copy and Paste FragmentStateAdapter.java and FragmentViewHolder.java into the same package. Then use it as in Usage.kt.