com.li.utils.ui.mdbottom.BottomNavigation.java Source code

Java tutorial

Introduction

Here is the source code for com.li.utils.ui.mdbottom.BottomNavigation.java

Source

/**
 * The MIT License (MIT)
 * <p/>
 * Copyright (c) 2016 Alessandro Crugnola
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
 * is furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.li.utils.ui.mdbottom;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.support.annotation.MenuRes;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

import rxop.li.com.rxoperation.R;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;

public class BottomNavigation extends FrameLayout implements OnItemClickListener {
    private static final String TAG = BottomNavigation.class.getSimpleName();

    @SuppressWarnings("checkstyle:staticvariablename")
    public static boolean DEBUG = false;

    static final int PENDING_ACTION_NONE = 0x0;
    static final int PENDING_ACTION_EXPANDED = 0x1;
    static final int PENDING_ACTION_COLLAPSED = 0x2;
    static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;

    private static final String WIDGET_PACKAGE_NAME;

    static {
        final Package pkg = BottomNavigation.class.getPackage();
        WIDGET_PACKAGE_NAME = pkg != null ? pkg.getName() : null;
    }

    static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] { BottomNavigation.class };

    /**
     * Current pending action (used inside the BottomBehavior instance)
     */
    private int mPendingAction = PENDING_ACTION_NONE;

    /**
     * This is the amount of space we have to cover in case there's a translucent navigation
     * enabled.
     */
    private int bottomInset;

    /**
     * This is the amount of space we have to cover in case there's a translucent status
     * enabled.
     */
    private int topInset;

    /**
     * This is the current view height. It does take into account the extra space
     * used in case we have to cover the navigation translucent area, and neither the shadow height.
     */
    private int defaultHeight;

    /**
     * Same as defaultHeight, but for tablet mode.
     */
    private int defaultWidth;

    /**
     * Shadow is created above the widget background. It simulates the
     * elevation.
     */
    private int shadowHeight;

    /**
     * Layout container used to create and manage the UI items.
     * It can be either Fixed or Shifting, based on the widget `mode`
     */
    private ItemsLayoutContainer itemsContainer;

    /**
     * This is where the color animation is happening
     */
    private View backgroundOverlay;

    /**
     * current menu
     */
    MenuParser.Menu menu;

    private MenuParser.Menu pendingMenu;

    /**
     * Default selected index.
     * After the items are populated changing this
     * won't have any effect
     */
    private int defaultSelectedIndex = 0;

    /**
     * View visible background color
     */
    private ColorDrawable backgroundDrawable;

    /**
     * Animation duration for the background color change
     */
    private long backgroundColorAnimation;

    /**
     * Optional typeface used for the items' text labels
     */
    SoftReference<Typeface> typeface;

    /**
     * Current BottomBehavior assigned from the CoordinatorLayout
     */
    private CoordinatorLayout.Behavior mBehavior;

    /**
     * Menu selection listener
     */
    private OnMenuItemSelectionListener listener;

    /**
     * The user defined layout_gravity
     */
    private int gravity;

    /**
     * View is attached
     */
    private boolean attached;

    private BadgeProvider badgeProvider;

    public BottomNavigation(final Context context) {
        this(context, null);
    }

    public BottomNavigation(final Context context, final AttributeSet attrs) {
        super(context, attrs);
        initialize(context, attrs, 0, 0);
    }

    public BottomNavigation(final Context context, final AttributeSet attrs, final int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize(context, attrs, defStyleAttr, 0);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public BottomNavigation(final Context context, final AttributeSet attrs, final int defStyleAttr,
            final int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initialize(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable parcelable = super.onSaveInstanceState();
        SavedState savedState = new SavedState(parcelable);

        if (null == menu) {
            savedState.selectedIndex = 0;
        } else {
            savedState.selectedIndex = Math.max(0, Math.min(getSelectedIndex(), menu.getItemsCount() - 1));
        }

        if (null != badgeProvider) {
            savedState.badgeBundle = badgeProvider.save();
        }

        return savedState;
    }

    @Override
    protected void onRestoreInstanceState(final Parcelable state) {
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());

        defaultSelectedIndex = savedState.selectedIndex;

        if (null != badgeProvider && null != savedState.badgeBundle) {
            badgeProvider.restore(savedState.badgeBundle);
        }
    }

    public BadgeProvider getBadgeProvider() {
        return badgeProvider;
    }

    private void initialize(final Context context, final AttributeSet attrs, final int defStyleAttr,
            final int defStyleRes) {
        typeface = new SoftReference<>(Typeface.DEFAULT);

        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BottomNavigation, defStyleAttr,
                defStyleRes);
        final int menuResId = array.getResourceId(R.styleable.BottomNavigation_bbn_entries, 0);
        pendingMenu = MenuParser.inflateMenu(context, menuResId);
        badgeProvider = parseBadgeProvider(this, context,
                array.getString(R.styleable.BottomNavigation_bbn_badgeProvider));
        array.recycle();

        backgroundColorAnimation = getResources().getInteger(R.integer.bbn_background_animation_duration);
        defaultSelectedIndex = 0;

        defaultHeight = getResources().getDimensionPixelSize(R.dimen.bbn_bottom_navigation_height);
        defaultWidth = getResources().getDimensionPixelSize(R.dimen.bbn_bottom_navigation_width);
        shadowHeight = getResources().getDimensionPixelOffset(R.dimen.bbn_top_shadow_height);

        // check if the bottom navigation is translucent
        //        if (!isInEditMode()) {
        //            final Activity activity = MiscUtils.getActivity(context);
        //            if (null != activity) {
        //                final SystemBarTintManager systembarTint = new SystemBarTintManager(activity);
        //                if (MiscUtils.hasTranslucentNavigation(activity)
        //                    && systembarTint.getConfig().isNavigationAtBottom()
        //                    && systembarTint.getConfig().hasNavigtionBar()) {
        //                    bottomInset = systembarTint.getConfig().getNavigationBarHeight();
        //                } else {
        //                    bottomInset = 0;
        //                }
        //                topInset = systembarTint.getConfig().getStatusBarHeight();
        //            }
        //        }

        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
        backgroundOverlay = new View(getContext());
        backgroundOverlay.setLayoutParams(params);
        addView(backgroundOverlay);
    }

    int getPendingAction() {
        return mPendingAction;
    }

    void resetPendingAction() {
        mPendingAction = PENDING_ACTION_NONE;
    }

    @Override
    public void setLayoutParams(final ViewGroup.LayoutParams params) {
        super.setLayoutParams(params);
    }

    private boolean isTablet(final int gravity) {
        return MiscUtils.isGravitiyLeft(gravity) || MiscUtils.isGravityRight(gravity);
    }

    @SuppressWarnings("unused")
    public void setSelectedIndex(final int position, final boolean animate) {
        if (null != itemsContainer) {
            setSelectedItemInternal(itemsContainer, ((ViewGroup) itemsContainer).getChildAt(position), position,
                    animate, false);
        } else {
            defaultSelectedIndex = position;
        }
    }

    @SuppressWarnings("unused")
    public int getSelectedIndex() {
        if (null != itemsContainer) {
            return itemsContainer.getSelectedIndex();
        }
        return -1;
    }

    @SuppressWarnings("unused")
    public void setExpanded(boolean expanded, boolean animate) {
        mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
                | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0);
        requestLayout();
    }

    public boolean isExpanded() {
        if (null != mBehavior && mBehavior instanceof BottomBehavior) {
            return ((BottomBehavior) mBehavior).isExpanded();
        }
        return false;
    }

    public void setOnMenuItemClickListener(final OnMenuItemSelectionListener listener) {
        this.listener = listener;
    }

    public void setMenuItems(@MenuRes final int menuResId) {
        defaultSelectedIndex = 0;
        if (isAttachedToWindow()) {
            setItems(MenuParser.inflateMenu(getContext(), menuResId));
            pendingMenu = null;
        } else {
            pendingMenu = MenuParser.inflateMenu(getContext(), menuResId);
        }
    }

    /**
     * Returns the current menu items count
     *
     * @return number of items in the current menu
     */
    public int getMenuItemCount() {
        if (null != menu) {
            return menu.getItemsCount();
        }
        return 0;
    }

    /**
     * Returns the id of the item at the specified position
     *
     * @param position the position inside the menu
     * @return the item ID
     */
    @IdRes
    public int getMenuItemId(final int position) {
        if (null != menu) {
            return menu.getItemAt(position).getId();
        }
        return 0;
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // log(TAG, INFO, "onMeasure: %d", gravity);

        if (MiscUtils.isGravityBottom(gravity)) {
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);

            if (widthMode == MeasureSpec.AT_MOST) {
                throw new IllegalArgumentException("layout_width must be equal to `match_parent`");
            }
            setMeasuredDimension(widthSize, defaultHeight + bottomInset + shadowHeight);

        } else if (MiscUtils.isGravitiyLeft(gravity) || MiscUtils.isGravityRight(gravity)) {
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

            if (heightMode == MeasureSpec.AT_MOST) {
                throw new IllegalArgumentException("layout_height must be equal to `match_parent`");
            }
            setMeasuredDimension(defaultWidth, heightSize);
        } else {
            throw new IllegalArgumentException(
                    "invalid layout_gravity. Only one start, end, left, right or bottom is allowed");
        }
    }

    @SuppressWarnings("unused")
    public int getNavigationHeight() {
        return defaultHeight;
    }

    @SuppressWarnings("unused")
    public int getNavigationWidth() {
        return defaultWidth;
    }

    @Override
    protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) getLayoutParams();
        marginLayoutParams.bottomMargin = -bottomInset;
    }

    public boolean isAttachedToWindow() {
        if (Build.VERSION.SDK_INT >= 19) {
            return super.isAttachedToWindow();
        }
        return attached;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        attached = true;

        final CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) getLayoutParams();
        this.gravity = GravityCompat.getAbsoluteGravity(params.gravity, ViewCompat.getLayoutDirection(this));
        initializeUI(gravity);

        if (null != pendingMenu) {
            setItems(pendingMenu);
            pendingMenu = null;
        }

        if (null == mBehavior) {
            if (CoordinatorLayout.LayoutParams.class.isInstance(params)) {
                mBehavior = params.getBehavior();

                if (isInEditMode()) {
                    return;
                }

                if (BottomBehavior.class.isInstance(mBehavior)) {
                    ((BottomBehavior) mBehavior).setLayoutValues(defaultHeight, bottomInset);
                } else if (TabletBehavior.class.isInstance(mBehavior)) {
                    final Activity activity = MiscUtils.getActivity(getContext());
                    boolean translucentStatus = MiscUtils.hasTranslucentStatusBar(activity);
                    ((TabletBehavior) mBehavior).setLayoutValues(defaultWidth, topInset, translucentStatus);
                }
            }
        }
    }

    public CoordinatorLayout.Behavior getBehavior() {
        if (null == mBehavior) {
            if (CoordinatorLayout.LayoutParams.class.isInstance(getLayoutParams())) {
                return ((CoordinatorLayout.LayoutParams) getLayoutParams()).getBehavior();
            }
        }
        return mBehavior;
    }

    private void setItems(MenuParser.Menu menu) {

        this.menu = menu;

        if (null != menu) {
            if (menu.getItemsCount() < 3 || menu.getItemsCount() > 5) {
                throw new IllegalArgumentException(
                        "BottomNavigation expects 3 to 5 items. " + menu.getItemsCount() + " found");
            }

            menu.setTabletMode(isTablet(gravity));

            initializeBackgroundColor(menu);
            initializeContainer(menu);
            initializeItems(menu);
        }

        requestLayout();
    }

    private void initializeUI(final int gravity) {
        final LayerDrawable layerDrawable;

        final boolean tablet = isTablet(gravity);
        final int elevation = getResources()
                .getDimensionPixelSize(!tablet ? R.dimen.bbn_elevation : R.dimen.bbn_elevation_tablet);
        final int bgResId = !tablet ? R.drawable.bbn_background
                : (MiscUtils.isGravityRight(gravity) ? R.drawable.bbn_background_tablet_right
                        : R.drawable.bbn_background_tablet_left);
        final int paddingBottom = !tablet ? shadowHeight : 0;

        // View elevation
        ViewCompat.setElevation(this, elevation);

        // Main background
        layerDrawable = (LayerDrawable) ContextCompat.getDrawable(getContext(), bgResId);
        layerDrawable.mutate();
        backgroundDrawable = (ColorDrawable) layerDrawable.findDrawableByLayerId(R.id.bbn_background);
        setBackground(layerDrawable);

        // Padding bottom
        setPadding(0, paddingBottom, 0, 0);
    }

    private void initializeBackgroundColor(final MenuParser.Menu menu) {

        final int color = menu.getBackground();
        backgroundDrawable.setColor(color);
    }

    private void initializeContainer(final MenuParser.Menu menu) {
        if (null != itemsContainer) {
            if (menu.isTablet() && !TabletLayout.class.isInstance(itemsContainer)) {
                removeView((View) itemsContainer);
                itemsContainer = null;
            } else if ((menu.isShifting() && !ShiftingLayout.class.isInstance(itemsContainer))
                    || (!menu.isShifting() && !FixedLayout.class.isInstance(itemsContainer))) {
                removeView((View) itemsContainer);
                itemsContainer = null;
            } else {
                itemsContainer.removeAll();
            }
        }

        if (null == itemsContainer) {
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                    menu.isTablet() ? defaultWidth : MATCH_PARENT, menu.isTablet() ? MATCH_PARENT : defaultHeight);

            if (menu.isTablet()) {
                itemsContainer = new TabletLayout(getContext());
            } else if (menu.isShifting()) {
                itemsContainer = new ShiftingLayout(getContext());
            } else {
                itemsContainer = new FixedLayout(getContext());
            }

            // force the layout manager ID
            ((View) itemsContainer).setId(R.id.bbn_layoutManager);
            itemsContainer.setLayoutParams(params);
            addView((View) itemsContainer);
        }
    }

    private void initializeItems(final MenuParser.Menu menu) {

        itemsContainer.setSelectedIndex(defaultSelectedIndex, false);
        itemsContainer.populate(menu);
        itemsContainer.setOnItemClickListener(this);

        if (menu.getItemAt(defaultSelectedIndex).hasColor()) {
            backgroundDrawable.setColor(menu.getItemAt(defaultSelectedIndex).getColor());
        }
    }

    @Override
    public void onItemClick(final ItemsLayoutContainer parent, final View view, final int index, boolean animate) {
        setSelectedItemInternal(parent, view, index, animate, true);
    }

    private void setSelectedItemInternal(final ItemsLayoutContainer layoutContainer, final View view,
            final int index, final boolean animate, final boolean fromUser) {

        final BottomNavigationItem item = menu.getItemAt(index);

        if (layoutContainer.getSelectedIndex() != index) {
            layoutContainer.setSelectedIndex(index, animate);

            if (item.hasColor() && !menu.isTablet()) {
                if (animate) {
                    MiscUtils.animate(this, view, backgroundOverlay, backgroundDrawable, item.getColor(),
                            backgroundColorAnimation);
                } else {
                    MiscUtils.switchColor(this, view, backgroundOverlay, backgroundDrawable, item.getColor());
                }
            }

            if (null != listener && fromUser) {
                listener.onMenuItemSelect(item.getId(), index);
            }

        } else {
            if (null != listener && fromUser) {
                listener.onMenuItemReselect(item.getId(), index);
            }
        }
    }

    public void setDefaultTypeface(final Typeface typeface) {
        this.typeface = new SoftReference<>(typeface);
    }

    public void setDefaultSelectedIndex(final int defaultSelectedIndex) {
        this.defaultSelectedIndex = defaultSelectedIndex;
    }

    public void invalidateBadge(final int itemId) {
        if (null != itemsContainer) {
            final BottomNavigationItemViewAbstract viewAbstract = (BottomNavigationItemViewAbstract) itemsContainer
                    .findViewById(itemId);
            if (null != viewAbstract) {
                viewAbstract.invalidateBadge();
            }
        }
    }

    static final ThreadLocal<Map<String, Constructor<BadgeProvider>>> S_CONSTRUCTORS = new ThreadLocal<>();

    static BadgeProvider parseBadgeProvider(final BottomNavigation navigation, final Context context,
            final String name) {

        if (TextUtils.isEmpty(name)) {
            return new BadgeProvider(navigation);
        }

        final String fullName;
        if (name.startsWith(".")) {
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME) ? (WIDGET_PACKAGE_NAME + '.' + name) : name;
        }

        try {
            Map<String, Constructor<BadgeProvider>> constructors = S_CONSTRUCTORS.get();
            if (constructors == null) {
                constructors = new HashMap<>();
                S_CONSTRUCTORS.set(constructors);
            }
            Constructor<BadgeProvider> c = constructors.get(fullName);
            if (c == null) {
                final Class<BadgeProvider> clazz = (Class<BadgeProvider>) Class.forName(fullName, true,
                        context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            return c.newInstance(navigation);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

    public interface OnMenuItemSelectionListener {
        void onMenuItemSelect(@IdRes final int itemId, final int position);

        void onMenuItemReselect(@IdRes final int itemId, final int position);
    }

    static class SavedState extends BaseSavedState {
        int selectedIndex;
        Bundle badgeBundle;

        public SavedState(Parcel in) {
            super(in);
            selectedIndex = in.readInt();
            badgeBundle = in.readBundle();
        }

        public SavedState(final Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(final Parcel out, final int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(selectedIndex);
            out.writeBundle(badgeBundle);
        }

        @Override
        public int describeContents() {
            return super.describeContents();
        }

        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

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