travisc 0f29cbf042 Make widget its own package, change lib into a pure export package.
This also moves expandable and transformation into their own packages, as was originally intended (but they had resources, which was problematic at that time). CoordinatorLayout has been moved out of the typical source root to ease its later deletion (when it has made it into core-ui).

PiperOrigin-RevId: 180728823
2018-01-11 11:08:30 -05:00

597 lines
22 KiB
Java

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.design.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.BoolRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.IdRes;
import android.support.annotation.IntegerRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.bottomnavigation.LabelVisibilityMode;
import android.support.design.bottomnavigation.ShiftingMode;
import android.support.design.internal.BottomNavigationMenu;
import android.support.design.internal.BottomNavigationMenuView;
import android.support.design.internal.BottomNavigationPresenter;
import android.support.design.theme.ThemeUtils;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.AbsSavedState;
import android.support.v4.view.ViewCompat;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.view.SupportMenuInflater;
import android.support.v7.view.menu.MenuBuilder;
import android.support.v7.widget.TintTypedArray;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
/**
* Represents a standard bottom navigation bar for application. It is an implementation of <a
* href="https://material.google.com/components/bottom-navigation.html">material design bottom
* navigation</a>.
*
* <p>Bottom navigation bars make it easy for users to explore and switch between top-level views in
* a single tap. They should be used when an application has three to five top-level destinations.
*
* <p>The bar can disappear on scroll, based on {@link HideBottomViewOnScrollBehavior}, when it is
* placed within a {@link CoordinatorLayout} and one of the children within the {@link
* CoordinatorLayout} is scrolled. This behavior is only set if the {@code layout_behavior} property
* is set to {@link HideBottomViewOnScrollBehavior}.
*
* <p>The bar contents can be populated by specifying a menu resource file. Each menu item title,
* icon and enabled state will be used for displaying bottom navigation bar items. Menu items can
* also be used for programmatically selecting which destination is currently active. It can be done
* using {@code MenuItem#setChecked(true)}
*
* <pre>
* layout resource file:
* &lt;android.support.design.widget.BottomNavigationView
* xmlns:android="http://schemas.android.com/apk/res/android"
* xmlns:app="http://schema.android.com/apk/res/res-auto"
* android:id="@+id/navigation"
* android:layout_width="match_parent"
* android:layout_height="56dp"
* android:layout_gravity="start"
* app:menu="@menu/my_navigation_items" /&gt;
*
* res/menu/my_navigation_items.xml:
* &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;item android:id="@+id/action_search"
* android:title="@string/menu_search"
* android:icon="@drawable/ic_search" /&gt;
* &lt;item android:id="@+id/action_settings"
* android:title="@string/menu_settings"
* android:icon="@drawable/ic_add" /&gt;
* &lt;item android:id="@+id/action_navigation"
* android:title="@string/menu_navigation"
* android:icon="@drawable/ic_action_navigation_menu" /&gt;
* &lt;/menu&gt;
* </pre>
*/
public class BottomNavigationView extends FrameLayout {
private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
private static final int MENU_PRESENTER_ID = 1;
private final MenuBuilder mMenu;
private final BottomNavigationMenuView mMenuView;
private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();
private MenuInflater mMenuInflater;
private OnNavigationItemSelectedListener mSelectedListener;
private OnNavigationItemReselectedListener mReselectedListener;
public BottomNavigationView(Context context) {
this(context, null);
}
public BottomNavigationView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
// Create the menu
mMenu = new BottomNavigationMenu(context);
mMenuView = new BottomNavigationMenuView(context);
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
mMenuView.setLayoutParams(params);
mPresenter.setBottomNavigationMenuView(mMenuView);
mPresenter.setId(MENU_PRESENTER_ID);
mMenuView.setPresenter(mPresenter);
mMenu.addMenuPresenter(mPresenter);
mPresenter.initForMenu(getContext(), mMenu);
// Custom attributes
TintTypedArray a =
TintTypedArray.obtainStyledAttributes(
context,
attrs,
R.styleable.BottomNavigationView,
defStyleAttr,
R.style.Widget_Design_BottomNavigationView);
if (a.hasValue(R.styleable.BottomNavigationView_itemIconTint)) {
mMenuView.setIconTintList(a.getColorStateList(R.styleable.BottomNavigationView_itemIconTint));
} else {
mMenuView.setIconTintList(createDefaultColorStateList(android.R.attr.textColorSecondary));
}
if (a.hasValue(R.styleable.BottomNavigationView_itemTextColor)) {
mMenuView.setItemTextColor(
a.getColorStateList(R.styleable.BottomNavigationView_itemTextColor));
} else {
mMenuView.setItemTextColor(createDefaultColorStateList(android.R.attr.textColorSecondary));
}
if (a.hasValue(R.styleable.BottomNavigationView_elevation)) {
ViewCompat.setElevation(
this, a.getDimensionPixelSize(R.styleable.BottomNavigationView_elevation, 0));
}
if (a.hasValue(R.styleable.BottomNavigationView_shiftingMode)) {
@ShiftingMode
int shiftingMode =
a.getInt(R.styleable.BottomNavigationView_shiftingMode, ShiftingMode.SHIFTING_MODE_AUTO);
mMenuView.setShiftingMode(shiftingMode);
}
setLabelVisibilityMode(
a.getInteger(
R.styleable.BottomNavigationView_labelVisibilityMode,
LabelVisibilityMode.LABEL_VISIBILITY_LEGACY));
setItemHorizontalTranslation(
a.getBoolean(R.styleable.BottomNavigationView_itemHorizontalTranslation, true));
int itemBackground = a.getResourceId(R.styleable.BottomNavigationView_itemBackground, 0);
mMenuView.setItemBackgroundRes(itemBackground);
if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
}
a.recycle();
addView(mMenuView, params);
if (Build.VERSION.SDK_INT < 21) {
addCompatibilityTopDivider(context);
}
mMenu.setCallback(
new MenuBuilder.Callback() {
@Override
public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) {
mReselectedListener.onNavigationItemReselected(item);
return true; // item is already selected
}
return mSelectedListener != null && !mSelectedListener.onNavigationItemSelected(item);
}
@Override
public void onMenuModeChange(MenuBuilder menu) {}
});
}
/**
* Set a listener that will be notified when a bottom navigation item is selected. This listener
* will also be notified when the currently selected item is reselected, unless an {@link
* OnNavigationItemReselectedListener} has also been set.
*
* @param listener The listener to notify
* @see #setOnNavigationItemReselectedListener(OnNavigationItemReselectedListener)
*/
public void setOnNavigationItemSelectedListener(
@Nullable OnNavigationItemSelectedListener listener) {
mSelectedListener = listener;
}
/**
* Set a listener that will be notified when the currently selected bottom navigation item is
* reselected. This does not require an {@link OnNavigationItemSelectedListener} to be set.
*
* @param listener The listener to notify
* @see #setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener)
*/
public void setOnNavigationItemReselectedListener(
@Nullable OnNavigationItemReselectedListener listener) {
mReselectedListener = listener;
}
/** Returns the {@link Menu} instance associated with this bottom navigation bar. */
@NonNull
public Menu getMenu() {
return mMenu;
}
/**
* Inflate a menu resource into this navigation view.
*
* <p>Existing items in the menu will not be modified or removed.
*
* @param resId ID of a menu resource to inflate
*/
public void inflateMenu(int resId) {
mPresenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, mMenu);
mPresenter.setUpdateSuspended(false);
mPresenter.updateMenuView(true);
}
/** @return The maximum number of items that can be shown in BottomNavigationView. */
public int getMaxItemCount() {
return BottomNavigationMenu.MAX_ITEM_COUNT;
}
/**
* Returns the tint which is applied to our menu items' icons.
*
* @see #setItemIconTintList(ColorStateList)
* @attr ref R.styleable#BottomNavigationView_itemIconTint
*/
@Nullable
public ColorStateList getItemIconTintList() {
return mMenuView.getIconTintList();
}
/**
* Set the tint which is applied to our menu items' icons.
*
* @param tint the tint to apply.
* @attr ref R.styleable#BottomNavigationView_itemIconTint
*/
public void setItemIconTintList(@Nullable ColorStateList tint) {
mMenuView.setIconTintList(tint);
}
/**
* Returns colors used for the different states (normal, selected, focused, etc.) of the menu item
* text.
*
* @see #setItemTextColor(ColorStateList)
* @return the ColorStateList of colors used for the different states of the menu items text.
* @attr ref R.styleable#BottomNavigationView_itemTextColor
*/
@Nullable
public ColorStateList getItemTextColor() {
return mMenuView.getItemTextColor();
}
/**
* Set the colors to use for the different states (normal, selected, focused, etc.) of the menu
* item text.
*
* @see #getItemTextColor()
* @attr ref R.styleable#BottomNavigationView_itemTextColor
*/
public void setItemTextColor(@Nullable ColorStateList textColor) {
mMenuView.setItemTextColor(textColor);
}
/**
* Returns the background resource of the menu items.
*
* @see #setItemBackgroundResource(int)
* @attr ref R.styleable#BottomNavigationView_itemBackground
*/
@DrawableRes
public int getItemBackgroundResource() {
return mMenuView.getItemBackgroundRes();
}
/**
* Set the background of our menu items to the given resource.
*
* @param resId The identifier of the resource.
* @attr ref R.styleable#BottomNavigationView_itemBackground
*/
public void setItemBackgroundResource(@DrawableRes int resId) {
mMenuView.setItemBackgroundRes(resId);
}
/**
* Returns the currently selected menu item ID, or zero if there is no menu.
*
* @see #setSelectedItemId(int)
*/
@IdRes
public int getSelectedItemId() {
return mMenuView.getSelectedItemId();
}
/**
* Set the selected menu item ID. This behaves the same as tapping on an item.
*
* @param itemId The menu item ID. If no item has this ID, the current selection is unchanged.
* @see #getSelectedItemId()
*/
public void setSelectedItemId(@IdRes int itemId) {
MenuItem item = mMenu.findItem(itemId);
if (item != null) {
if (!mMenu.performItemAction(item, mPresenter, 0)) {
item.setChecked(true);
}
}
}
/**
* Returns the current shifting mode flag used by this {@link BottomNavigationView}.
*
* @attr ref android.support.design.R.styleable#BottomNavigationView_shiftingMode
* @see #setShiftingMode(int)
* @deprecated use {@link BottomNavigationView#setLabelVisibilityMode} instead
*/
@Deprecated
@ShiftingMode
public int getShiftingMode() {
return mMenuView.getShiftingMode();
}
/**
* Set shifting mode flag for this {@link BottomNavigationView}. Enabling shifting mode hides the
* text labels of menu items in this {@link BottomNavigationView} unless the menu item is
* selected. If this flag is set to {@link ShiftingMode#SHIFTING_MODE_OFF}, this menu will not
* have shifting behavior even if it has more than 3 children. If this flag is set to {@link
* ShiftingMode#SHIFTING_MODE_ON}, this menu will have shifting behavior for any number of
* children. If this flag is set to {@link ShiftingMode#SHIFTING_MODE_AUTO} this menu will have
* shifting behavior only if it has 3 or more children.
*
* @param shiftingMode one of {@link ShiftingMode#SHIFTING_MODE_OFF}, {@link
* ShiftingMode#SHIFTING_MODE_ON}, or {@link ShiftingMode#SHIFTING_MODE_AUTO}
* @attr ref android.support.design.R.styleable#BottomNavigationView_shiftingMode
* @see <a
* href="https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-specs">Material
* Design guidelines</a>
* @see #getShiftingMode()
* @deprecated use {@link BottomNavigationView#setLabelVisibilityMode} instead
*/
@Deprecated
public void setShiftingMode(@ShiftingMode int shiftingMode) {
if (mMenuView.getShiftingMode() != shiftingMode) {
mMenuView.setShiftingMode(shiftingMode);
mPresenter.updateMenuView(false /* cleared */);
}
}
/**
* Sets the navigation items' label visibility mode.
*
* <p>The label is either always shown, never shown, or only shown when activated. Also supports
* legacy mode, which uses {@link ShiftingMode} to decide whether the label should be shown.
*
* @attr ref android.support.design.R.styleable#BottomNavigationView_labelVisibilityMode
* @param labelVisibilityMode mode which decides whether or not the label should be shown. Can be
* one of {@link LabelVisibilityMode#LABEL_VISIBILITY_LEGACY}, {@link
* LabelVisibilityMode#LABEL_VISIBILITY_SELECTED}, {@link
* LabelVisibilityMode#LABEL_VISIBILITY_LABELED}, or {@link
* LabelVisibilityMode#LABEL_VISIBILITY_UNLABELED}
* @see #getLabelVisibilityMode()
*/
public void setLabelVisibilityMode(@LabelVisibilityMode int labelVisibilityMode) {
if (mMenuView.getLabelVisibilityMode() != labelVisibilityMode) {
mMenuView.setLabelVisibilityMode(labelVisibilityMode);
mPresenter.updateMenuView(false);
}
}
/**
* Sets the navigation items' label visibility mode using a resource ID.
*
* <p>The label is either always shown, never shown, or only shown when activated. Also supports
* legacy mode, which uses {@link ShiftingMode} to decide whether the label should be shown.
*
* @attr ref android.support.design.R.styleable#BottomNavigationView_labelVisibilityMode
* @param labelVisibilityModeId resource ID for the mode which decides whether or not the label
* should be shown. Can be one of {@link LabelVisibilityMode#LABEL_VISIBILITY_LEGACY}, {@link
* LabelVisibilityMode#LABEL_VISIBILITY_SELECTED}, {@link
* LabelVisibilityMode#LABEL_VISIBILITY_LABELED}, or {@link
* LabelVisibilityMode#LABEL_VISIBILITY_UNLABELED}
* @see #getLabelVisibilityMode()
*/
public void setLabelVisibilityModeResource(@IntegerRes int labelVisibilityModeId) {
setLabelVisibilityMode(getContext().getResources().getInteger(labelVisibilityModeId));
}
/**
* Returns the current label visibility mode used by this {@link BottomNavigationView}.
*
* @attr ref android.support.design.R.styleable#BottomNavigationView_labelVisibilityMode
* @see #setLabelVisibilityMode(int)
*/
@LabelVisibilityMode
public int getLabelVisibilityMode() {
return mMenuView.getLabelVisibilityMode();
}
/**
* Sets whether the menu items horizontally translate when in shifting mode.
*
* @param itemHorizontalTranslation whether the items horizontally translate in shifting mode
* @see #getItemHorizontalTranslation()
*/
public void setItemHorizontalTranslation(boolean itemHorizontalTranslation) {
if (mMenuView.getItemHorizontalTranslation() != itemHorizontalTranslation) {
mMenuView.setItemHorizontalTranslation(itemHorizontalTranslation);
mPresenter.updateMenuView(false);
}
}
/**
* Sets whether the menu items horizontally translate when in shifting mode using a resource ID.
*
* @param itemHorizontalTranslation whether the items horizontally translate in shifting mode
* @see #getItemHorizontalTranslation()
*/
public void setItemHorizontalTranslation(@BoolRes int itemHorizontalTranslation) {
setItemHorizontalTranslation(getContext().getResources().getBoolean(itemHorizontalTranslation));
}
/**
* Returns whether the menu items horizontally translate in shifting mode.
*
* @return whether the menu items horizontally translate in shifting mode
* @see #setItemHorizontalTranslation(boolean)
*/
public boolean getItemHorizontalTranslation() {
return mMenuView.getItemHorizontalTranslation();
}
/** Listener for handling selection events on bottom navigation items. */
public interface OnNavigationItemSelectedListener {
/**
* Called when an item in the bottom navigation menu is selected.
*
* @param item The selected item
* @return true to display the item as the selected item and false if the item should not be
* selected. Consider setting non-selectable items as disabled preemptively to make them
* appear non-interactive.
*/
boolean onNavigationItemSelected(@NonNull MenuItem item);
}
/** Listener for handling reselection events on bottom navigation items. */
public interface OnNavigationItemReselectedListener {
/**
* Called when the currently selected item in the bottom navigation menu is selected again.
*
* @param item The selected item
*/
void onNavigationItemReselected(@NonNull MenuItem item);
}
private void addCompatibilityTopDivider(Context context) {
View divider = new View(context);
divider.setBackgroundColor(
ContextCompat.getColor(context, R.color.design_bottom_navigation_shadow_color));
FrameLayout.LayoutParams dividerParams =
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
getResources().getDimensionPixelSize(R.dimen.design_bottom_navigation_shadow_height));
divider.setLayoutParams(dividerParams);
addView(divider);
}
private MenuInflater getMenuInflater() {
if (mMenuInflater == null) {
mMenuInflater = new SupportMenuInflater(getContext());
}
return mMenuInflater;
}
private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
final TypedValue value = new TypedValue();
if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
return null;
}
ColorStateList baseColor = AppCompatResources.getColorStateList(getContext(), value.resourceId);
if (!getContext()
.getTheme()
.resolveAttribute(android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
return null;
}
int colorPrimary = value.data;
int defaultColor = baseColor.getDefaultColor();
return new ColorStateList(
new int[][] {DISABLED_STATE_SET, CHECKED_STATE_SET, EMPTY_STATE_SET},
new int[] {
baseColor.getColorForState(DISABLED_STATE_SET, defaultColor), colorPrimary, defaultColor
});
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.menuPresenterState = new Bundle();
mMenu.savePresenterStates(savedState.menuPresenterState);
return savedState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
mMenu.restorePresenterStates(savedState.menuPresenterState);
}
static class SavedState extends AbsSavedState {
Bundle menuPresenterState;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
readFromParcel(source, loader);
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeBundle(menuPresenterState);
}
private void readFromParcel(Parcel in, ClassLoader loader) {
menuPresenterState = in.readBundle(loader);
}
public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}