Adding shifting mode to BottomNavigationView.

This change adds a new mode to BottomNavigationView that
follow the https://material.google.com/components/bottom-navigation.html
spec.

Bug: 27675079

Change-Id: I868bc3c8cedd39c4e5a66c3c0ffbadff93bdf329
diff --git a/design/Android.mk b/design/Android.mk
index e9ba5b6..38ca592 100644
--- a/design/Android.mk
+++ b/design/Android.mk
@@ -101,7 +101,8 @@
     android-support-design-res \
     android-support-v4 \
     android-support-v7-appcompat \
-    android-support-v7-recyclerview
+    android-support-v7-recyclerview \
+    android-support-transition
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
@@ -115,7 +116,8 @@
     android-support-design-res \
     android-support-v4 \
     android-support-v7-appcompat \
-    android-support-v7-recyclerview
+    android-support-v7-recyclerview \
+    android-support-transition
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
diff --git a/design/AndroidManifest.xml b/design/AndroidManifest.xml
index 82f19db..d51186dde 100644
--- a/design/AndroidManifest.xml
+++ b/design/AndroidManifest.xml
@@ -14,7 +14,9 @@
      limitations under the License.
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
           package="android.support.design">
-    <uses-sdk android:minSdkVersion="9"/>
+    <uses-sdk android:minSdkVersion="9"
+              tools:overrideLibrary="android.support.transition"/>
     <application />
 </manifest>
diff --git a/design/base/android/support/design/internal/BottomNavigationAnimationHelperBase.java b/design/base/android/support/design/internal/BottomNavigationAnimationHelperBase.java
new file mode 100644
index 0000000..22501c1
--- /dev/null
+++ b/design/base/android/support/design/internal/BottomNavigationAnimationHelperBase.java
@@ -0,0 +1,25 @@
+/*
+ * 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.view.ViewGroup;
+
+class BottomNavigationAnimationHelperBase {
+    void beginDelayedTransition(ViewGroup view) {
+        // Do nothing.
+    }
+}
diff --git a/design/build.gradle b/design/build.gradle
index 4407ba9..8ae45d7 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -6,6 +6,7 @@
     compile project(':support-v4')
     compile project(':support-appcompat-v7')
     compile project(':support-recyclerview-v7')
+    compile project(':support-transition')
 
     androidTestCompile ("com.android.support.test:runner:${project.rootProject.ext.testRunnerVersion}") {
         exclude module: 'support-annotations'
diff --git a/design/ics/android/support/design/internal/BottomNavigationAnimationHelperIcs.java b/design/ics/android/support/design/internal/BottomNavigationAnimationHelperIcs.java
new file mode 100644
index 0000000..6681d0b
--- /dev/null
+++ b/design/ics/android/support/design/internal/BottomNavigationAnimationHelperIcs.java
@@ -0,0 +1,42 @@
+/*
+ * 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.support.transition.AutoTransition;
+import android.support.transition.TransitionManager;
+import android.support.transition.TransitionSet;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.view.ViewGroup;
+
+class BottomNavigationAnimationHelperIcs extends BottomNavigationAnimationHelperBase {
+    private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
+
+    private final TransitionSet mSet;
+
+    BottomNavigationAnimationHelperIcs() {
+        mSet = new AutoTransition();
+        mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
+        mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
+        mSet.setInterpolator(new FastOutSlowInInterpolator());
+        TextScale textScale = new TextScale();
+        mSet.addTransition(textScale);
+    }
+
+    void beginDelayedTransition(ViewGroup view) {
+        TransitionManager.beginDelayedTransition(view, mSet);
+    }
+}
diff --git a/design/ics/android/support/design/internal/TextScale.java b/design/ics/android/support/design/internal/TextScale.java
new file mode 100644
index 0000000..219353e
--- /dev/null
+++ b/design/ics/android/support/design/internal/TextScale.java
@@ -0,0 +1,81 @@
+/*
+ * 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.ValueAnimator;
+import android.support.transition.Transition;
+import android.support.transition.TransitionValues;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.Map;
+
+/**
+ * @hide
+ */
+public class TextScale extends Transition {
+    private static final String PROPNAME_SCALE = "android:textscale:scale";
+
+    @Override
+    public void captureStartValues(TransitionValues transitionValues) {
+        captureValues(transitionValues);
+    }
+
+    @Override
+    public void captureEndValues(TransitionValues transitionValues) {
+        captureValues(transitionValues);
+    }
+
+    private void captureValues(TransitionValues transitionValues) {
+        if (transitionValues.view instanceof TextView) {
+            TextView textview = (TextView) transitionValues.view;
+            transitionValues.values.put(PROPNAME_SCALE, textview.getScaleX());
+        }
+    }
+
+    @Override
+    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
+            TransitionValues endValues) {
+        if (startValues == null || endValues == null || !(startValues.view instanceof TextView)
+                || !(endValues.view instanceof TextView)) {
+            return null;
+        }
+        final TextView view = (TextView) endValues.view;
+        Map<String, Object> startVals = startValues.values;
+        Map<String, Object> endVals = endValues.values;
+        final float startSize = startVals.get(PROPNAME_SCALE) != null ? (float) startVals.get(
+                PROPNAME_SCALE) : 1f;
+        final float endSize = endVals.get(PROPNAME_SCALE) != null ? (float) endVals.get(
+                PROPNAME_SCALE) : 1f;
+        if (startSize == endSize) {
+            return null;
+        }
+
+        ValueAnimator animator = ValueAnimator.ofFloat(startSize, endSize);
+
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float animatedValue = (float) valueAnimator.getAnimatedValue();
+                view.setScaleX(animatedValue);
+                view.setScaleY(animatedValue);
+            }
+        });
+        return animator;
+    }
+}
diff --git a/design/res/layout/design_bottom_navigation_item.xml b/design/res/layout/design_bottom_navigation_item.xml
index 5aa1042..cc7bb5f 100644
--- a/design/res/layout/design_bottom_navigation_item.xml
+++ b/design/res/layout/design_bottom_navigation_item.xml
@@ -21,6 +21,7 @@
         android:layout_height="24dp"
         android:layout_gravity="center_horizontal"
         android:layout_marginTop="@dimen/design_bottom_navigation_margin"
+        android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
         android:duplicateParentState="true" />
     <android.support.design.internal.BaselineLayout
         android:layout_width="wrap_content"
diff --git a/design/res/values/dimens.xml b/design/res/values/dimens.xml
index 546d137..63e98b8 100644
--- a/design/res/values/dimens.xml
+++ b/design/res/values/dimens.xml
@@ -62,6 +62,7 @@
     <dimen name="design_bottom_navigation_text_size">12sp</dimen>
     <dimen name="design_bottom_navigation_active_text_size">14sp</dimen>
     <dimen name="design_bottom_navigation_margin">8dp</dimen>
+    <dimen name="design_bottom_navigation_item_min_width">56dp</dimen>
     <dimen name="design_bottom_navigation_item_max_width">96dp</dimen>
     <dimen name="design_bottom_navigation_active_item_max_width">168dp</dimen>
 
diff --git a/design/src/android/support/design/internal/BottomNavigationItemView.java b/design/src/android/support/design/internal/BottomNavigationItemView.java
index 02d7186..d5e8176 100644
--- a/design/src/android/support/design/internal/BottomNavigationItemView.java
+++ b/design/src/android/support/design/internal/BottomNavigationItemView.java
@@ -16,22 +16,20 @@
 
 package android.support.design.internal;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
 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.ViewCompat;
-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.view.Gravity;
 import android.view.LayoutInflater;
-import android.view.ViewPropertyAnimator;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -43,13 +41,13 @@
     public static final int INVALID_ITEM_POSITION = -1;
 
     private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
-    private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
 
-    private final float mShiftAmount;
+    private final int mDefaultMargin;
+    private final int mShiftAmount;
     private final float mScaleUpFactor;
     private final float mScaleDownFactor;
-    private final float mInactiveLabelSize;
-    private final float mActiveLabelSize;
+
+    private boolean mShiftingMode;
 
     private ImageView mIcon;
     private final TextView mSmallLabel;
@@ -70,13 +68,15 @@
 
     public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mInactiveLabelSize =
-                getResources().getDimension(R.dimen.design_bottom_navigation_text_size);
-        mActiveLabelSize =
-                getResources().getDimension(R.dimen.design_bottom_navigation_active_text_size);
-        mShiftAmount = mInactiveLabelSize - mActiveLabelSize;
-        mScaleUpFactor = mActiveLabelSize / mInactiveLabelSize;
-        mScaleDownFactor = mInactiveLabelSize / mActiveLabelSize;
+        final Resources res = getResources();
+        int inactiveLabelSize =
+                res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size);
+        int activeLabelSize = res.getDimensionPixelSize(
+                R.dimen.design_bottom_navigation_active_text_size);
+        mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin);
+        mShiftAmount = inactiveLabelSize - activeLabelSize;
+        mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize;
+        mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize;
 
         LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
         setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
@@ -90,7 +90,7 @@
     public void initialize(MenuItemImpl itemData, int menuType) {
         mItemData = itemData;
         setCheckable(itemData.isCheckable());
-        setChecked(itemData.isChecked(), false);
+        setChecked(itemData.isChecked());
         setEnabled(itemData.isEnabled());
         setIcon(itemData.getIcon());
         setTitle(itemData.getTitle());
@@ -105,6 +105,10 @@
         return mItemPosition;
     }
 
+    public void setShiftingMode(boolean enabled) {
+        mShiftingMode = enabled;
+    }
+
     @Override
     public MenuItemImpl getItemData() {
         return mItemData;
@@ -123,24 +127,56 @@
 
     @Override
     public void setChecked(boolean checked) {
-        setChecked(checked, true);
-    }
-
-    public void setChecked(boolean checked, boolean animate) {
         mItemData.setChecked(checked);
 
-        if (checked) {
-            mLargeLabel.setVisibility(VISIBLE);
+        ViewCompat.setPivotX(mLargeLabel, mLargeLabel.getWidth() / 2);
+        ViewCompat.setPivotY(mLargeLabel, mLargeLabel.getBaseline());
+        ViewCompat.setPivotX(mSmallLabel, mSmallLabel.getWidth() / 2);
+        ViewCompat.setPivotY(mSmallLabel, mSmallLabel.getBaseline());
+        if (mShiftingMode) {
+            if (checked) {
+                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
+                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+                iconParams.topMargin = mDefaultMargin;
+                mIcon.setLayoutParams(iconParams);
+                mLargeLabel.setVisibility(VISIBLE);
+                ViewCompat.setScaleX(mLargeLabel, 1f);
+                ViewCompat.setScaleY(mLargeLabel, 1f);
+            } else {
+                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
+                iconParams.gravity = Gravity.CENTER;
+                iconParams.topMargin = mDefaultMargin;
+                mIcon.setLayoutParams(iconParams);
+                mLargeLabel.setVisibility(INVISIBLE);
+                ViewCompat.setScaleX(mLargeLabel, 0.5f);
+                ViewCompat.setScaleY(mLargeLabel, 0.5f);
+            }
             mSmallLabel.setVisibility(INVISIBLE);
         } else {
-            mLargeLabel.setVisibility(INVISIBLE);
-            mSmallLabel.setVisibility(VISIBLE);
-        }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
-            if (animate) {
-                animate(checked);
+            if (checked) {
+                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
+                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+                iconParams.topMargin = mDefaultMargin + mShiftAmount;
+                mIcon.setLayoutParams(iconParams);
+                mLargeLabel.setVisibility(VISIBLE);
+                mSmallLabel.setVisibility(INVISIBLE);
+
+                ViewCompat.setScaleX(mLargeLabel, 1f);
+                ViewCompat.setScaleY(mLargeLabel, 1f);
+                ViewCompat.setScaleX(mSmallLabel, mScaleUpFactor);
+                ViewCompat.setScaleY(mSmallLabel, mScaleUpFactor);
             } else {
-                mIcon.setTranslationY(checked ? mShiftAmount : 0f);
+                LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
+                iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+                iconParams.topMargin = mDefaultMargin;
+                mIcon.setLayoutParams(iconParams);
+                mLargeLabel.setVisibility(INVISIBLE);
+                mSmallLabel.setVisibility(VISIBLE);
+
+                ViewCompat.setScaleX(mLargeLabel, mScaleDownFactor);
+                ViewCompat.setScaleY(mLargeLabel, mScaleDownFactor);
+                ViewCompat.setScaleX(mSmallLabel, 1f);
+                ViewCompat.setScaleY(mSmallLabel, 1f);
             }
         }
 
@@ -206,41 +242,4 @@
                 ? null : ContextCompat.getDrawable(getContext(), background);
         ViewCompat.setBackground(this, backgroundDrawable);
     }
-
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
-    private void animate(final boolean active) {
-        // Grow or shrink the text of the tab.
-        final ViewPropertyAnimator textAnimator;
-        if (active) {
-            mLargeLabel.setVisibility(VISIBLE);
-            mSmallLabel.setVisibility(INVISIBLE);
-            textAnimator = scaleLabel(mLargeLabel, active);
-        } else {
-            mLargeLabel.setVisibility(INVISIBLE);
-            mSmallLabel.setVisibility(VISIBLE);
-            textAnimator = scaleLabel(mSmallLabel, active);
-        }
-
-        final ViewPropertyAnimator translationAnimation = mIcon.animate()
-                .setDuration(ACTIVE_ANIMATION_DURATION_MS)
-                .setInterpolator(new LinearOutSlowInInterpolator())
-                .translationY(active ? mShiftAmount : 0);
-
-        textAnimator.start();
-        translationAnimation.start();
-    }
-
-    private ViewPropertyAnimator scaleLabel(TextView label, boolean active) {
-        final float startingTextScale = active ? mScaleDownFactor : mScaleUpFactor;
-        label.setPivotX(label.getWidth() / 2);
-        label.setPivotY(label.getBaseline());
-        label.setScaleX(startingTextScale);
-        label.setScaleY(startingTextScale);
-        ViewPropertyAnimator textAnimator = label.animate()
-                .setDuration(ACTIVE_ANIMATION_DURATION_MS)
-                .setInterpolator(new LinearOutSlowInInterpolator())
-                .scaleX(1f)
-                .scaleY(1f);
-        return textAnimator;
-    }
 }
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
index 7080d2e..ec1b542 100644
--- a/design/src/android/support/design/internal/BottomNavigationMenuView.java
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -18,28 +18,35 @@
 
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.os.Build;
 import android.support.annotation.Nullable;
 import android.support.design.R;
 import android.support.v4.util.Pools;
+import android.support.v4.view.ViewCompat;
 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 {
+public class BottomNavigationMenuView extends ViewGroup implements MenuView {
+
     private final int mInactiveItemMaxWidth;
+    private final int mInactiveItemMinWidth;
     private final int mActiveItemMaxWidth;
+    private final int mItemHeight;
     private final OnClickListener mOnClickListener;
+    private final BottomNavigationAnimationHelperBase mAnimationHelper;
     private static final Pools.Pool<BottomNavigationItemView> sItemPool =
             new Pools.SynchronizedPool<>(5);
 
+    private boolean mShiftingMode = true;
+
     private BottomNavigationItemView[] mButtons;
     private int mActiveButton = 0;
     private ColorStateList mItemIconTint;
@@ -55,13 +62,20 @@
 
     public BottomNavigationMenuView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        setGravity(Gravity.CENTER);
-        setOrientation(HORIZONTAL);
-
-        mInactiveItemMaxWidth = getResources().getDimensionPixelSize(
+        final Resources res = getResources();
+        mInactiveItemMaxWidth = res.getDimensionPixelSize(
                 R.dimen.design_bottom_navigation_item_max_width);
-        mActiveItemMaxWidth = getResources()
-                .getDimensionPixelSize(R.dimen.design_bottom_navigation_active_item_max_width);
+        mInactiveItemMinWidth = res.getDimensionPixelSize(
+                R.dimen.design_bottom_navigation_item_min_width);
+        mActiveItemMaxWidth = res.getDimensionPixelSize(
+                R.dimen.design_bottom_navigation_active_item_max_width);
+        mItemHeight = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_height);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            mAnimationHelper = new BottomNavigationAnimationHelperIcs();
+        } else {
+            mAnimationHelper = new BottomNavigationAnimationHelperBase();
+        }
 
         mOnClickListener = new OnClickListener() {
             @Override
@@ -84,6 +98,81 @@
     }
 
     @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        final int count = getChildCount();
+
+        final int childState = 0;
+        final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
+
+        final int[] childWidths = new int[count];
+        if (mShiftingMode) {
+            final int inactiveCount = count - 1;
+            final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
+            final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
+            final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
+            final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
+            int extra = width - activeWidth - inactiveWidth * inactiveCount;
+            for (int i = 0; i < count; i++) {
+                childWidths[i] = (i == mActiveButton) ? activeWidth : inactiveWidth;
+                if (extra > 0) {
+                    childWidths[i]++;
+                    extra--;
+                }
+            }
+        } else {
+            final int maxAvailable = width / count;
+            final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
+            int extra = width - childWidth * count;
+            for (int i = 0; i < count; i++) {
+                childWidths[i] = childWidth;
+                if (extra > 0) {
+                    childWidths[i]++;
+                    extra--;
+                }
+            }
+        }
+
+        int totalWidth = 0;
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+            child.measure(MeasureSpec.makeMeasureSpec(childWidths[i], MeasureSpec.EXACTLY),
+                    heightSpec);
+            ViewGroup.LayoutParams params = child.getLayoutParams();
+            params.width = child.getMeasuredWidth();
+            totalWidth += child.getMeasuredWidth();
+        }
+        setMeasuredDimension(
+                ViewCompat.resolveSizeAndState(totalWidth,
+                        MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), childState),
+                ViewCompat.resolveSizeAndState(mItemHeight, heightSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int count = getChildCount();
+        final int width = right - left;
+        final int height = bottom - top;
+        int used = 0;
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+            if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
+                child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
+            } else {
+                child.layout(used, 0, child.getMeasuredWidth() + used, height);
+            }
+            used += child.getMeasuredWidth();
+        }
+    }
+
+    @Override
     public int getWindowAnimations() {
         return 0;
     }
@@ -137,6 +226,7 @@
         }
         removeAllViews();
         mButtons = new BottomNavigationItemView[mMenu.size()];
+        mShiftingMode = mMenu.size() > 3;
         for (int i = 0; i < mMenu.size(); i++) {
             mPresenter.setUpdateSuspended(true);
             mMenu.getItem(i).setCheckable(true);
@@ -146,6 +236,7 @@
             child.setIconTintList(mItemIconTint);
             child.setTextColor(mItemTextColor);
             child.setItemBackground(mItemBackgroundRes);
+            child.setShiftingMode(mShiftingMode);
             child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
             child.setItemPosition(i);
             child.setOnClickListener(mOnClickListener);
@@ -170,6 +261,8 @@
     private void activateNewButton(int newButton) {
         if (mActiveButton == newButton) return;
 
+        mAnimationHelper.beginDelayedTransition(this);
+
         mPresenter.setUpdateSuspended(true);
         mButtons[mActiveButton].setChecked(false);
         mButtons[newButton].setChecked(true);
@@ -178,28 +271,6 @@
         mActiveButton = newButton;
     }
 
-    public boolean updateOnSizeChange(int width) {
-        if (getChildCount() == 0) {
-            return false;
-        }
-        int available = width / getChildCount();
-        int itemWidth = Math.min(available, mActiveItemMaxWidth);
-
-        boolean changed = false;
-
-        for (int i = 0; i < mButtons.length; i++) {
-            ViewGroup.LayoutParams params = mButtons[i].getLayoutParams();
-            if (params.width == itemWidth) {
-                continue;
-            }
-            changed = true;
-            params.width = itemWidth;
-            params.height = ViewGroup.LayoutParams.MATCH_PARENT;
-            mButtons[i].setLayoutParams(params);
-        }
-        return changed;
-    }
-
     private BottomNavigationItemView getNewItem() {
         BottomNavigationItemView item = sItemPool.acquire();
         if (item == null) {
diff --git a/design/src/android/support/design/widget/BottomNavigationView.java b/design/src/android/support/design/widget/BottomNavigationView.java
index c5e6ea1..476889f 100644
--- a/design/src/android/support/design/widget/BottomNavigationView.java
+++ b/design/src/android/support/design/widget/BottomNavigationView.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.content.res.ColorStateList;
-import android.os.Parcelable;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -32,12 +31,12 @@
 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.ViewGroup;
 import android.widget.FrameLayout;
-import android.widget.LinearLayout;
 
 /**
  * <p>
@@ -96,8 +95,9 @@
         mMenu = new BottomNavigationMenu(context);
 
         mMenuView = new BottomNavigationMenuView(context);
-        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+        params.gravity = Gravity.CENTER;
         mMenuView.setLayoutParams(params);
 
         mPresenter.setBottomNavigationMenuView(mMenuView);
@@ -133,7 +133,7 @@
         }
         a.recycle();
 
-        addView(mMenuView);
+        addView(mMenuView, params);
 
         mMenu.setCallback(new MenuBuilder.Callback() {
             @Override
@@ -156,14 +156,6 @@
         mListener = listener;
     }
 
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        if (mMenuView.updateOnSizeChange(getMeasuredWidth())) {
-            // updateOnSizeChanged has changed LPs, so we need to remeasure
-            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        }
-    }
-
     /**
      * Returns the {@link Menu} instance associated with this bottom navigation bar.
      */
@@ -275,7 +267,7 @@
          *
          * @return true to display the item as the selected item
          */
-        public boolean onNavigationItemSelected(@NonNull MenuItem item);
+        boolean onNavigationItemSelected(@NonNull MenuItem item);
     }
 
     private MenuInflater getMenuInflater() {
diff --git a/samples/SupportDesignDemos/Android.mk b/samples/SupportDesignDemos/Android.mk
index de9e302..8ae120e 100644
--- a/samples/SupportDesignDemos/Android.mk
+++ b/samples/SupportDesignDemos/Android.mk
@@ -28,16 +28,19 @@
         android-support-v4 \
         android-support-v7-appcompat \
         android-support-v7-recyclerview \
+        android-support-transition \
         android-support-design
 LOCAL_RESOURCE_DIR = \
         $(LOCAL_PATH)/res \
         frameworks/support/v7/appcompat/res \
         frameworks/support/v7/recyclerview/res \
+        frameworks/support/transition/res \
         frameworks/support/design/res
 LOCAL_AAPT_FLAGS := \
         --auto-add-overlay \
         --extra-packages android.support.v7.appcompat \
         --extra-packages android.support.v7.recyclerview \
+        --extra-packages android.support.transition \
         --extra-packages android.support.design \
         --no-version-vectors
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
diff --git a/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml b/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml
index b21ba3b..83e7314 100644
--- a/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml
+++ b/samples/SupportDesignDemos/res/layout/design_bottom_navigation_view.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<FrameLayout
+<RelativeLayout
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         android:layout_width="match_parent"
@@ -30,15 +30,22 @@
         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:text="@string/bottomnavigation_add"
+        android:layout_below="@+id/button_disable"/>
+
+    <Button
+        android:id="@+id/button_remove"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/bottomnavigation_remove"
+        android:layout_below="@+id/button_add"/>
 
     <Button
         android:id="@+id/button_tint"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginTop="100dp"
-        android:text="@string/bottomnavigation_tint"/>
+        android:text="@string/bottomnavigation_tint"
+        android:layout_below="@+id/button_remove"/>
 
     <android.support.design.widget.BottomNavigationView
             android:id="@+id/bottom_navigation"
@@ -46,6 +53,7 @@
             android:layout_height="56dp"
             android:layout_gravity="bottom"
             android:background="#eee"
+            android:layout_alignParentBottom="true"
             app:menu="@menu/sample_bottom_menu"/>
 
-</FrameLayout>
+</RelativeLayout>
diff --git a/samples/SupportDesignDemos/res/values/strings.xml b/samples/SupportDesignDemos/res/values/strings.xml
index a6a1bc9..95900ca 100644
--- a/samples/SupportDesignDemos/res/values/strings.xml
+++ b/samples/SupportDesignDemos/res/values/strings.xml
@@ -124,6 +124,7 @@
     <string name="design_bottom_navigation_view">Bottom navigation view</string>
 
     <string name="bottomnavigation_disable">Disable item</string>
-    <string name="bottomnavigation_add">Add item</string>
+    <string name="bottomnavigation_add">Add an item</string>
+    <string name="bottomnavigation_remove">Remove an item</string>
     <string name="bottomnavigation_tint">Toggle tint</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
index fce3c3b..72b50db 100644
--- a/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
+++ b/samples/SupportDesignDemos/src/com/example/android/support/design/widget/BottomNavigationViewUsage.java
@@ -50,8 +50,17 @@
         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);
+                if (bottom.getMenu().size() < 5) {
+                    MenuItem item = bottom.getMenu().add("Bananas");
+                    item.setIcon(android.R.drawable.ic_lock_power_off);
+                }
+            }
+        });
+        Button buttonRemove = (Button) findViewById(R.id.button_remove);
+        buttonRemove.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                bottom.getMenu().removeItem(0);
             }
         });
         Button buttonTint = (Button) findViewById(R.id.button_tint);