Instantly share code, notes, and snippets.
Last active
February 13, 2023 23:51
-
Star
17
(17)
You must be signed in to star a gist -
Fork
7
(7)
You must be signed in to fork a gist
-
Save NikolaDespotoski/c426abe41d98d5c2f68621c85532ea88 to your computer and use it in GitHub Desktop.
Saving and restoring BottomNavigationView state
This file contains hidden or 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
import android.content.Context; | |
import android.graphics.Canvas; | |
import android.graphics.drawable.Drawable; | |
import android.os.Build; | |
import android.os.Parcel; | |
import android.os.Parcelable; | |
import android.support.annotation.CallSuper; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.RequiresApi; | |
import android.support.design.widget.BottomNavigationView; | |
import android.support.design.widget.NavigationView; | |
import android.support.v4.content.ContextCompat; | |
import android.support.v4.os.ParcelableCompat; | |
import android.support.v4.os.ParcelableCompatCreatorCallbacks; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v7.view.menu.MenuItemImpl; | |
import android.support.v7.view.menu.MenuView; | |
import android.util.AttributeSet; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import static android.support.design.widget.TabLayout.Tab.INVALID_POSITION; | |
/** | |
* Created by Nikola on 10/31/2016. | |
*/ | |
public class RichBottomNavigationView extends BottomNavigationView { | |
private ViewGroup mBottomItemsHolder; | |
private int mLastSelection = INVALID_POSITION; | |
private Drawable mShadowDrawable; | |
private boolean mShadowVisible = true; | |
private int mWidth; | |
private int mHeight; | |
private int mShadowElevation = 8; | |
public RichBottomNavigationView(Context context) { | |
super(context); | |
init(); | |
} | |
public RichBottomNavigationView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(); | |
} | |
public RichBottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(); | |
; | |
} | |
private void init() { | |
mShadowDrawable = ContextCompat.getDrawable(getContext(), R.drawable.shadow); | |
if (mShadowDrawable != null) { | |
mShadowDrawable.setCallback(this); | |
} | |
setBackgroundColor(ContextCompat.getColor(getContext(), android.R.color.transparent)); | |
setShadowVisible(true); | |
setWillNotDraw(false); | |
} | |
@Override | |
protected void onSizeChanged(int w, int h, int oldw, int oldh) { | |
super.onSizeChanged(w, h + mShadowElevation, oldw, oldh); | |
mWidth = w; | |
mHeight = h; | |
updateShadowBounds(); | |
} | |
private void updateShadowBounds() { | |
if (mShadowDrawable != null && mBottomItemsHolder != null) { | |
mShadowDrawable.setBounds(0, 0, mWidth, mShadowElevation); | |
} | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
@Override | |
public void draw(Canvas canvas) { | |
super.draw(canvas); | |
if (mShadowDrawable != null && mShadowVisible) { | |
mShadowDrawable.draw(canvas); | |
} | |
} | |
public void setShadowVisible(boolean shadowVisible) { | |
setWillNotDraw(!mShadowVisible); | |
updateShadowBounds(); | |
} | |
public int getShadowElevation() { | |
return mShadowVisible ? mShadowElevation : 0; | |
} | |
public int getSelectedItem() { | |
return mLastSelection = findSelectedItem(); | |
} | |
@CallSuper | |
public void setSelectedItem(int position) { | |
if (position >= getMenu().size() || position < 0) return; | |
View menuItemView = getMenuItemView(position); | |
if (menuItemView == null) return; | |
MenuItemImpl itemData = ((MenuView.ItemView) menuItemView).getItemData(); | |
itemData.setChecked(true); | |
boolean previousHapticFeedbackEnabled = menuItemView.isHapticFeedbackEnabled(); | |
menuItemView.setSoundEffectsEnabled(false); | |
menuItemView.setHapticFeedbackEnabled(false); //avoid hearing click sounds, disable haptic and restore settings later of that view | |
menuItemView.performClick(); | |
menuItemView.setHapticFeedbackEnabled(previousHapticFeedbackEnabled); | |
menuItemView.setSoundEffectsEnabled(true); | |
mLastSelection = position; | |
} | |
@Override | |
protected Parcelable onSaveInstanceState() { | |
Parcelable superState = super.onSaveInstanceState(); | |
BottomNavigationState state = new BottomNavigationState(superState); | |
mLastSelection = getSelectedItem(); | |
state.lastSelection = mLastSelection; | |
return state; | |
} | |
@Override | |
protected void onRestoreInstanceState(Parcelable state) { | |
if (!(state instanceof BottomNavigationState)) { | |
super.onRestoreInstanceState(state); | |
return; | |
} | |
BottomNavigationState bottomNavigationState = (BottomNavigationState) state; | |
mLastSelection = bottomNavigationState.lastSelection; | |
dispatchRestoredState(); | |
super.onRestoreInstanceState(bottomNavigationState.getSuperState()); | |
} | |
private void dispatchRestoredState() { | |
if (mLastSelection != 0) { //Since the first item is always selected by the default implementation, dont waste time | |
setSelectedItem(mLastSelection); | |
} | |
} | |
private View getMenuItemView(int position) { | |
View bottomItem = mBottomItemsHolder.getChildAt(position); | |
if (bottomItem instanceof MenuView.ItemView) { | |
return bottomItem; | |
} | |
return null; | |
} | |
private int findSelectedItem() { | |
int itemCount = getMenu().size(); | |
for (int i = 0; i < itemCount; i++) { | |
View bottomItem = mBottomItemsHolder.getChildAt(i); | |
if (bottomItem instanceof MenuView.ItemView) { | |
MenuItemImpl itemData = ((MenuView.ItemView) bottomItem).getItemData(); | |
if (itemData.isChecked()) return i; | |
} | |
} | |
return INVALID_POSITION; | |
} | |
@Override | |
protected void onFinishInflate() { | |
super.onFinishInflate(); | |
mBottomItemsHolder = (ViewGroup) getChildAt(0); | |
updateShadowBounds(); | |
//This sucks. | |
MarginLayoutParams layoutParams = (MarginLayoutParams) mBottomItemsHolder.getLayoutParams(); | |
layoutParams.topMargin = (mShadowElevation + 2) / 2; | |
} | |
static class BottomNavigationState extends BaseSavedState { | |
public int lastSelection; | |
@RequiresApi(api = Build.VERSION_CODES.N) | |
public BottomNavigationState(Parcel in, ClassLoader loader) { | |
super(in, loader); | |
lastSelection = in.readInt(); | |
} | |
public BottomNavigationState(Parcelable superState) { | |
super(superState); | |
} | |
@Override | |
public void writeToParcel(@NonNull Parcel dest, int flags) { | |
super.writeToParcel(dest, flags); | |
dest.writeInt(lastSelection); | |
} | |
public static final Parcelable.Creator<NavigationView.SavedState> CREATOR | |
= ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<NavigationView.SavedState>() { | |
@Override | |
public NavigationView.SavedState createFromParcel(Parcel parcel, ClassLoader loader) { | |
return new NavigationView.SavedState(parcel, loader); | |
} | |
@Override | |
public NavigationView.SavedState[] newArray(int size) { | |
return new NavigationView.SavedState[size]; | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I just ran into the same problem - pretty sure the the issue is that the
BottomNavigationState
CREATOR
methods are returning newNavigationView.SavedState
objects....so the
BottomNavigationState
constructor is never called. Which skips the parcelreadInt
, which causes things to break down the line when some other view tries to read things in from the parcel.Changing the
CREATOR
toParcelable.Creator<BottomNavigationState>
seems work for me:Unless there is a reason that I don't see for using
NavigationView.SavedState
?