Fix tab carousel flicker issues

Cases fixed:
- Animation of the tab carousel causes a brief jump
at the end of the animation because the vertical scroll
listener is trying to move the tab carousel to a different
Y coordinate at the same time

- Horizontally scrolling the tab carousel causes it to
get into an "in-between" state where it is out of sync
with the ViewPager (there is also flicker on each tab
because the alpha values are wrong)

- Rotating from phone landscape updates page to phone portrait
was never implemented (didn't scroll the tab carousel to the
right tab)

- Rotating from phone portrait updates page to phone landscape
would cause a noticeable flicker where the page would slide to
the left so the correct page was selected because it was scrolled
when executing a Runnable

Fix issues by scrolling the HorizontalScrollView if necessary
in onLayout(). Consume touch down/up events on the tab carousel.
Add flag to know when the tab carousel is already animating.

Bug: 5220668
Change-Id: Icecaa99b43682111fb7c7d201a059b3962b00cd6
diff --git a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
index 106ff0e..b01316b 100644
--- a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
@@ -88,7 +88,7 @@
     private View mDetailFragmentView;
     private View mUpdatesFragmentView;
 
-    private final Handler mHandler = new Handler();
+    private boolean mScrollToCurrentPage = false;
 
     public ContactDetailFragmentCarousel(Context context) {
         this(context, null);
@@ -144,6 +144,28 @@
                 resolveSize(screenHeight, heightMeasureSpec));
     }
 
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        if (mScrollToCurrentPage) {
+            mScrollToCurrentPage = false;
+            // Use scrollTo() instead of smoothScrollTo() to prevent a visible flicker to the user
+            scrollTo(mCurrentPage == ABOUT_PAGE ? 0 : mAllowedHorizontalScrollLength, 0);
+            updateTouchInterceptors();
+        }
+    }
+
+    /**
+     * Set the current page that should be restored when the view is first laid out.
+     */
+    public void restoreCurrentPage(int pageIndex) {
+        setCurrentPage(pageIndex);
+        // It is only possible to scroll the view after onMeasure() has been called (where the
+        // allowed horizontal scroll length is determined). Hence, set a flag that will be read
+        // in onLayout() after the children and this view have finished being laid out.
+        mScrollToCurrentPage = true;
+    }
+
     /**
      * Set the current page. This auto-scrolls the carousel to the current page and dims out
      * the non-selected page.
@@ -183,31 +205,13 @@
             mEnableSwipe = enable;
             if (mUpdatesFragmentView != null) {
                 mUpdatesFragmentView.setVisibility(enable ? View.VISIBLE : View.GONE);
+                mScrollToCurrentPage = true;
                 requestLayout();
                 invalidate();
             }
-            // This method could have been called before the view has been measured (i.e.
-            // immediately after a rotation), so snap to edge only after the view is ready.
-            postRunnableToSnapToEdge();
         }
     }
 
-    /**
-     * Snap to the currently selected page only once all the view setup and measurement has
-     * completed (i.e. we need to know the allowed horizontal scroll width in order to
-     * snap to the correct page).
-     */
-    private void postRunnableToSnapToEdge() {
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (isAttachedToWindow() && mAboutFragment != null && mUpdatesFragment != null) {
-                    snapToEdge();
-                }
-            }
-        });
-    }
-
     public int getCurrentPage() {
         return mCurrentPage;
     }
@@ -302,8 +306,4 @@
         }
         return false;
     }
-
-    private boolean isAttachedToWindow() {
-        return getWindowToken() != null;
-    }
 }
diff --git a/src/com/android/contacts/detail/ContactDetailLayoutController.java b/src/com/android/contacts/detail/ContactDetailLayoutController.java
index 6b8829e..7a7f400 100644
--- a/src/com/android/contacts/detail/ContactDetailLayoutController.java
+++ b/src/com/android/contacts/detail/ContactDetailLayoutController.java
@@ -21,6 +21,8 @@
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
 
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
 import android.animation.ObjectAnimator;
 import android.app.Activity;
 import android.app.FragmentManager;
@@ -77,6 +79,7 @@
 
     private ContactLoader.Result mContactData;
 
+    private boolean mTabCarouselIsAnimating;
     private boolean mContactHasUpdates;
 
     private LayoutMode mLayoutMode;
@@ -176,6 +179,7 @@
                 }
 
                 mTabCarousel.setListener(mTabCarouselListener);
+                mTabCarousel.restoreCurrentTab(currentPageIndex);
                 mDetailFragment.setVerticalScrollListener(
                         new VerticalScrollListener(TAB_INDEX_DETAIL));
                 mUpdatesFragment.setVerticalScrollListener(
@@ -211,7 +215,7 @@
 
                 mFragmentCarousel.setFragmentViews(mDetailFragmentView, mUpdatesFragmentView);
                 mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment);
-                mFragmentCarousel.setCurrentPage(currentPageIndex);
+                mFragmentCarousel.restoreCurrentPage(currentPageIndex);
                 break;
             }
         }
@@ -239,6 +243,7 @@
     public void showEmptyState() {
         switch (mLayoutMode) {
             case FRAGMENT_CAROUSEL: {
+                mFragmentCarousel.setCurrentPage(0);
                 mFragmentCarousel.enableSwipe(false);
                 mDetailFragment.showEmptyState();
                 break;
@@ -323,6 +328,7 @@
                 break;
             case FRAGMENT_CAROUSEL: {
                 // Disable swipe so only the detail fragment shows
+                mFragmentCarousel.setCurrentPage(0);
                 mFragmentCarousel.enableSwipe(false);
                 break;
             }
@@ -449,12 +455,14 @@
                     mTabCarousel, "y", desiredValue).setDuration(75);
             mTabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator(
                     mActivity, android.R.anim.accelerate_decelerate_interpolator));
+            mTabCarouselAnimator.addListener(mTabCarouselAnimatorListener);
         }
 
         private void cancelTabCarouselAnimator() {
             if (mTabCarouselAnimator != null) {
                 mTabCarouselAnimator.cancel();
                 mTabCarouselAnimator = null;
+                mTabCarouselIsAnimating = false;
             }
         }
     };
@@ -478,6 +486,34 @@
         }
     }
 
+    /**
+     * This listener keeps track of whether the tab carousel animation is currently going on or not,
+     * in order to prevent other simultaneous changes to the Y position of the tab carousel which
+     * can cause flicker.
+     */
+    private final AnimatorListener mTabCarouselAnimatorListener = new AnimatorListener() {
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            mTabCarouselIsAnimating = false;
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mTabCarouselIsAnimating = false;
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+            mTabCarouselIsAnimating = true;
+        }
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+            mTabCarouselIsAnimating = true;
+        }
+    };
+
     private final ContactDetailTabCarousel.Listener mTabCarouselListener =
             new ContactDetailTabCarousel.Listener() {
 
@@ -529,10 +565,11 @@
                 int totalItemCount) {
             int currentPageIndex = mViewPager.getCurrentItem();
             // Don't move the carousel if: 1) the contact does not have social updates because then
-            // tab carousel must not be visible, 2) if the view pager is still being scrolled, or
-            // 3) if the current page being viewed is not this one.
+            // tab carousel must not be visible, 2) if the view pager is still being scrolled,
+            // 3) if the current page being viewed is not this one, or 4) if the tab carousel
+            // is already being animated vertically.
             if (!mContactHasUpdates || mViewPagerState != ViewPager.SCROLL_STATE_IDLE ||
-                    mPageIndex != currentPageIndex) {
+                    mPageIndex != currentPageIndex || mTabCarouselIsAnimating) {
                 return;
             }
             // If the FIRST item is not visible on the screen, then the carousel must be pinned
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 8a51d81..9300b54 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -63,6 +63,7 @@
 
     private int mTabDisplayLabelHeight;
 
+    private boolean mScrollToCurrentTab = false;
     private int mLastScrollPosition;
 
     private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
@@ -103,12 +104,7 @@
         mUpdatesTab = (CarouselTab) findViewById(R.id.tab_update);
         mUpdatesTab.setLabel(mContext.getString(R.string.contactDetailUpdates));
 
-        // TODO: We can't always assume the "about" page will be the current page.
-        mAboutTab.showSelectedState();
-        mAboutTab.setAlphaLayerValue(0);
         mAboutTab.enableTouchInterceptor(mAboutTabTouchInterceptListener);
-
-        mUpdatesTab.setAlphaLayerValue(MAX_ALPHA);
         mUpdatesTab.enableTouchInterceptor(mUpdatesTabTouchInterceptListener);
 
         // Retrieve the photo view for the "about" tab
@@ -144,6 +140,15 @@
                 resolveSize(tabHeight, heightMeasureSpec));
     }
 
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        if (mScrollToCurrentTab) {
+            mScrollToCurrentTab = false;
+            scrollTo(mCurrentTab == TAB_INDEX_ABOUT ? 0 : mAllowedHorizontalScrollLength, 0);
+        }
+    }
+
     private final OnClickListener mAboutTabTouchInterceptListener = new OnClickListener() {
         @Override
         public void onClick(View v) {
@@ -174,6 +179,17 @@
     }
 
     /**
+     * Set the current tab that should be restored when the view is first laid out.
+     */
+    public void restoreCurrentTab(int position) {
+        setCurrentTab(position);
+        // It is only possible to scroll the view after onMeasure() has been called (where the
+        // allowed horizontal scroll length is determined). Hence, set a flag that will be read
+        // in onLayout() after the children and this view have finished being laid out.
+        mScrollToCurrentTab = true;
+    }
+
+    /**
      * Restore the Y position of this view to the last manually requested value. This can be done
      * after the parent has been re-laid out again, where this view's position could have been
      * lost if the view laid outside its parent's bounds.
@@ -225,8 +241,6 @@
      * Updates the tab selection.
      */
     public void setCurrentTab(int position) {
-        // TODO: Handle device rotation (saving and restoring state of the selected tab)
-        // This will take more work because there is no tab carousel in phone landscape
         switch (position) {
             case TAB_INDEX_ABOUT:
                 mAboutTab.showSelectedState();
@@ -270,10 +284,10 @@
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mListener.onTouchDown();
-                return false;
+                return true;
             case MotionEvent.ACTION_UP:
                 mListener.onTouchUp();
-                return false;
+                return true;
         }
         return super.onTouchEvent(event);
     }