Skip to content

Instantly share code, notes, and snippets.

@NikolaDespotoski
Last active February 13, 2023 23:51
Show Gist options
  • Save NikolaDespotoski/c426abe41d98d5c2f68621c85532ea88 to your computer and use it in GitHub Desktop.
Save NikolaDespotoski/c426abe41d98d5c2f68621c85532ea88 to your computer and use it in GitHub Desktop.
Saving and restoring BottomNavigationView state
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];
}
});
}
}
@PaulWoitaschek
Copy link

You should treat the mShadowElevation as dp. Right now you treat it as px.

@NikolaDespotoski
Copy link
Author

NikolaDespotoski commented Nov 5, 2016

@PaulWoitaschek Actually, that was my first intention, but since I am trying to mimic upward shadow of 8dp, 8dp will make the shadow drawable height too big.

@temirfe
Copy link

temirfe commented Nov 22, 2016

can you provide usage example, please?

@adroitandroid
Copy link

This is useful, thanks! Just a small bit, setShadowVisible should set the global before using it.

@BenoitDuffez
Copy link

Seems like the theme is not working as expected, because of the setBackgroundColor() call in init().

Other than that, it's perfect, thanks!

@BenoitDuffez
Copy link

Also, there is a problem with the instance state save. Try to run this code with a killed saved instance state, insta-crash. Do this by checking the "do not retain activities" in the dev settings on the phone.
I would submit a patch if I knew why this happens! Here's the exception:

java.lang.RuntimeException: Parcel android.os.Parcel@46b99b7: Unmarshalling unknown type code 2131558549 at offset 1204

@NikolaDespotoski
Copy link
Author

@BenoitDuffez Can you provide entire stacktrace of the crash?

@cyoungberg
Copy link

I just ran into the same problem - pretty sure the the issue is that the BottomNavigationState CREATOR methods are returning new NavigationView.SavedState objects.

...so the BottomNavigationState constructor is never called. Which skips the parcel readInt, which causes things to break down the line when some other view tries to read things in from the parcel.

Changing the CREATOR to Parcelable.Creator<BottomNavigationState> seems work for me:


   static class BottomNavigationState extends BaseSavedState {
        public int lastSelection;

        private BottomNavigationState(Parcel in) {
            super(in);
            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<BottomNavigationState> CREATOR
                = new Parcelable.Creator<BottomNavigationState>() {
            public BottomNavigationState createFromParcel(Parcel in) {
                return new BottomNavigationState(in);
            }

            public BottomNavigationState[] newArray(int size) {
                return new BottomNavigationState[size];
            }
        };
    }

Unless there is a reason that I don't see for using NavigationView.SavedState ?

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