package com.airbnb.epoxy; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.os.ParcelableCompat; import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.util.LongSparseArray; import android.util.SparseArray; import android.view.View; import com.airbnb.epoxy.ViewHolderState.ViewState; import com.airbnb.viewmodeladapter.R; import java.util.Collection; /** * Helper for {@link EpoxyAdapter} to store the state of Views in the adapter. This is useful for * saving changes due to user input, such as text input or selection, when a view is scrolled off * screen or if the adapter needs to be recreated. * <p/> * This saved state is separate from the state represented by a {@link EpoxyModel}, which should * represent the more permanent state of the data shown in the view. This class stores transient * state that is added to the View after it is bound to a {@link EpoxyModel}. For example, a {@link * EpoxyModel} may inflate and bind an EditText and then be responsible for styling it and attaching * listeners. If the user then inputs text, scrolls the view offscreen and then scrolls back, this * class will preserve the inputted text without the {@link EpoxyModel} needing to be aware of its * existence. * <p/> * This class relies on the adapter having stable ids, as the state of a view is mapped to the id of * the {@link EpoxyModel}. */ @SuppressWarnings("WeakerAccess") class ViewHolderState extends LongSparseArray<ViewState> implements Parcelable { public ViewHolderState() { } private ViewHolderState(int size) { super(size); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { final int size = size(); dest.writeInt(size); for (int i = 0; i < size; i++) { dest.writeLong(keyAt(i)); dest.writeParcelable(valueAt(i), 0); } } public static final Creator<ViewHolderState> CREATOR = new Creator<ViewHolderState>() { public ViewHolderState[] newArray(int size) { return new ViewHolderState[size]; } public ViewHolderState createFromParcel(Parcel source) { int size = source.readInt(); ViewHolderState state = new ViewHolderState(size); for (int i = 0; i < size; i++) { long key = source.readLong(); ViewState value = source.readParcelable(ViewState.class.getClassLoader()); state.put(key, value); } return state; } }; public boolean hasStateForHolder(EpoxyViewHolder holder) { return get(holder.getItemId()) != null; } public void save(Collection<EpoxyViewHolder> holders) { for (EpoxyViewHolder holder : holders) { save(holder); } } /** Save the state of the view bound to the given holder. */ public void save(EpoxyViewHolder holder) { if (!holder.getModel().shouldSaveViewState()) { return; } // Reuse the previous sparse array if available. We shouldn't need to clear it since the // exact same view type is being saved to it, which // should have identical ids for all its views, and will just overwrite the previous state. ViewState state = get(holder.getItemId()); if (state == null) { state = new ViewState(); } state.save(holder.itemView); put(holder.getItemId(), state); } /** * If a state was previously saved for this view holder via {@link #save} it will be restored * here. */ public void restore(EpoxyViewHolder holder) { if (!holder.getModel().shouldSaveViewState()) { return; } ViewState state = get(holder.getItemId()); if (state != null) { state.restore(holder.itemView); } } /** * A wrapper around a sparse array as a helper to save the state of a view. This also adds * parcelable support. */ public static class ViewState extends SparseArray<Parcelable> implements Parcelable { public ViewState() { } private ViewState(int size, int[] keys, Parcelable[] values) { super(size); for (int i = 0; i < size; ++i) { put(keys[i], values[i]); } } public void save(View view) { int originalId = view.getId(); setIdIfNoneExists(view); view.saveHierarchyState(this); view.setId(originalId); } public void restore(View view) { int originalId = view.getId(); setIdIfNoneExists(view); view.restoreHierarchyState(this); view.setId(originalId); } /** * If a view hasn't had an id set we need to set a temporary one in order to save state, since a * view won't save its state unless it has an id. The view's id is also the key into the sparse * array for its saved state, so the temporary one we choose just needs to be consistent between * saving and restoring state. */ private void setIdIfNoneExists(View view) { if (view.getId() == View.NO_ID) { view.setId(R.id.view_model_state_saving_id); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { int size = size(); int[] keys = new int[size]; Parcelable[] values = new Parcelable[size]; for (int i = 0; i < size; ++i) { keys[i] = keyAt(i); values[i] = valueAt(i); } parcel.writeInt(size); parcel.writeIntArray(keys); parcel.writeParcelableArray(values, flags); } public static final Creator<ViewState> CREATOR = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<ViewState>() { @Override public ViewState createFromParcel(Parcel source, ClassLoader loader) { int size = source.readInt(); int[] keys = new int[size]; source.readIntArray(keys); Parcelable[] values = source.readParcelableArray(loader); return new ViewState(size, keys, values); } @Override public ViewState[] newArray(int size) { return new ViewState[size]; } }); } }