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

Commit 0cda9aeb authored by Paul Soulos's avatar Paul Soulos
Browse files

Adds fancier animation to ExpandingEntryCardView

Bug: 16218702
Change-Id: I2b3d440b3cedf48becb9f82c8fe67f903f8611c8
parent 81cc3b3d
Loading
Loading
Loading
Loading
+24 −53
Original line number Diff line number Diff line
@@ -18,65 +18,36 @@
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:id="@+id/content_scroller">
    android:id="@+id/content_scroller"
    android:background="@color/card_margin_color">

    <!-- All the cards should be inserted into this LinearLayout -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:id="@+id/card_container"
        android:paddingTop="@dimen/first_card_marginTop"
        android:background="@color/card_margin_color"  >
        android:id="@+id/card_container" >

        <!-- We cannot set the border directly on ExpandingEntryCardView without it looking
             funny because of the card's elevation value. So we need a parent FrameLayout -->
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/quickcontact_card_border">
        <com.android.contacts.quickcontact.ExpandingEntryCardView
            style="@style/ExpandingEntryCardStyle"
            android:id="@+id/no_contact_data_card"
            android:visibility="gone" />
        </FrameLayout>

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/quickcontact_card_border">
        <com.android.contacts.quickcontact.ExpandingEntryCardView
            style="@style/ExpandingEntryCardStyle"
            android:id="@+id/communication_card"
            android:visibility="gone" />
        </FrameLayout>

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/quickcontact_card_border">
        <com.android.contacts.quickcontact.ExpandingEntryCardView
            style="@style/ExpandingEntryCardStyle"
            android:id="@+id/recent_card"
            android:visibility="gone" />
        </FrameLayout>

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/quickcontact_card_border">
        <com.android.contacts.quickcontact.ExpandingEntryCardView
            style="@style/ExpandingEntryCardStyle"
            android:id="@+id/about_card"
            android:visibility="gone" />
        </FrameLayout>

        <!-- Fill the rest of the LinearLayout with the correct background color -->
        <View
            android:id="@+id/card_empty_space"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/card_margin_color" />

    </LinearLayout>

+128 −88
Original line number Diff line number Diff line
@@ -15,11 +15,6 @@
 */
package com.android.contacts.quickcontact;

import com.android.contacts.R;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -27,18 +22,25 @@ import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.transition.ChangeBounds;
import android.transition.ChangeScroll;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.Transition.TransitionListener;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.android.contacts.R;

import java.util.ArrayList;
import java.util.List;

@@ -48,6 +50,11 @@ import java.util.List;
public class ExpandingEntryCardView extends LinearLayout {

    private static final String TAG = "ExpandingEntryCardView";
    private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200;
    private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100;

    public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300;
    public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300;

    /**
     * Entry data.
@@ -150,6 +157,7 @@ public class ExpandingEntryCardView extends LinearLayout {

    public interface ExpandingEntryCardViewListener {
        void onCollapse(int heightDelta);
        void onExpand(int heightDelta);
    }

    private View mExpandCollapseButton;
@@ -171,6 +179,8 @@ public class ExpandingEntryCardView extends LinearLayout {
    private int mThemeColor;
    private ColorFilter mThemeColorFilter;
    private boolean mIsAlwaysExpanded;
    /** The ViewGroup to run the expand/collapse animation on */
    private ViewGroup mAnimationViewGroup;

    private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
        @Override
@@ -214,7 +224,7 @@ public class ExpandingEntryCardView extends LinearLayout {
     */
    public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
            boolean isExpanded, boolean isAlwaysExpanded,
            ExpandingEntryCardViewListener listener) {
            ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) {
        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
        mIsExpanded = isExpanded;
        mIsAlwaysExpanded = isAlwaysExpanded;
@@ -235,6 +245,7 @@ public class ExpandingEntryCardView extends LinearLayout {
            mCollapsedEntriesCount = mEntries.size();
        }
        mListener = listener;
        mAnimationViewGroup = animationViewGroup;

        if (mIsExpanded) {
            updateExpandCollapseButton(getCollapseButtonText());
@@ -300,6 +311,12 @@ public class ExpandingEntryCardView extends LinearLayout {

    private void addEntry(View entry) {
        if (mEntriesViewGroup.getChildCount() > 0) {
            mEntriesViewGroup.addView(createSeparator(entry));
        }
        mEntriesViewGroup.addView(entry);
    }

    private View createSeparator(View entry) {
        View separator = new View(getContext());
        separator.setBackgroundColor(getResources().getColor(
                R.color.expanding_entry_card_item_separator_color));
@@ -326,9 +343,7 @@ public class ExpandingEntryCardView extends LinearLayout {
            layoutParams.leftMargin = marginStart;
        }
        separator.setLayoutParams(layoutParams);
            mEntriesViewGroup.addView(separator);
        }
        mEntriesViewGroup.addView(entry);
        return separator;
    }

    private CharSequence getExpandButtonText() {
@@ -543,77 +558,102 @@ public class ExpandingEntryCardView extends LinearLayout {
    }

    private void expand() {
        final int startingHeight = mEntriesViewGroup.getHeight();
        ChangeBounds boundsTransition = new ChangeBounds();
        boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);

        mIsExpanded = true;
        // In order to insert new entries, we may need to inflate them for the first time
        inflateAllEntries(LayoutInflater.from(getContext()));
        insertEntriesIntoViewGroup();
        updateExpandCollapseButton(getCollapseButtonText());
        Fade fadeIn = new Fade(Fade.IN);
        fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
        fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);

        TransitionSet transitionSet = new TransitionSet();
        transitionSet.addTransition(boundsTransition);
        transitionSet.addTransition(fadeIn);

        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
                this : mAnimationViewGroup;

        transitionSet.addListener(new TransitionListener() {
            @Override
            public void onTransitionStart(Transition transition) {
                // The listener is used to turn off suppressing, the proper delta is not necessary
                mListener.onExpand(0);
            }

            @Override
            public void onTransitionEnd(Transition transition) {
            }

        // When expanding, all the TextViews haven't been laid out yet. Therefore,
        // calling measure() would return an incorrect result. Therefore, we need a pre draw
        // listener.
        final ViewTreeObserver observer = mEntriesViewGroup.getViewTreeObserver();
        observer.addOnPreDrawListener(new OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                if (observer.isAlive()) {
                    mEntriesViewGroup.getViewTreeObserver().removeOnPreDrawListener(this);
            public void onTransitionCancel(Transition transition) {
            }
                createExpandAnimator(startingHeight, mEntriesViewGroup.getHeight()).start();
                // Do not draw the final frame of the animation immediately.
                return false;

            @Override
            public void onTransitionPause(Transition transition) {
            }

            @Override
            public void onTransitionResume(Transition transition) {
            }
        });

        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);

        mIsExpanded = true;
        // In order to insert new entries, we may need to inflate them for the first time
        inflateAllEntries(LayoutInflater.from(getContext()));
        insertEntriesIntoViewGroup();
        updateExpandCollapseButton(getCollapseButtonText());
    }

    private void collapse() {
        int startingHeight = mEntriesViewGroup.getHeight();
        int finishHeight = measureCollapsedViewGroupHeight();
        mListener.onCollapse(startingHeight - finishHeight);

        final int startingHeight = mEntriesViewGroup.getMeasuredHeight();
        mIsExpanded = false;
        updateExpandCollapseButton(getExpandButtonText());
        createExpandAnimator(startingHeight, finishHeight).start();

        final ChangeBounds boundsTransition = new ChangeBounds();
        boundsTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);

        final ChangeScroll scrollTransition = new ChangeScroll();
        scrollTransition.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);

        TransitionSet transitionSet = new TransitionSet();
        transitionSet.addTransition(boundsTransition);
        transitionSet.addTransition(scrollTransition);

        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
                this : mAnimationViewGroup;

        boundsTransition.addListener(new TransitionListener() {
            @Override
            public void onTransitionStart(Transition transition) {
                /*
                 * onTransitionStart is called after the view hierarchy has been changed but before
                 * the animation begins.
                 */
                int finishingHeight = mEntriesViewGroup.getMeasuredHeight();
                mListener.onCollapse(startingHeight - finishingHeight);
            }

    private int measureCollapsedViewGroupHeight() {
        if (mCollapsedEntriesCount == 0) {
            return 0;
            @Override
            public void onTransitionEnd(Transition transition) {
            }
        final View bottomCollapsedView = mEntryViews.get(mCollapsedEntriesCount - 1).get(0);
        return bottomCollapsedView.getTop() + bottomCollapsedView.getHeight();

            @Override
            public void onTransitionCancel(Transition transition) {
            }

    /**
     * Create ValueAnimator that performs an expand animation on the content LinearLayout.
     *
     * The animation needs to be performed manually using a ValueAnimator, since LinearLayout
     * doesn't have a single set-able height property (ie, no setHeight()).
     */
    private ValueAnimator createExpandAnimator(int start, int end) {
        ValueAnimator animator = ValueAnimator.ofInt(start, end);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int value = (Integer) valueAnimator.getAnimatedValue();
                ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams();
                layoutParams.height = value;
                mEntriesViewGroup.setLayoutParams(layoutParams);
            public void onTransitionPause(Transition transition) {
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                insertEntriesIntoViewGroup();
                // Now that the animation is done, stop using a fixed height.
                ViewGroup.LayoutParams layoutParams = mEntriesViewGroup.getLayoutParams();
                layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
                mEntriesViewGroup.setLayoutParams(layoutParams);
            public void onTransitionResume(Transition transition) {
            }
        });
        return animator;

        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);

        insertEntriesIntoViewGroup();
    }

    /**
+14 −5
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;
import android.widget.Toolbar;

@@ -320,6 +321,11 @@ public class QuickContactActivity extends ContactsActivity {
        public void onCollapse(int heightDelta) {
            mScroller.prepareForShrinkingScrollChild(heightDelta);
        }

        @Override
        public void onExpand(int heightDelta) {
            mScroller.prepareForExpandingScrollChild();
        }
    };

    /**
@@ -493,11 +499,12 @@ public class QuickContactActivity extends ContactsActivity {

        mMaterialColorMapUtils = new MaterialColorMapUtils(getResources());

        mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);

        mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
        mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card);
        mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
        mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
        mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);

        mNoContactDetailsCard.setOnClickListener(mEntryClickHandler);
        mContactCard.setOnClickListener(mEntryClickHandler);
@@ -808,7 +815,8 @@ public class QuickContactActivity extends ContactsActivity {
                    /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
                    /* isExpanded = */ mContactCard.isExpanded(),
                    /* isAlwaysExpanded = */ false,
                    mExpandingEntryCardViewListener);
                    mExpandingEntryCardViewListener,
                    mScroller);
            mContactCard.setVisibility(View.VISIBLE);
        } else {
            mContactCard.setVisibility(View.GONE);
@@ -840,7 +848,8 @@ public class QuickContactActivity extends ContactsActivity {
                /* numInitialVisibleEntries = */ 1,
                /* isExpanded = */ true,
                /* isAlwaysExpanded = */ true,
                mExpandingEntryCardViewListener);
                mExpandingEntryCardViewListener,
                mScroller);

        if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) {
            initializeNoContactDetailCard();
@@ -888,7 +897,7 @@ public class QuickContactActivity extends ContactsActivity {
        final PorterDuffColorFilter greyColorFilter =
                new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP);
        mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true,
                /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener);
                /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller);
        mNoContactDetailsCard.setVisibility(View.VISIBLE);
        mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor);
        mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter);
@@ -1525,7 +1534,7 @@ public class QuickContactActivity extends ContactsActivity {
            mRecentCard.initialize(interactionsWrapper,
                    /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN,
                    /* isExpanded = */ mRecentCard.isExpanded(), /* isAlwaysExpanded = */ false,
                    mExpandingEntryCardViewListener);
                    mExpandingEntryCardViewListener, mScroller);
            mRecentCard.setVisibility(View.VISIBLE);
        }

+19 −5
Original line number Diff line number Diff line
package com.android.contacts.widget;

import com.android.contacts.R;
import com.android.contacts.quickcontact.ExpandingEntryCardView;
import com.android.contacts.test.NeededForReflection;
import com.android.contacts.util.SchedulingUtils;

@@ -86,7 +87,6 @@ public class MultiShrinkScroller extends LinearLayout {
    private MultiShrinkScrollerListener mListener;
    private TextView mLargeTextView;
    private View mPhotoTouchInterceptOverlay;
    private View mLeftOverSpaceView;
    /** Contains desired location/size of the title, once the header is fully compressed */
    private TextView mInvisiblePlaceholderTextView;
    private View mTitleGradientView;
@@ -243,7 +243,6 @@ public class MultiShrinkScroller extends LinearLayout {
        mTransparentView = findViewById(R.id.transparent_view);
        mLargeTextView = (TextView) findViewById(R.id.large_title);
        mInvisiblePlaceholderTextView = (TextView) findViewById(R.id.placeholder_textview);
        mLeftOverSpaceView = findViewById(R.id.card_empty_space);
        mListener = listener;
        mIsOpenContactSquare = isOpenContactSquare;

@@ -436,6 +435,7 @@ public class MultiShrinkScroller extends LinearLayout {
            final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
                    mMaximumHeaderHeight);
            animator.addListener(mHeaderExpandAnimationListener);
            animator.setDuration(ExpandingEntryCardView.DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
            animator.start();
            // Scroll nested scroll view to its top
            if (mScrollView.getScrollY() != 0) {
@@ -787,8 +787,7 @@ public class MultiShrinkScroller extends LinearLayout {
     * Returns the amount of mScrollViewChild that doesn't fit inside its parent.
     */
    private int getOverflowingChildViewSize() {
        final int usedScrollViewSpace = mScrollViewChild.getHeight()
                - mLeftOverSpaceView.getHeight();
        final int usedScrollViewSpace = mScrollViewChild.getHeight();
        return -getHeight() + usedScrollViewSpace + mToolbar.getLayoutParams().height;
    }

@@ -1098,11 +1097,26 @@ public class MultiShrinkScroller extends LinearLayout {
     * space at the bottom of this ViewGroup.
     */
    public void prepareForShrinkingScrollChild(int heightDelta) {
        // The Transition framework may suppress layout on the scene root and its children. If
        // mScrollView has its layout suppressed, user scrolling interactions will not display
        // correctly. By turning suppress off for mScrollView, mScrollView properly adjusts its
        // graphics as the user scrolls during the transition.
        mScrollView.suppressLayout(false);

        final int newEmptyScrollViewSpace = -getOverflowingChildViewSize() + heightDelta;
        if (newEmptyScrollViewSpace > 0 && !mIsTwoPanel) {
            final int newDesiredToolbarHeight = Math.min(mToolbar.getLayoutParams().height
                    + newEmptyScrollViewSpace, getMaximumScrollableHeaderHeight());
            ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).start();
            ObjectAnimator.ofInt(this, "toolbarHeight", newDesiredToolbarHeight).setDuration(
                    ExpandingEntryCardView.DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS).start();
        }
    }

    public void prepareForExpandingScrollChild() {
        // The Transition framework may suppress layout on the scene root and its children. If
        // mScrollView has its layout suppressed, user scrolling interactions will not display
        // correctly. By turning suppress off for mScrollView, mScrollView properly adjusts its
        // graphics as the user scrolls during the transition.
        mScrollView.suppressLayout(false);
    }
}