Fix bugs in contact card tab carousel

- Fix NPE in scroll length calculation. I was calculating
the allowed scroll length on the fly when it was requested, but
apparently it was being requested before the data was
being loaded in. Anyhow, move the measure code to its
proper place in onMeasure().

- Programatically determine tab width instead of hardcoding it to
240dip

- Move findViewById calls to the onFinishInflate() method
so it only happens once instead of every time loadData() is called

- Fix the bug where if you vertically fling the contact details
too fast, the pinned headers get stuck in some odd state (b/c the scroll
listener missed a frame, we need to always make sure the tab
carousel is in the correct place at all times)

Bug: 4965089

Change-Id: I9ee3261dfff86d7df6f3eb27ec464eea26a900db
diff --git a/res/layout/carousel_about_tab.xml b/res/layout/carousel_about_tab.xml
index bf67ee2..7f70eab 100644
--- a/res/layout/carousel_about_tab.xml
+++ b/res/layout/carousel_about_tab.xml
@@ -16,8 +16,9 @@
 
 <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/detail_tab_carousel_tab_width"
-    android:layout_height="@dimen/detail_tab_carousel_height"
+    android:layout_width="0dip"
+    android:layout_height="match_parent"
+    android:layout_weight="1"
     android:background="@color/detail_tab_background">
 
     <ImageView android:id="@+id/photo"
diff --git a/res/layout/carousel_updates_tab.xml b/res/layout/carousel_updates_tab.xml
index 9da2272..d235280 100644
--- a/res/layout/carousel_updates_tab.xml
+++ b/res/layout/carousel_updates_tab.xml
@@ -16,8 +16,9 @@
 
 <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/detail_tab_carousel_tab_width"
-    android:layout_height="@dimen/detail_tab_carousel_height"
+    android:layout_width="0dip"
+    android:layout_height="match_parent"
+    android:layout_weight="1"
     android:background="@color/detail_tab_background">
 
     <!-- Transparent view to overlay on the contact's photo
diff --git a/res/layout/contact_detail_container_with_updates.xml b/res/layout/contact_detail_container_with_updates.xml
index de7d145..48f1800 100644
--- a/res/layout/contact_detail_container_with_updates.xml
+++ b/res/layout/contact_detail_container_with_updates.xml
@@ -26,8 +26,9 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
-    <com.android.contacts.detail.ContactDetailTabCarousel
+    <include
         android:id="@+id/tab_carousel"
+        layout="@layout/contact_detail_tab_carousel"
         android:layout_alignParentTop="true"
         android:layout_alignParentLeft="true"
         android:layout_width="match_parent"
diff --git a/res/layout/contact_detail_tab_carousel.xml b/res/layout/contact_detail_tab_carousel.xml
index a7321ee..175194c 100644
--- a/res/layout/contact_detail_tab_carousel.xml
+++ b/res/layout/contact_detail_tab_carousel.xml
@@ -14,16 +14,18 @@
      limitations under the License.
 -->
 
-<HorizontalScrollView
+<view
     xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.detail.ContactDetailTabCarousel"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:scrollbars="none">
+    android:layout_height="@dimen/detail_tab_carousel_height"
+    android:scrollbars="none"
+    android:fadingEdge="none">
 
     <LinearLayout
         android:id="@+id/tab_container"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/detail_tab_carousel_height"
         android:orientation="horizontal">
 
         <include
@@ -36,4 +38,4 @@
 
     </LinearLayout>
 
-</HorizontalScrollView>
\ No newline at end of file
+</view>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index b0624cb..a0c4845 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -91,9 +91,6 @@
     <!-- Height of the tab carousel on the contact detail page -->
     <dimen name="detail_tab_carousel_height">150dip</dimen>
 
-    <!-- Width of a tab in the tab carousel on the contact detail page -->
-    <dimen name="detail_tab_carousel_tab_width">240dip</dimen>
-
     <!-- Height of the tab text label in the tab carousel on the contact detail page -->
     <dimen name="detail_tab_carousel_tab_label_height">40dip</dimen>
 
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 9c403b3..c448d21 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -33,7 +33,6 @@
 
 import android.accounts.Account;
 import android.app.ActionBar;
-import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
@@ -49,12 +48,12 @@
 import android.support.v4.view.ViewPager.OnPageChangeListener;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.View.OnClickListener;
-import android.view.LayoutInflater;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
@@ -466,10 +465,10 @@
             if (mTabCarousel == null) {
                 return;
             }
-            // Only re-position the tab carousel vertically if the FIRST item is still visible on
-            // the screen, otherwise the carousel should be in the correct place (pinned at the
-            // top).
+            // If the FIRST item is not visible on the screen, then the carousel must be pinned
+            // at the top of the screen.
             if (firstVisibleItem != 0) {
+                mTabCarousel.setY(-mTabCarousel.getAllowedVerticalScrollLength());
                 return;
             }
             View topView = view.getChildAt(firstVisibleItem);
@@ -477,7 +476,7 @@
                 return;
             }
             int amtToScroll = Math.max((int) view.getChildAt(firstVisibleItem).getY(),
-                    -mTabCarousel.getAllowedVerticalScrollLength());
+                    - mTabCarousel.getAllowedVerticalScrollLength());
             mTabCarousel.setY(amtToScroll);
         }
 
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 2ce26c6..6e1b199 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -20,8 +20,8 @@
 import com.android.contacts.R;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.util.AttributeSet;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
@@ -32,18 +32,27 @@
 /**
  * This is a horizontally scrolling carousel with 2 tabs: one to see info about the contact and
  * one to see updates from the contact.
- * TODO: Create custom views for the tabs so their width can be programatically set as 2/3 of the
- * screen width.
  */
 public class ContactDetailTabCarousel extends HorizontalScrollView implements OnTouchListener {
-    private static final String TAG = "ContactDetailTabCarousel";
+
+    private static final String TAG = ContactDetailTabCarousel.class.getSimpleName();
+
+    private static final int TAB_INDEX_ABOUT = 0;
+    private static final int TAB_INDEX_UPDATES = 1;
+    private static final int TAB_COUNT = 2;
+
+    private static final double TAB_WIDTH_SCREEN_PERCENTAGE = 0.75;
 
     private ImageView mPhotoView;
     private TextView mStatusView;
 
     private Listener mListener;
 
-    private View[] mTabs = new View[2];
+    private final View[] mTabs = new View[TAB_COUNT];
+
+    private int mTabWidth;
+    private int mTabHeight;
+    private int mTabDisplayLabelHeight;
 
     private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
     private int mAllowedVerticalScrollLength = Integer.MIN_VALUE;
@@ -58,22 +67,78 @@
         public void onTabSelected(int position);
     }
 
-    public ContactDetailTabCarousel(Context context) {
-        this(context, null);
-    }
-
     public ContactDetailTabCarousel(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public ContactDetailTabCarousel(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        final LayoutInflater inflater =
-            (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        inflater.inflate(R.layout.contact_detail_tab_carousel, this);
+        super(context, attrs);
 
         setOnTouchListener(this);
+
+        Resources resources = mContext.getResources();
+        mTabHeight = resources.getDimensionPixelSize(R.dimen.detail_tab_carousel_height);
+        mTabDisplayLabelHeight = resources.getDimensionPixelSize(
+                R.dimen.detail_tab_carousel_tab_label_height);
+        mAllowedVerticalScrollLength = mTabHeight - mTabDisplayLabelHeight;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        View aboutView = findViewById(R.id.tab_about);
+        View updateView = findViewById(R.id.tab_update);
+
+        TextView aboutTab = (TextView) aboutView.findViewById(R.id.label);
+        aboutTab.setText(mContext.getString(R.string.contactDetailAbout));
+        aboutTab.setClickable(true);
+        aboutTab.setSelected(true);
+
+        TextView updatesTab = (TextView) updateView.findViewById(R.id.label);
+        updatesTab.setText(mContext.getString(R.string.contactDetailUpdates));
+        updatesTab.setClickable(true);
+
+        aboutTab.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mListener.onTabSelected(TAB_INDEX_ABOUT);
+            }
+        });
+        updatesTab.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mListener.onTabSelected(TAB_INDEX_ABOUT);
+            }
+        });
+
+        mTabs[TAB_INDEX_ABOUT] = aboutTab;
+        mTabs[TAB_INDEX_UPDATES] = updatesTab;
+
+        // Retrieve the photo view for the "about" tab
+        mPhotoView = (ImageView) aboutView.findViewById(R.id.photo);
+
+        // Retrieve the social update views for the "updates" tab
+        mStatusView = (TextView) updateView.findViewById(R.id.status);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        measureChildren(widthMeasureSpec);
+    }
+
+    private void measureChildren(int widthMeasureSpec) {
+        int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
+        // Compute the width of a tab as a fraction of the screen width
+        mTabWidth = (int) (TAB_WIDTH_SCREEN_PERCENTAGE * screenWidth);
+
+        // Find the allowed scrolling length by subtracting the current visible screen width
+        // from the total length of the tabs.
+        mAllowedHorizontalScrollLength = mTabWidth * TAB_COUNT - screenWidth;
+
+        // Set the child {@link LinearLayout} to be TAB_COUNT * the computed tab width so that the
+        // {@link LinearLayout}'s children (which are the tabs) will evenly split that width.
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            child.measure(MeasureSpec.makeMeasureSpec(TAB_COUNT * mTabWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mTabHeight, MeasureSpec.EXACTLY));
+        }
     }
 
     @Override
@@ -86,19 +151,6 @@
      * Returns the number of pixels that this view can be scrolled horizontally.
      */
     public int getAllowedHorizontalScrollLength() {
-        // We can't compute this in the constructor because the view widths are 0, so do the
-        // calculation only when this getter method is called (all the views should be created
-        // by this time).
-        if (mAllowedHorizontalScrollLength == Integer.MIN_VALUE) {
-            // Find the total length of two tabs side-by-side
-            int totalLength = 0;
-            for (int i=0; i < mTabs.length; i++) {
-                totalLength += mTabs[i].getWidth();
-            }
-            // Find the allowed scrolling length by subtracting the current visible screen width
-            // from the total length of the tabs.
-            mAllowedHorizontalScrollLength = totalLength - getWidth();
-        }
         return mAllowedHorizontalScrollLength;
     }
 
@@ -107,16 +159,6 @@
      * the tab labels to still show.
      */
     public int getAllowedVerticalScrollLength() {
-        if (mAllowedVerticalScrollLength == Integer.MIN_VALUE) {
-            // Find the total height of a tab
-            View aboutView = findViewById(R.id.tab_about);
-            int totalHeight = aboutView.getHeight();
-            // Find the height of a tab label
-            TextView aboutTab = (TextView) aboutView.findViewById(R.id.label);
-            int labelHeight = aboutTab.getHeight();
-            // Find the allowed scrolling length by subtracting the two values
-            mAllowedVerticalScrollLength = totalHeight - labelHeight;
-        }
         return mAllowedVerticalScrollLength;
     }
 
@@ -153,40 +195,6 @@
             return;
         }
 
-        View aboutView = findViewById(R.id.tab_about);
-        View updateView = findViewById(R.id.tab_update);
-
-        TextView aboutTab = (TextView) aboutView.findViewById(R.id.label);
-        aboutTab.setText(mContext.getString(R.string.contactDetailAbout));
-        aboutTab.setClickable(true);
-        aboutTab.setSelected(true);
-
-        TextView updatesTab = (TextView) updateView.findViewById(R.id.label);
-        updatesTab.setText(mContext.getString(R.string.contactDetailUpdates));
-        updatesTab.setClickable(true);
-
-        aboutTab.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mListener.onTabSelected(0);
-            }
-        });
-        updatesTab.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mListener.onTabSelected(1);
-            }
-        });
-
-        mTabs[0] = aboutTab;
-        mTabs[1] = updatesTab;
-
-        // Retrieve the photo view for the "about" tab
-        mPhotoView = (ImageView) aboutView.findViewById(R.id.photo);
-
-        // Retrieve the social update view for the "updates" tab
-        mStatusView = (TextView) updateView.findViewById(R.id.status);
-
         ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView);
         ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView);
     }
@@ -203,10 +211,10 @@
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mListener.onTouchDown();
-                return true;
+                return false;
             case MotionEvent.ACTION_UP:
                 mListener.onTouchUp();
-                return true;
+                return false;
         }
         return super.onTouchEvent(event);
     }