Modified floating action button.

No longer relies on RelativeLayout for positioning.
Moves as tabs scroll.
Repositions to the correct place in landscape dialer.

Bug: 15167378
Change-Id: I721336b3dbd60defbce77f3a09b4ba94cfc826b6
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 416c313..b45e3b0 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -38,7 +38,7 @@
     <!-- Match call_button_height to Phone's dimens/in_call_end_button_height -->
     <dimen name="call_button_height">74dp</dimen>
 
-    <dimen name="floating_action_button_dialpad_margin_bottom">12dp</dimen>
+    <dimen name="floating_action_button_dialpad_margin_bottom_offset">4dp</dimen>
 
     <!-- Dimensions for speed dial tiles -->
     <dimen name="contact_tile_divider_width">1dp</dimen>
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index 4ea9651..922b383 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -48,6 +48,7 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewTreeObserver;
 import android.view.View.OnDragListener;
 import android.view.View.OnTouchListener;
 import android.view.animation.AccelerateInterpolator;
@@ -88,6 +89,7 @@
 import com.android.dialer.list.SearchFragment;
 import com.android.dialer.list.SmartDialSearchFragment;
 import com.android.dialer.widget.ActionBarController;
+import com.android.dialer.widget.FloatingActionButtonController;
 import com.android.dialer.widget.SearchEditTextLayout;
 import com.android.dialer.widget.SearchEditTextLayout.OnBackButtonClickedListener;
 import com.android.dialerbind.DatabaseHelperManager;
@@ -180,18 +182,10 @@
     };
 
     /**
-     * Set to true if the device is in landscape orientation.
-     */
-    private boolean mIsLandscape;
-
-    /**
      * Fragment containing the speed dial list, recents list, and all contacts list.
      */
     private ListsFragment mListsFragment;
 
-    private View mFloatingActionButtonContainer;
-    private ImageButton mFloatingActionButton;
-
     private boolean mInDialpadSearch;
     private boolean mInRegularSearch;
     private boolean mClearSearchOnPause;
@@ -199,6 +193,11 @@
     private boolean mShowDialpadOnResume;
 
     /**
+     * Whether or not the device is in landscape orientation.
+     */
+    private boolean mIsLandscape;
+
+    /**
      * The position of the currently selected tab in the attached {@link ListsFragment}.
      */
     private int mCurrentTabPosition = 0;
@@ -235,10 +234,9 @@
     private DialerDatabaseHelper mDialerDatabaseHelper;
     private DragDropController mDragDropController;
     private ActionBarController mActionBarController;
+    private FloatingActionButtonController mFloatingActionButtonController;
 
     private int mActionBarHeight;
-    private int mFloatingActionButtonMarginBottom;
-    private int mFloatingActionButtonDialpadMarginBottom;
 
     private class OptionsPopupMenu extends PopupMenu {
         public OptionsPopupMenu(Context context, View anchor) {
@@ -352,10 +350,6 @@
 
         final Resources resources = getResources();
         mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height);
-        mFloatingActionButtonMarginBottom = resources.getDimensionPixelOffset(
-                R.dimen.floating_action_button_margin_bottom);
-        mFloatingActionButtonDialpadMarginBottom = resources.getDimensionPixelOffset(
-                R.dimen.floating_action_button_dialpad_margin_bottom);
 
         setContentView(R.layout.dialtacts_activity);
         getWindow().setBackgroundDrawable(null);
@@ -383,6 +377,14 @@
             }
         });
 
+        boolean mIsLandscape = getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE;
+        View floatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
+        ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
+        floatingActionButton.setOnClickListener(this);
+        mFloatingActionButtonController = new FloatingActionButtonController(this, mIsLandscape,
+                floatingActionButtonContainer);
+
         ImageButton optionsMenuButton = (ImageButton) mSearchEditTextLayout.findViewById(
                 R.id.dialtacts_options_menu_button);
         optionsMenuButton.setOnClickListener(this);
@@ -403,9 +405,8 @@
             mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
             mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN);
             mActionBarController.restoreInstanceState(savedInstanceState);
+            mFloatingActionButtonController.restoreInstanceState(savedInstanceState);
         }
-        mIsLandscape = getResources().getConfiguration().orientation ==
-                Configuration.ORIENTATION_LANDSCAPE;
 
         mSlideIn = AnimationUtils.loadAnimation(this,
                 mIsLandscape ? R.anim.slide_in_right : R.anim.slide_in);
@@ -417,15 +418,18 @@
         parentLayout = (RelativeLayout) findViewById(R.id.dialtacts_mainlayout);
         parentLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
         parentLayout.setOnDragListener(new LayoutOnDragListener());
+        parentLayout.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        int screenWidth = parentLayout.getWidth();
+                        mFloatingActionButtonController.setScreenWidth(screenWidth);
+                        parentLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                    }
+                });
 
         setupActivityOverlay();
 
-        mFloatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
-        ViewUtil.setupFloatingActionButton(mFloatingActionButtonContainer, getResources());
-
-        mFloatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
-        mFloatingActionButton.setOnClickListener(this);
-
         mRemoveViewContainer = findViewById(R.id.remove_view_container);
 
         mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
@@ -480,6 +484,7 @@
         outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
         outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown);
         mActionBarController.saveInstanceState(outState);
+        mFloatingActionButtonController.saveInstanceState(outState);
     }
 
     @Override
@@ -630,7 +635,7 @@
      * Callback from child DialpadFragment when the dialpad is shown.
      */
     public void onDialpadShown() {
-        updateFloatingActionButton();
+        mFloatingActionButtonController.updateByDialpadVisibility(true);
         if (mDialpadFragment.getAnimate()) {
             mDialpadFragment.getView().startAnimation(mSlideIn);
         } else {
@@ -659,7 +664,7 @@
         mDialpadFragment.setAnimate(animate);
 
         updateSearchFragmentPosition();
-        updateFloatingActionButton();
+        mFloatingActionButtonController.updateByDialpadVisibility(false);
         if (animate) {
             mDialpadFragment.getView().startAnimation(mSlideOut);
         } else {
@@ -993,7 +998,7 @@
 
     @Override
     public void setFloatingActionButtonVisible(boolean visible) {
-        mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
+        mFloatingActionButtonController.setVisible(visible);
     }
 
     private boolean phoneIsInUse() {
@@ -1091,72 +1096,19 @@
 
     @Override
     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-
+        mFloatingActionButtonController.onPageScrolled(position, positionOffset);
     }
 
     @Override
     public void onPageSelected(int position) {
         mCurrentTabPosition = position;
-        // If the dialpad is showing, the floating action button should always be middle aligned.
-        if (!mIsDialpadShown) {
-            alignFloatingActionButtonByTab(mCurrentTabPosition);
-        }
+        mFloatingActionButtonController.updateByTab(position);
     }
 
     @Override
     public void onPageScrollStateChanged(int state) {
     }
 
-    private void updateFloatingActionButton() {
-        if (mIsDialpadShown) {
-            mFloatingActionButton.setImageResource(R.drawable.fab_ic_call);
-            mFloatingActionButton.setContentDescription(
-                    getResources().getString(R.string.description_dial_button));
-            alignFloatingActionButtonMiddle();
-        } else {
-            mFloatingActionButton.setImageResource(R.drawable.fab_ic_dial);
-            mFloatingActionButton.setContentDescription(
-                    getResources().getString(R.string.action_menu_dialpad_button));
-            alignFloatingActionButtonByTab(mCurrentTabPosition);
-        }
-    }
-
-    private void alignFloatingActionButtonByTab(int position) {
-        if (position == ListsFragment.TAB_INDEX_SPEED_DIAL) {
-            alignFloatingActionButtonMiddle();
-        } else {
-            alignFloatingActionButtonRight();
-        }
-    }
-
-    private void alignFloatingActionButtonRight() {
-        final RelativeLayout.LayoutParams params =
-                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
-        params.removeRule(RelativeLayout.CENTER_HORIZONTAL);
-        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
-        updateFloatingActionButtonMargin(params);
-        mFloatingActionButtonContainer.setLayoutParams(params);
-    }
-
-    private void alignFloatingActionButtonMiddle() {
-        final RelativeLayout.LayoutParams params =
-                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
-        params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT);
-        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
-        updateFloatingActionButtonMargin(params);
-        mFloatingActionButtonContainer.setLayoutParams(params);
-    }
-
-    private void updateFloatingActionButtonMargin(RelativeLayout.LayoutParams params) {
-        params.setMarginsRelative(
-                params.getMarginStart(),
-                params.topMargin,
-                params.getMarginEnd(),
-                mIsDialpadShown ?
-                        mFloatingActionButtonDialpadMarginBottom :
-                        mFloatingActionButtonMarginBottom);
-    }
-
     private TelephonyManager getTelephonyManager() {
         return (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
     }
diff --git a/src/com/android/dialer/widget/FloatingActionButtonController.java b/src/com/android/dialer/widget/FloatingActionButtonController.java
new file mode 100644
index 0000000..3f59153
--- /dev/null
+++ b/src/com/android/dialer/widget/FloatingActionButtonController.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2014 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.android.dialer.widget;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.contacts.common.util.ViewUtil;
+import com.android.dialer.R;
+import com.android.dialer.list.ListsFragment;
+
+/**
+ * Controls the movement and appearance of the FAB.
+ */
+public class FloatingActionButtonController {
+    private static final String KEY_IS_DIALPAD_VISIBLE = "key_is_dialpad_visible";
+    private static final String KEY_CURRENT_TAB_POSITION = "key_current_tab_position";
+    private static final int ANIMATION_DURATION = 250;
+
+    private int mScreenWidth;
+
+    private int mCurrentTabPosition;
+
+    private ImageButton mFloatingActionButton;
+    private View mFloatingActionButtonContainer;
+
+    private boolean mIsLandscape;
+    private boolean mIsDialpadVisible;
+    private boolean mAnimateFloatingActionButton;
+
+    private String mDescriptionDialButtonStr;
+    private String mActionMenuDialpadButtonStr;
+
+    /**
+     * Interpolator for FAB animations.
+     */
+    private Interpolator mFabInterpolator;
+
+    /**
+     * Additional offset for FAB to be lowered when dialpad is open.
+     */
+    private int mFloatingActionButtonDialpadMarginBottomOffset;
+
+    public FloatingActionButtonController(Activity activity, boolean isLandscape,
+                View container) {
+        Resources resources = activity.getResources();
+        mIsLandscape = isLandscape;
+        mFabInterpolator = AnimationUtils.loadInterpolator(activity,
+                android.R.interpolator.fast_out_slow_in);
+        mFloatingActionButtonDialpadMarginBottomOffset = resources.getDimensionPixelOffset(
+                R.dimen.floating_action_button_dialpad_margin_bottom_offset);
+        mFloatingActionButton = (ImageButton) activity.
+                findViewById(R.id.floating_action_button);
+        mDescriptionDialButtonStr = resources.getString(R.string.description_dial_button);
+        mActionMenuDialpadButtonStr = resources.getString(R.string.action_menu_dialpad_button);
+        mFloatingActionButtonContainer = container;
+        ViewUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources);
+    }
+
+    /**
+     * Passes the screen width into the class. Necessary for translation calculations.
+     *
+     * @param screenWidth the width of the screen
+     */
+    public void setScreenWidth(int screenWidth) {
+        mScreenWidth = screenWidth;
+        updateByDialpadVisibility(mIsDialpadVisible);
+    }
+
+    public void setVisible(boolean visible) {
+        mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * Updates the FAB location (middle to right position) as the PageView scrolls.
+     *
+     * @param position tab position to align for
+     * @param positionOffset a fraction used to calculate position of the FAB during page scroll
+     */
+    public void onPageScrolled(int position, float positionOffset) {
+        // As the page is scrolling, if we're on the first tab, update the FAB position so it
+        // moves along with it.
+        if (position == ListsFragment.TAB_INDEX_SPEED_DIAL) {
+            mFloatingActionButtonContainer.setTranslationX(
+                    (int) (positionOffset * (mScreenWidth / 2f
+                            - mFloatingActionButton.getWidth())));
+            mFloatingActionButtonContainer.setTranslationY(0);
+        }
+    }
+
+    /**
+     * Updates the FAB location given a tab position.
+     *
+     * @param position tab position to align for
+     */
+    public void updateByTab(int position) {
+        // If the screen width hasn't been set yet, don't do anything.
+        if (mScreenWidth == 0 || mIsDialpadVisible) return;
+        alignFloatingActionButtonByTab(position, false);
+        mAnimateFloatingActionButton = true;
+    }
+
+    /**
+     * Updates the FAB location to the proper location given whether or not the dialer is open.
+     *
+     * @param dialpadVisible whether or not the dialpad is currently open
+     */
+    public void updateByDialpadVisibility(boolean dialpadVisible) {
+        // If the screen width hasn't been set yet, don't do anything.
+        if (mScreenWidth == 0) return;
+        mIsDialpadVisible = dialpadVisible;
+
+        moveFloatingActionButton(mAnimateFloatingActionButton);
+        mAnimateFloatingActionButton = true;
+    }
+
+    /**
+     * Moves the FAB to the best known location given what the class currently knows.
+     *
+     * @param animate whether or not to smoothly animate the button
+     */
+    private void moveFloatingActionButton(boolean animate) {
+        if (mIsDialpadVisible) {
+            mFloatingActionButton.setImageResource(R.drawable.fab_ic_call);
+            mFloatingActionButton.setContentDescription(mDescriptionDialButtonStr);
+            alignFloatingActionButton(animate);
+        } else {
+            mFloatingActionButton.setImageResource(R.drawable.fab_ic_dial);
+            mFloatingActionButton.setContentDescription(mActionMenuDialpadButtonStr);
+            alignFloatingActionButtonByTab(mCurrentTabPosition, mAnimateFloatingActionButton);
+        }
+    }
+
+    /**
+     * Aligns the FAB to the position for the indicated tab.
+     *
+     * @param position tab position to align for
+     * @param animate whether or not to smoothly animate the button
+     */
+    private void alignFloatingActionButtonByTab(int position, boolean animate) {
+        mCurrentTabPosition = position;
+        alignFloatingActionButton(animate);
+    }
+
+    /**
+     * Aligns the FAB to the correct position.
+     *
+     * @param animate whether or not to smoothly animate the button
+     */
+    private void alignFloatingActionButton(boolean animate) {
+        int translationX = calculateTranslationX();
+        int translationY = mIsDialpadVisible ? mFloatingActionButtonDialpadMarginBottomOffset : 0;
+        if (animate) {
+            mFloatingActionButtonContainer.animate()
+                    .translationX(translationX)
+                    .translationY(translationY)
+                    .setInterpolator(mFabInterpolator)
+                    .setDuration(ANIMATION_DURATION).start();
+        } else {
+            mFloatingActionButtonContainer.setTranslationX(translationX);
+            mFloatingActionButtonContainer.setTranslationY(translationY);
+        }
+    }
+
+    /**
+     * Calculates the translationX distance for the FAB.
+     */
+    private int calculateTranslationX() {
+        if (mIsDialpadVisible) {
+            return mIsLandscape ? mScreenWidth / 4 : 0;
+        }
+        if (mCurrentTabPosition == ListsFragment.TAB_INDEX_SPEED_DIAL) {
+            return 0;
+        }
+        return mScreenWidth / 2 - mFloatingActionButton.getWidth();
+    }
+
+    /**
+     * Saves the current state of the floating action button into a provided {@link Bundle}
+     */
+    public void saveInstanceState(Bundle outState) {
+        outState.putBoolean(KEY_IS_DIALPAD_VISIBLE, mIsDialpadVisible);
+        outState.putInt(KEY_CURRENT_TAB_POSITION, mCurrentTabPosition);
+    }
+
+    /**
+     * Restores the floating action button state from a provided {@link Bundle}
+     */
+    public void restoreInstanceState(Bundle inState) {
+        mIsDialpadVisible = inState.getBoolean(KEY_IS_DIALPAD_VISIBLE);
+        mCurrentTabPosition = inState.getInt(KEY_CURRENT_TAB_POSITION);
+    }
+}