Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 226dcdf3 authored by Katherine Kuan's avatar Katherine Kuan
Browse files

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
parent d525aee2
Loading
Loading
Loading
Loading
+24 −24
Original line number Diff line number Diff line
@@ -88,7 +88,7 @@ public class ContactDetailFragmentCarousel extends HorizontalScrollView implemen
    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 @@ public class ContactDetailFragmentCarousel extends HorizontalScrollView implemen
                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 @@ public class ContactDetailFragmentCarousel extends HorizontalScrollView implemen
            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 @@ public class ContactDetailFragmentCarousel extends HorizontalScrollView implemen
        }
        return false;
    }

    private boolean isAttachedToWindow() {
        return getWindowToken() != null;
    }
}
+41 −4
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import com.android.contacts.NfcHandler;
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 @@ public class ContactDetailLayoutController {

    private ContactLoader.Result mContactData;

    private boolean mTabCarouselIsAnimating;
    private boolean mContactHasUpdates;

    private LayoutMode mLayoutMode;
@@ -176,6 +179,7 @@ public class ContactDetailLayoutController {
                }

                mTabCarousel.setListener(mTabCarouselListener);
                mTabCarousel.restoreCurrentTab(currentPageIndex);
                mDetailFragment.setVerticalScrollListener(
                        new VerticalScrollListener(TAB_INDEX_DETAIL));
                mUpdatesFragment.setVerticalScrollListener(
@@ -211,7 +215,7 @@ public class ContactDetailLayoutController {

                mFragmentCarousel.setFragmentViews(mDetailFragmentView, mUpdatesFragmentView);
                mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment);
                mFragmentCarousel.setCurrentPage(currentPageIndex);
                mFragmentCarousel.restoreCurrentPage(currentPageIndex);
                break;
            }
        }
@@ -239,6 +243,7 @@ public class ContactDetailLayoutController {
    public void showEmptyState() {
        switch (mLayoutMode) {
            case FRAGMENT_CAROUSEL: {
                mFragmentCarousel.setCurrentPage(0);
                mFragmentCarousel.enableSwipe(false);
                mDetailFragment.showEmptyState();
                break;
@@ -323,6 +328,7 @@ public class ContactDetailLayoutController {
                break;
            case FRAGMENT_CAROUSEL: {
                // Disable swipe so only the detail fragment shows
                mFragmentCarousel.setCurrentPage(0);
                mFragmentCarousel.enableSwipe(false);
                break;
            }
@@ -449,12 +455,14 @@ public class ContactDetailLayoutController {
                    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 @@ public class ContactDetailLayoutController {
        }
    }

    /**
     * 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 @@ public class ContactDetailLayoutController {
                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
+23 −9
Original line number Diff line number Diff line
@@ -63,6 +63,7 @@ public class ContactDetailTabCarousel extends HorizontalScrollView implements On

    private int mTabDisplayLabelHeight;

    private boolean mScrollToCurrentTab = false;
    private int mLastScrollPosition;

    private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
@@ -103,12 +104,7 @@ public class ContactDetailTabCarousel extends HorizontalScrollView implements On
        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 @@ public class ContactDetailTabCarousel extends HorizontalScrollView implements On
                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) {
@@ -173,6 +178,17 @@ public class ContactDetailTabCarousel extends HorizontalScrollView implements On
        updateAlphaLayers();
    }

    /**
     * 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
@@ -225,8 +241,6 @@ public class ContactDetailTabCarousel extends HorizontalScrollView implements On
     * 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 @@ public class ContactDetailTabCarousel extends HorizontalScrollView implements On
        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);
    }