Initial implementation for BottomNavigationView widget.
Bug: 27675079
Change-Id: Ic24dfa979557f5af06294c61da5d74821254a583
diff --git a/api/current.txt b/api/current.txt
index 4b9459e..1fe440a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -307,6 +307,24 @@
method public boolean onDependentViewChanged(android.support.design.widget.CoordinatorLayout, android.view.View, android.view.View);
}
+ public class BottomNavigationView extends android.widget.FrameLayout {
+ ctor public BottomNavigationView(android.content.Context);
+ ctor public BottomNavigationView(android.content.Context, android.util.AttributeSet);
+ ctor public BottomNavigationView(android.content.Context, android.util.AttributeSet, int);
+ method public android.content.res.ColorStateList getItemIconTintList();
+ method public android.content.res.ColorStateList getItemTextColor();
+ method public android.view.Menu getMenu();
+ method public void inflateMenu(int);
+ method public void setItemBackgroundResource(int);
+ method public void setItemIconTintList(android.content.res.ColorStateList);
+ method public void setItemTextColor(android.content.res.ColorStateList);
+ method public void setOnNavigationItemSelectedListener(android.support.design.widget.BottomNavigationView.OnNavigationItemSelectedListener);
+ }
+
+ public static abstract interface BottomNavigationView.OnNavigationItemSelectedListener {
+ method public abstract boolean onNavigationItemSelected(android.view.MenuItem);
+ }
+
public class BottomSheetBehavior extends android.support.design.widget.CoordinatorLayout.Behavior {
ctor public BottomSheetBehavior();
ctor public BottomSheetBehavior(android.content.Context, android.util.AttributeSet);
diff --git a/design/res-public/values/public_styles.xml b/design/res-public/values/public_styles.xml
index a7c0af6..0dcde45 100644
--- a/design/res-public/values/public_styles.xml
+++ b/design/res-public/values/public_styles.xml
@@ -24,6 +24,7 @@
<public type="style" name="TextAppearance.Design.Snackbar.Message"/>
<public type="style" name="TextAppearance.Design.Tab"/>
<public type="style" name="Theme.Design"/>
+ <public type="style" name="Theme.Design.BottomNavigationView"/>
<public type="style" name="Theme.Design.BottomSheetDialog"/>
<public type="style" name="Theme.Design.Light"/>
<public type="style" name="Theme.Design.Light.BottomSheetDialog"/>
diff --git a/design/res/drawable-v21/design_bottom_navigation_item_background.xml b/design/res/drawable-v21/design_bottom_navigation_item_background.xml
new file mode 100644
index 0000000..f30f08b
--- /dev/null
+++ b/design/res/drawable-v21/design_bottom_navigation_item_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?attr/colorPrimary" />
\ No newline at end of file
diff --git a/design/res/drawable/design_bottom_navigation_item_background.xml b/design/res/drawable/design_bottom_navigation_item_background.xml
new file mode 100644
index 0000000..7674f42
--- /dev/null
+++ b/design/res/drawable/design_bottom_navigation_item_background.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="#ff0000"/>
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="#ffffff"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/design/res/layout/design_bottom_navigation_item.xml b/design/res/layout/design_bottom_navigation_item.xml
new file mode 100644
index 0000000..7f93eba
--- /dev/null
+++ b/design/res/layout/design_bottom_navigation_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:duplicateParentState="true" />
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/design_bottom_navigation_text_size"
+ android:gravity="center"
+ android:duplicateParentState="true" />
+</merge>
\ No newline at end of file
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index fc233da..d56cd88 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -397,5 +397,12 @@
<attr name="textColorError" format="color" />
</declare-styleable>
-</resources>
+ <declare-styleable name="BottomNavigationView">
+ <!-- The menu resource to inflate and populate items from. -->
+ <attr name="menu"/>
+ <attr name="itemIconTint"/>
+ <attr name="itemTextColor"/>
+ <attr name="itemBackground"/>
+ </declare-styleable>
+</resources>
diff --git a/design/res/values/dimens.xml b/design/res/values/dimens.xml
index 604ac52..6439b57e3 100644
--- a/design/res/values/dimens.xml
+++ b/design/res/values/dimens.xml
@@ -58,4 +58,14 @@
<dimen name="design_bottom_sheet_modal_elevation">16dp</dimen>
<dimen name="design_bottom_sheet_modal_peek_height">256dp</dimen>
+ <dimen name="design_bottom_navigation_height">56dp</dimen>
+ <dimen name="design_bottom_navigation_text_size">12sp</dimen>
+ <dimen name="design_bottom_navigation_active_text_size">14sp</dimen>
+ <dimen name="design_bottom_navigation_top_padding">8dp</dimen>
+ <dimen name="design_bottom_navigation_active_top_padding">6dp</dimen>
+ <dimen name="design_bottom_navigation_horizontal_padding">12dp</dimen>
+ <dimen name="design_bottom_navigation_bottom_padding">8dp</dimen>
+ <dimen name="design_bottom_navigation_item_max_width">96dp</dimen>
+ <dimen name="design_bottom_navigation_active_item_max_width">168dp</dimen>
+
</resources>
diff --git a/design/res/values/styles.xml b/design/res/values/styles.xml
index b25646c..ccc310e 100644
--- a/design/res/values/styles.xml
+++ b/design/res/values/styles.xml
@@ -42,6 +42,10 @@
<item name="tabMode">fixed</item>
</style>
+ <style name="Widget.Design.BottomNavigationView" parent="">
+ <item name="itemBackground">?attr/selectableItemBackgroundBorderless</item>
+ </style>
+
<style name="Base.Widget.Design.TabLayout" parent="android:Widget">
<item name="tabMaxWidth">@dimen/design_tab_max_width</item>
<item name="tabIndicatorColor">?attr/colorAccent</item>
diff --git a/design/src/android/support/design/internal/BottomNavigationItemView.java b/design/src/android/support/design/internal/BottomNavigationItemView.java
new file mode 100644
index 0000000..0d7df1a
--- /dev/null
+++ b/design/src/android/support/design/internal/BottomNavigationItemView.java
@@ -0,0 +1,244 @@
+/*
+ * 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.internal;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.design.R;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.support.v7.view.menu.MenuItemImpl;
+import android.support.v7.view.menu.MenuView;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * @hide
+ */
+public class BottomNavigationItemView extends ForegroundLinearLayout implements MenuView.ItemView {
+ public static final int INVALID_ITEM_POSTION = -1;
+
+ private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
+ private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
+
+ private final int mHorizontalPadding;
+ private final int mBottomPadding;
+ private final int mTopPadding;
+ private final int mActiveTopPadding;
+ private final float mInactiveLabelSize;
+ private final float mActiveLabelSize;
+
+ private ImageView mIcon;
+ private TextView mLabel;
+ private int mItemPosition = INVALID_ITEM_POSTION;
+
+ private MenuItemImpl mItemData;
+
+ private ColorStateList mIconTint;
+ private ColorStateList mTextColor;
+
+ public BottomNavigationItemView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mHorizontalPadding = getResources().getDimensionPixelSize(
+ R.dimen.design_bottom_navigation_horizontal_padding);
+ mBottomPadding = getResources().getDimensionPixelSize(
+ R.dimen.design_bottom_navigation_bottom_padding);
+ mTopPadding = getResources().getDimensionPixelSize(
+ R.dimen.design_bottom_navigation_top_padding);
+ mActiveTopPadding = getResources().getDimensionPixelSize(
+ R.dimen.design_bottom_navigation_active_top_padding);
+ mInactiveLabelSize =
+ getResources().getDimension(R.dimen.design_bottom_navigation_text_size);
+ mActiveLabelSize =
+ getResources().getDimension(R.dimen.design_bottom_navigation_active_text_size);
+
+ setOrientation(VERTICAL);
+ setGravity(Gravity.CENTER);
+ LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
+ setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
+ mIcon = (ImageView) findViewById(R.id.icon);
+ mLabel = (TextView) findViewById(R.id.label);
+ }
+
+ @Override
+ public void initialize(MenuItemImpl itemData, int menuType) {
+ mItemData = itemData;
+ setCheckable(itemData.isCheckable());
+ setChecked(itemData.isChecked());
+ setEnabled(itemData.isEnabled());
+ setIcon(itemData.getIcon());
+ setTitle(itemData.getTitle());
+ }
+
+ public void setItemPosition(int position) {
+ mItemPosition = position;
+ }
+
+ public int getItemPosition() {
+ return mItemPosition;
+ }
+
+ @Override
+ public MenuItemImpl getItemData() {
+ return null;
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ mLabel.setText(title);
+ }
+
+ @Override
+ public void setCheckable(boolean checkable) {
+ refreshDrawableState();
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mItemData.setChecked(checked);
+ if (checked) {
+ mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mActiveLabelSize);
+ setPadding(mHorizontalPadding, mActiveTopPadding, mHorizontalPadding, mBottomPadding);
+ } else {
+ mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mInactiveLabelSize);
+ setPadding(mHorizontalPadding, mTopPadding, mHorizontalPadding, mBottomPadding);
+ }
+ refreshDrawableState();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mLabel.setEnabled(enabled);
+ mIcon.setEnabled(enabled);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(final int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mItemData != null && mItemData.isCheckable() && mItemData.isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ public void setShortcut(boolean showShortcut, char shortcutKey) {
+ }
+
+ @Override
+ public void setIcon(Drawable icon) {
+ if (icon != null) {
+ Drawable.ConstantState state = icon.getConstantState();
+ icon = DrawableCompat.wrap(state == null ? icon : state.newDrawable()).mutate();
+ DrawableCompat.setTintList(icon, mIconTint);
+ }
+ mIcon.setImageDrawable(icon);
+ }
+
+ @Override
+ public boolean prefersCondensedTitle() {
+ return false;
+ }
+
+ @Override
+ public boolean showsIcon() {
+ return true;
+ }
+
+ public void setIconTintList(ColorStateList tint) {
+ mIconTint = tint;
+ if (mItemData != null) {
+ // Update the icon so that the tint takes effect
+ setIcon(mItemData.getIcon());
+ }
+ mLabel.setTextColor(mIconTint);
+ }
+
+ public void setTextColor(ColorStateList color) {
+ mTextColor = color;
+ mLabel.setTextColor(color);
+ }
+
+ public void setItemBackground(int background) {
+ Drawable backgroundDrawable = background == 0
+ ? null : ContextCompat.getDrawable(getContext(), background);
+ setBackgroundDrawable(backgroundDrawable);
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public Animator getAnimator(boolean active) {
+ final float currentTextSize = mLabel.getTextSize();
+ final int currentTopPadding = getPaddingTop();
+
+ final float finalTextSize = active ? mActiveLabelSize : mInactiveLabelSize;
+ final int finalTopPadding = active ? mActiveTopPadding : mTopPadding;
+
+ if (currentTextSize == finalTextSize && currentTopPadding == finalTopPadding) {
+ return null;
+ }
+
+ // Grow or shrink the text of the tab.
+ ValueAnimator textAnimator = ValueAnimator.ofFloat(currentTextSize, finalTextSize);
+ textAnimator.setDuration(ACTIVE_ANIMATION_DURATION_MS);
+ textAnimator.setInterpolator(new LinearOutSlowInInterpolator());
+ textAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float animatedValue = (float) valueAnimator.getAnimatedValue();
+ mLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, animatedValue);
+ }
+ });
+
+ // Reduce or increase the padding top of the tab.
+ ValueAnimator paddingTopAnimator = ValueAnimator.ofInt(currentTopPadding, finalTopPadding);
+ paddingTopAnimator.setDuration(ACTIVE_ANIMATION_DURATION_MS);
+ paddingTopAnimator.setInterpolator(new LinearOutSlowInInterpolator());
+ paddingTopAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ int animatedValue = (int) valueAnimator.getAnimatedValue();
+ setPadding(mHorizontalPadding, animatedValue,
+ mHorizontalPadding, mBottomPadding);
+ }
+ });
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(textAnimator, paddingTopAnimator);
+ return set;
+ }
+}
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
new file mode 100644
index 0000000..a4df28f
--- /dev/null
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -0,0 +1,218 @@
+/*
+ * 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.internal;
+
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.design.R;
+import android.support.v4.util.Pools;
+import android.support.v7.view.menu.MenuBuilder;
+import android.support.v7.view.menu.MenuItemImpl;
+import android.support.v7.view.menu.MenuView;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+/**
+ * @hide
+ */
+public class BottomNavigationMenuView extends LinearLayout implements MenuView {
+ private final int mInactiveItemMaxWidth;
+ private final int mActiveItemMaxWidth;
+ private final OnClickListener mOnClickListener;
+ private static final Pools.Pool<BottomNavigationItemView> sItemPool =
+ new Pools.SynchronizedPool<>(5);
+
+ private BottomNavigationItemView[] mButtons;
+ private int mActiveButton = 0;
+ private ColorStateList mItemIconTint;
+ private ColorStateList mItemTextColor;
+ private int mItemBackgroundRes;
+
+ private BottomNavigationPresenter mPresenter;
+ private MenuBuilder mMenu;
+
+ public BottomNavigationMenuView(Context context) {
+ this(context, null);
+ }
+
+ public BottomNavigationMenuView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BottomNavigationMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setGravity(Gravity.CENTER);
+ setOrientation(HORIZONTAL);
+
+ mInactiveItemMaxWidth = getResources().getDimensionPixelSize(
+ R.dimen.design_bottom_navigation_item_max_width);
+ mActiveItemMaxWidth = getResources()
+ .getDimensionPixelSize(R.dimen.design_bottom_navigation_active_item_max_width);
+
+ mOnClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final int itemPosition = ((BottomNavigationItemView) v).getItemPosition();
+ activateNewButton(itemPosition);
+ }
+ };
+ }
+
+ @Override
+ public void initialize(MenuBuilder menu) {
+ mMenu = menu;
+ if (mMenu == null) return;
+ if (mMenu.size() > mActiveButton) {
+ mMenu.getItem(mActiveButton).setChecked(true);
+ }
+ }
+
+ @Override
+ public int getWindowAnimations() {
+ return 0;
+ }
+
+ public void setIconTintList(ColorStateList color) {
+ mItemIconTint = color;
+ if (mButtons == null) return;
+ for (BottomNavigationItemView item : mButtons) {
+ item.setIconTintList(color);
+ }
+ }
+
+ @Nullable
+ public ColorStateList getIconTintList() {
+ return mItemIconTint;
+ }
+
+ public void setItemTextColor(ColorStateList color) {
+ mItemTextColor = color;
+ if (mButtons == null) return;
+ for (BottomNavigationItemView item : mButtons) {
+ item.setTextColor(color);
+ }
+ }
+
+ public ColorStateList getItemTextColor() {
+ return mItemTextColor;
+ }
+
+ public void setItemBackgroundRes(int background) {
+ mItemBackgroundRes = background;
+ if (mButtons == null) return;
+ for (BottomNavigationItemView item : mButtons) {
+ item.setItemBackground(background);
+ }
+ }
+
+ public int getItemBackgroundRes() {
+ return mItemBackgroundRes;
+ }
+
+ public void setPresenter(BottomNavigationPresenter presenter) {
+ mPresenter = presenter;
+ }
+
+ public void buildMenuView() {
+ if (mButtons != null) {
+ for (BottomNavigationItemView item : mButtons) {
+ sItemPool.release(item);
+ }
+ }
+ removeAllViews();
+ mButtons = new BottomNavigationItemView[mMenu.size()];
+ for (int i = 0; i < mMenu.size(); i++) {
+ mPresenter.setUpdateSuspended(true);
+ mMenu.getItem(i).setCheckable(true);
+ mPresenter.setUpdateSuspended(false);
+ BottomNavigationItemView child = getNewItem();
+ mButtons[i] = child;
+ child.setIconTintList(mItemIconTint);
+ child.setTextColor(mItemTextColor);
+ child.setItemBackground(mItemBackgroundRes);
+ child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
+ child.setItemPosition(i);
+ child.setOnClickListener(mOnClickListener);
+ addView(child);
+ }
+ }
+
+ public void updateMenuView() {
+ final int menuSize = mMenu.size();
+ if (menuSize != mButtons.length) {
+ // The size has changed. Rebuild menu view from scratch.
+ buildMenuView();
+ return;
+ }
+ for (int i = 0; i < menuSize; i++) {
+ mPresenter.setUpdateSuspended(true);
+ mButtons[i].initialize((MenuItemImpl) mMenu.getItem(i), 0);
+ mPresenter.setUpdateSuspended(false);
+ }
+ }
+
+ private void activateNewButton(int newButton) {
+ if (mActiveButton == newButton) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(
+ mButtons[mActiveButton].getAnimator(false),
+ mButtons[newButton].getAnimator(true));
+ animatorSet.start();
+ }
+ mPresenter.setUpdateSuspended(true);
+ mButtons[mActiveButton].setChecked(false);
+ mButtons[newButton].setChecked(true);
+ mPresenter.setUpdateSuspended(false);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ // Manually force UI update since we cannot use animations.
+ mPresenter.updateMenuView(true);
+ }
+ mActiveButton = newButton;
+ }
+
+ public void updateOnSizeChange(int width) {
+ if (getChildCount() == 0) return;
+ int available = width / getChildCount();
+ int itemWidth = Math.min(available, mActiveItemMaxWidth);
+
+ for (int i = 0; i < mButtons.length; i++) {
+ ViewGroup.LayoutParams params = mButtons[i].getLayoutParams();
+ if (params.width == itemWidth) {
+ continue;
+ }
+ params.width = itemWidth;
+ params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ mButtons[i].setLayoutParams(params);
+ }
+ }
+
+ private BottomNavigationItemView getNewItem() {
+ BottomNavigationItemView item = sItemPool.acquire();
+ if (item == null) {
+ item = new BottomNavigationItemView(getContext());
+ }
+ return item;
+ }
+}
diff --git a/design/src/android/support/design/internal/BottomNavigationPresenter.java b/design/src/android/support/design/internal/BottomNavigationPresenter.java
new file mode 100644
index 0000000..8dc0549
--- /dev/null
+++ b/design/src/android/support/design/internal/BottomNavigationPresenter.java
@@ -0,0 +1,103 @@
+/*
+ * 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.internal;
+
+import android.content.Context;
+import android.os.Parcelable;
+import android.support.v7.view.menu.MenuBuilder;
+import android.support.v7.view.menu.MenuItemImpl;
+import android.support.v7.view.menu.MenuPresenter;
+import android.support.v7.view.menu.MenuView;
+import android.support.v7.view.menu.SubMenuBuilder;
+import android.view.ViewGroup;
+
+/**
+ * @hide
+ */
+public class BottomNavigationPresenter implements MenuPresenter {
+ private MenuBuilder mMenu;
+ private BottomNavigationMenuView mMenuView;
+ private boolean mUpdateSuspended = false;
+
+ public void setBottomNavigationMenuView(BottomNavigationMenuView menuView) {
+ mMenuView = menuView;
+ }
+
+ @Override
+ public void initForMenu(Context context, MenuBuilder menu) {
+ mMenuView.initialize(mMenu);
+ mMenu = menu;
+ }
+
+ @Override
+ public MenuView getMenuView(ViewGroup root) {
+ return mMenuView;
+ }
+
+ @Override
+ public void updateMenuView(boolean cleared) {
+ if (mUpdateSuspended) return;
+ if (cleared) {
+ mMenuView.buildMenuView();
+ } else {
+ mMenuView.updateMenuView();
+ }
+ }
+
+ @Override
+ public void setCallback(Callback cb) {}
+
+ @Override
+ public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+ return false;
+ }
+
+ @Override
+ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {}
+
+ @Override
+ public boolean flagActionItems() {
+ return false;
+ }
+
+ @Override
+ public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+ return false;
+ }
+
+ @Override
+ public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+ return false;
+ }
+
+ @Override
+ public int getId() {
+ return -1;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ return null;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {}
+
+ public void setUpdateSuspended(boolean updateSuspended) {
+ mUpdateSuspended = updateSuspended;
+ }
+}
diff --git a/design/src/android/support/design/widget/BottomNavigationView.java b/design/src/android/support/design/widget/BottomNavigationView.java
new file mode 100644
index 0000000..1ccb6ff
--- /dev/null
+++ b/design/src/android/support/design/widget/BottomNavigationView.java
@@ -0,0 +1,287 @@
+/*
+ * 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.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.R;
+import android.support.design.internal.BottomNavigationMenuView;
+import android.support.design.internal.BottomNavigationPresenter;
+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.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+/**
+ * Represents a standard bottom navigation bar for application. It is an implementation of material
+ * design bottom navigation. See https://material.google.com/components/bottom-navigation.html
+ *
+ * Bottom navigation bars make it easy for users to explore and switch between top-level views in
+ * a single tap. It should be used when application has three to five top-level destinations.
+ *
+ * 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.
+ *
+ * <android.support.design.widget.BottomNavigationView
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:app="http://schemas.android.com/apk/res-auto"
+ * android:id="@+id/navigation"
+ * android:layout_width="wrap_content"
+ * android:layout_height="match_parent"
+ * android:layout_gravity="start"
+ * app:menu="@menu/my_navigation_items" />
+ */
+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 final MenuBuilder mMenu;
+ private final BottomNavigationMenuView mMenuView;
+ private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();
+ private MenuInflater mMenuInflater;
+
+ private OnNavigationItemSelectedListener mListener;
+
+ 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 MenuBuilder(context);
+
+ mMenuView = new BottomNavigationMenuView(context, attrs);
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ mMenuView.setLayoutParams(params);
+
+ mPresenter.setBottomNavigationMenuView(mMenuView);
+ mMenuView.setPresenter(mPresenter);
+ mMenu.addMenuPresenter(mPresenter);
+
+
+ // 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));
+ }
+
+ 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);
+
+ mMenu.setCallback(new MenuBuilder.Callback() {
+ @Override
+ public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+ return mListener != null && mListener.onNavigationItemSelected(item);
+ }
+
+ @Override
+ public void onMenuModeChange(MenuBuilder menu) {}
+ });
+ }
+
+ /**
+ * Set a listener that will be notified when a bottom navigation item is selected.
+ *
+ * @param listener The listener to notify
+ */
+ public void setOnNavigationItemSelectedListener(
+ @Nullable OnNavigationItemSelectedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // TODO(aurimas): move updateOnSizeChange to a different location that is less expensive.
+ mMenuView.updateOnSizeChange(MeasureSpec.getSize(widthMeasureSpec));
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ mMenuView.updateOnSizeChange(w);
+ }
+
+ /**
+ * 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.</p>
+ *
+ * @param resId ID of a menu resource to inflate
+ */
+ public void inflateMenu(int resId) {
+ mPresenter.setUpdateSuspended(true);
+ getMenuInflater().inflate(resId, mMenu);
+ mPresenter.initForMenu(getContext(), mMenu);
+ mPresenter.setUpdateSuspended(false);
+ mPresenter.updateMenuView(true);
+ }
+
+ /**
+ * 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 the tint which is applied to menu items' icons.
+ *
+ * @see #setItemTextColor(ColorStateList)
+ *
+ * @attr ref R.styleable#BottomNavigationView_itemTextColor
+ */
+ @Nullable
+ public ColorStateList getItemTextColor() {
+ return mMenuView.getItemTextColor();
+ }
+
+ /**
+ * Set the text color to be used on menu items.
+ *
+ * @see #getItemTextColor()
+ *
+ * @attr ref R.styleable#BottomNavigationView_itemTextColor
+ */
+ public void setItemTextColor(@Nullable ColorStateList textColor) {
+ mMenuView.setItemTextColor(textColor);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Listener for handling 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
+ */
+ public boolean onNavigationItemSelected(@NonNull MenuItem item);
+ }
+
+ 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
+ });
+ }
+}
diff --git a/samples/SupportDesignDemos/AndroidManifest.xml b/samples/SupportDesignDemos/AndroidManifest.xml
index 4d05bc6..ec97037 100644
--- a/samples/SupportDesignDemos/AndroidManifest.xml
+++ b/samples/SupportDesignDemos/AndroidManifest.xml
@@ -307,5 +307,14 @@
</intent-filter>
</activity>
+ <activity android:name=".widget.BottomNavigationViewUsage"
+ android:label="@string/design_bottom_navigation_view"
+ android:theme="@style/Theme.Design">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="com.example.android.support.design.SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
</application>
</manifest>
diff --git a/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml b/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml
new file mode 100644
index 0000000..7e66c79
--- /dev/null
+++ b/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Button
+ android:id="@+id/button_disable"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/bottomnavigation_disable"/>
+
+ <Button
+ android:id="@+id/button_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="50dp"
+ android:text="@string/bottomnavigation_add"/>
+
+
+ <android.support.design.widget.BottomNavigationView
+ android:id="@+id/bottom_navigation"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_gravity="bottom"
+ android:background="#eee"
+ app:menu="@menu/sample_bottom_menu"/>
+
+</FrameLayout>
diff --git a/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml b/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml
new file mode 100644
index 0000000..4294f80
--- /dev/null
+++ b/samples/SupportDesignDemos/res/menu/sample_bottom_menu.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 Google Inc.
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/action_search"
+ android:title="@string/menu_search"
+ android:icon="@drawable/ic_search"/>
+ <item android:id="@+id/action_settings"
+ android:title="@string/menu_settings"
+ android:icon="@drawable/ic_add"/>
+ <item android:id="@+id/action_navigation"
+ android:title="@string/tab_text"
+ android:icon="@drawable/ic_action_navigation_menu"/>
+</menu>
\ No newline at end of file
diff --git a/samples/SupportDesignDemos/res/values/strings.xml b/samples/SupportDesignDemos/res/values/strings.xml
index 8f21310..1040e0e 100644
--- a/samples/SupportDesignDemos/res/values/strings.xml
+++ b/samples/SupportDesignDemos/res/values/strings.xml
@@ -106,4 +106,8 @@
<string name="bottomsheet_show">Show</string>
<string name="item_n">Item %d</string>
+ <string name="design_bottom_navigation_view">Bottom navigation view</string>
+
+ <string name="bottomnavigation_disable">Disable item</string>
+ <string name="bottomnavigation_add">Add item</string>
</resources>
diff --git a/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
new file mode 100644
index 0000000..e19554d
--- /dev/null
+++ b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
@@ -0,0 +1,53 @@
+/*
+ * 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 com.example.android.support.design.widget;
+
+import android.os.Bundle;
+import android.support.design.R;
+import android.support.design.widget.BottomNavigationView;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+
+/**
+ * This demonstrates idiomatic usage of the bottom navigation widget.
+ */
+public class BottomNavigationViewUsage extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.design_bottom_navigation_view);
+ Button buttonDisable = (Button) findViewById(R.id.button_disable);
+ final BottomNavigationView bottom =
+ (BottomNavigationView) findViewById(R.id.bottom_navigation);
+ buttonDisable.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ bottom.getMenu().getItem(0).setEnabled(!bottom.getMenu().getItem(0).isEnabled());
+ }
+ });
+ Button buttonAdd = (Button) findViewById(R.id.button_add);
+ buttonAdd.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ MenuItem item = bottom.getMenu().add("Bananas");
+ item.setIcon(android.R.drawable.ic_lock_power_off);
+ }
+ });
+ }
+}