Skip to content

Instantly share code, notes, and snippets.

@SerhatSurguvec
Last active April 10, 2019 08:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SerhatSurguvec/59ad3cb5ce362dcda6d953810d1a515d to your computer and use it in GitHub Desktop.
Save SerhatSurguvec/59ad3cb5ce362dcda6d953810d1a515d to your computer and use it in GitHub Desktop.
ViewPager2 Individual Options Menu Support for FragmentStateAdapter
/**
* 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()));
}
}
/**
* {@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;
}
}
inner class ViewPagerAdapter : FragmentStateAdapter(this) {
override fun getItem(position: Int): Fragment {
return YourFragment.newInstance()
}
override fun getItemCount(): Int {
return yourList.size
}
}
@SerhatSurguvec
Copy link
Author

Copy and Paste FragmentStateAdapter.java and FragmentViewHolder.java into the same package. Then use it as in Usage.kt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment