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);
}