Animate show/hide updates
Also fixes the vertical text position which was wrong due to the shadow
Bug:5268733
Bug:5204655
Change-Id: I011a482500e13b1b189c7e27dbcd40e2e1f42318
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 2e59f71..b7cc87d 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -1265,6 +1265,21 @@
mLoadInvitableAccountTypes = loadInvitableAccountTypes;
}
+ /**
+ * Sets whether to load stream items. Will trigger a reload if the value has changed.
+ * At the moment, this is only used for debugging purposes
+ */
+ public void setLoadStreamItems(boolean value) {
+ if (mLoadStreamItems != value) {
+ mLoadStreamItems = value;
+ onContentChanged();
+ }
+ }
+
+ public boolean getLoadStreamItems() {
+ return mLoadStreamItems;
+ }
+
public Uri getLookupUri() {
return mLookupUri;
}
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index b353a0b..601e9fb 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -43,6 +43,7 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@@ -56,6 +57,9 @@
public class ContactDetailActivity extends ContactsActivity {
private static final String TAG = "ContactDetailActivity";
+ /** Shows a toogle button for hiding/showing updates. Don't submit with true */
+ private static final boolean DEBUG_TRANSITIONS = false;
+
/**
* Boolean intent key that specifies whether pressing the "up" affordance in this activity
* should cause it to finish itself or launch an intent to bring the user back to a specific
@@ -129,6 +133,19 @@
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.star, menu);
+ if (DEBUG_TRANSITIONS) {
+ final MenuItem toggleSocial =
+ menu.add(mLoaderFragment.getLoadStreamItems() ? "less" : "more");
+ toggleSocial.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ toggleSocial.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ mLoaderFragment.toggleLoadStreamItems();
+ invalidateOptionsMenu();
+ return false;
+ }
+ });
+ }
return true;
}
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index b87edbd..de08c4c 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -55,7 +55,6 @@
import com.android.contacts.preference.DisplayOptionsPreferenceFragment;
import com.android.contacts.util.AccountFilterUtil;
import com.android.contacts.util.AccountPromptUtils;
-import com.android.contacts.util.AccountSelectionUtil;
import com.android.contacts.util.AccountsListAdapter;
import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
import com.android.contacts.util.Constants;
@@ -88,6 +87,7 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@@ -111,6 +111,9 @@
private static final String TAG = "PeopleActivity";
+ /** Shows a toogle button for hiding/showing updates. Don't submit with true */
+ private static final boolean DEBUG_TRANSITIONS = false;
+
// These values needs to start at 2. See {@link ContactEntryListFragment}.
private static final int SUBACTIVITY_NEW_CONTACT = 2;
private static final int SUBACTIVITY_EDIT_CONTACT = 3;
@@ -1324,6 +1327,21 @@
});
addGroup.setActionView(mAddGroupImageView);
}
+
+ if (DEBUG_TRANSITIONS && mContactDetailLoaderFragment != null) {
+ final MenuItem toggleSocial =
+ menu.add(mContactDetailLoaderFragment.getLoadStreamItems() ? "less" : "more");
+ toggleSocial.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ toggleSocial.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ mContactDetailLoaderFragment.toggleLoadStreamItems();
+ invalidateOptionsMenu();
+ return false;
+ }
+ });
+ }
+
return true;
}
diff --git a/src/com/android/contacts/detail/CarouselTab.java b/src/com/android/contacts/detail/CarouselTab.java
index cdcf6b2..9331bed 100644
--- a/src/com/android/contacts/detail/CarouselTab.java
+++ b/src/com/android/contacts/detail/CarouselTab.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
+import android.view.ViewPropertyAnimator;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -31,7 +32,10 @@
private static final String TAG = CarouselTab.class.getSimpleName();
+ private static final long FADE_TRANSITION_TIME = 150;
+
private TextView mLabelView;
+ private View mLabelBackgroundView;
/**
* This view adds an alpha layer over the entire tab.
@@ -55,6 +59,8 @@
mLabelView = (TextView) findViewById(R.id.label);
mLabelView.setClickable(true);
+ mLabelBackgroundView = findViewById(R.id.label_background);
+
mAlphaLayer = findViewById(R.id.alpha_overlay);
mTouchInterceptLayer = findViewById(R.id.touch_intercept_overlay);
}
@@ -90,4 +96,20 @@
public void setAlphaLayerValue(float alpha) {
ContactDetailDisplayUtils.setAlphaOnViewBackground(mAlphaLayer, alpha);
}
+
+ public void fadeInLabelViewAnimator(int startDelay, boolean fadeBackground) {
+ final ViewPropertyAnimator labelAnimator = mLabelView.animate();
+ mLabelView.setAlpha(0.0f);
+ labelAnimator.alpha(1.0f);
+ labelAnimator.setStartDelay(startDelay);
+ labelAnimator.setDuration(FADE_TRANSITION_TIME);
+
+ if (fadeBackground) {
+ final ViewPropertyAnimator backgroundAnimator = mLabelBackgroundView.animate();
+ mLabelBackgroundView.setAlpha(0.0f);
+ backgroundAnimator.alpha(1.0f);
+ backgroundAnimator.setStartDelay(startDelay);
+ backgroundAnimator.setDuration(FADE_TRANSITION_TIME);
+ }
+ }
}
diff --git a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
index 756b1c7..0c3e6ac 100644
--- a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
@@ -23,6 +23,7 @@
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewPropertyAnimator;
import android.view.View.OnTouchListener;
import android.widget.HorizontalScrollView;
@@ -84,7 +85,7 @@
private ViewOverlay mAboutFragment;
private ViewOverlay mUpdatesFragment;
- private View mDetailFragmentView;
+ private View mAboutFragmentView;
private View mUpdatesFragmentView;
public ContactDetailFragmentCarousel(Context context) {
@@ -157,8 +158,8 @@
/**
* Set the view containers for the detail and updates fragment.
*/
- public void setFragmentViews(View detailFragmentView, View updatesFragmentView) {
- mDetailFragmentView = detailFragmentView;
+ public void setFragmentViews(View aboutFragmentView, View updatesFragmentView) {
+ mAboutFragmentView = aboutFragmentView;
mUpdatesFragmentView = updatesFragmentView;
}
@@ -180,7 +181,7 @@
if (mUpdatesFragmentView != null) {
mUpdatesFragmentView.setVisibility(enable ? View.VISIBLE : View.GONE);
if (mCurrentPage == ABOUT_PAGE) {
- mDetailFragmentView.requestFocus();
+ mAboutFragmentView.requestFocus();
} else {
mUpdatesFragmentView.requestFocus();
}
@@ -237,7 +238,7 @@
if (!mEnableSwipe) {
return;
}
- mLastScrollPosition= l;
+ mLastScrollPosition = l;
updateAlphaLayers();
}
@@ -283,4 +284,14 @@
}
return false;
}
+
+ /**
+ * Starts an "appear" animation by moving in the "Updates" from the right.
+ */
+ public void animateAppear() {
+ final int x = Math.round((1.0f - FRAGMENT_WIDTH_SCREEN_WIDTH_FRACTION) * getWidth());
+ mUpdatesFragmentView.setTranslationX(x);
+ final ViewPropertyAnimator animator = mUpdatesFragmentView.animate();
+ animator.translationX(0.0f);
+ }
}
diff --git a/src/com/android/contacts/detail/ContactDetailLayoutController.java b/src/com/android/contacts/detail/ContactDetailLayoutController.java
index 74811e4..cd479ca 100644
--- a/src/com/android/contacts/detail/ContactDetailLayoutController.java
+++ b/src/com/android/contacts/detail/ContactDetailLayoutController.java
@@ -65,6 +65,7 @@
private final LayoutInflater mLayoutInflater;
private final FragmentManager mFragmentManager;
+ private View mViewContainer;
private ContactDetailFragment mDetailFragment;
private ContactDetailUpdatesFragment mUpdatesFragment;
@@ -84,6 +85,7 @@
private Uri mContactUri;
private boolean mTabCarouselIsAnimating;
+
private boolean mContactHasUpdates;
private LayoutMode mLayoutMode;
@@ -104,6 +106,7 @@
mContactDetailFragmentListener = contactDetailFragmentListener;
// Retrieve views in case this is view pager and carousel mode
+ mViewContainer = viewContainer;
mViewPager = (ViewPager) viewContainer.findViewById(R.id.pager);
mTabCarousel = (ContactDetailTabCarousel) viewContainer.findViewById(R.id.tab_carousel);
@@ -228,7 +231,7 @@
// Setup the layout if we already have a saved state
if (savedState != null) {
if (mContactHasUpdates) {
- showContactWithUpdates();
+ showContactWithUpdates(false);
} else {
showContactWithoutUpdates();
}
@@ -236,10 +239,17 @@
}
public void setContactData(ContactLoader.Result data) {
+ final Boolean contactHadUpdates;
+ if (mContactData == null) {
+ contactHadUpdates = null;
+ } else {
+ contactHadUpdates = mContactHasUpdates;
+ }
mContactData = data;
mContactHasUpdates = !data.getStreamItems().isEmpty();
if (mContactHasUpdates) {
- showContactWithUpdates();
+ showContactWithUpdates(
+ contactHadUpdates != null && contactHadUpdates.booleanValue() == false);
} else {
showContactWithoutUpdates();
}
@@ -277,7 +287,7 @@
* Setup the layout for the contact with updates.
* TODO: Clean up this method so it's easier to understand.
*/
- private void showContactWithUpdates() {
+ private void showContactWithUpdates(boolean animateStateChange) {
if (mContactData == null) {
return;
}
@@ -288,6 +298,10 @@
switch (mLayoutMode) {
case TWO_COLUMN: {
+ // This is screen is very hard to animate properly, because there is such a hard
+ // cut from the regular version. A proper animation would have to reflow text and
+ // move things around. No animation for now
+
// Set the contact data (hide the static photo because the photo will already be in
// the header that scrolls with contact details).
mDetailFragment.setShowStaticPhoto(false);
@@ -307,11 +321,18 @@
resetViewPager();
resetTabCarousel();
}
+ if (!isDifferentContact && animateStateChange) {
+ mTabCarousel.animateAppear(mViewContainer.getWidth(),
+ mDetailFragment.getFirstListItemOffset());
+ }
break;
}
case FRAGMENT_CAROUSEL: {
// Allow swiping between all fragments
mFragmentCarousel.enableSwipe(true);
+ if (!isDifferentContact && animateStateChange) {
+ mFragmentCarousel.animateAppear();
+ }
break;
}
default:
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 186cedd..6cd48e3 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -20,13 +20,14 @@
import com.android.contacts.R;
import com.android.contacts.util.PhoneCapabilityTester;
-import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
+import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
+import android.view.ViewPropertyAnimator;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.TextView;
@@ -39,6 +40,9 @@
private static final String TAG = ContactDetailTabCarousel.class.getSimpleName();
+ private static final int TRANSITION_TIME = 200;
+ private static final int TRANSITION_MOVE_IN_TIME = 150;
+
private static final int TAB_INDEX_ABOUT = 0;
private static final int TAB_INDEX_UPDATES = 1;
private static final int TAB_COUNT = 2;
@@ -55,14 +59,16 @@
private ImageView mPhotoView;
private TextView mStatusView;
private ImageView mStatusPhotoView;
- private boolean mHasPhoto;
private OnClickListener mPhotoClickListener;
private Listener mListener;
private int mCurrentTab = TAB_INDEX_ABOUT;
+ private View mTabAndShadowContainer;
+ private View mShadow;
private CarouselTab mAboutTab;
+ private View mTabDivider;
private CarouselTab mUpdatesTab;
/** Last Y coordinate of the carousel when the tab at the given index was selected */
@@ -107,19 +113,26 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ mTabAndShadowContainer = findViewById(R.id.tab_and_shadow_container);
mAboutTab = (CarouselTab) findViewById(R.id.tab_about);
mAboutTab.setLabel(mContext.getString(R.string.contactDetailAbout));
+ mTabDivider = findViewById(R.id.tab_divider);
+
mUpdatesTab = (CarouselTab) findViewById(R.id.tab_update);
mUpdatesTab.setLabel(mContext.getString(R.string.contactDetailUpdates));
mAboutTab.enableTouchInterceptor(mAboutTabTouchInterceptListener);
mUpdatesTab.enableTouchInterceptor(mUpdatesTabTouchInterceptListener);
+ mShadow = findViewById(R.id.shadow);
+
// Retrieve the photo view for the "about" tab
+ // TODO: This should be moved down to mAboutTab, so that it hosts its own controls
mPhotoView = (ImageView) mAboutTab.findViewById(R.id.photo);
// Retrieve the social update views for the "updates" tab
+ // TODO: This should be moved down to mUpdatesTab, so that it hosts its own controls
mStatusView = (TextView) mUpdatesTab.findViewById(R.id.status);
mStatusPhotoView = (ImageView) mUpdatesTab.findViewById(R.id.status_photo);
}
@@ -128,22 +141,31 @@
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
// Compute the width of a tab as a fraction of the screen width
- int tabWidth = (int) (mTabWidthScreenWidthFraction * screenWidth);
+ int tabWidth = Math.round(mTabWidthScreenWidthFraction * screenWidth);
// Find the allowed scrolling length by subtracting the current visible screen width
// from the total length of the tabs.
mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth;
- int tabHeight = (int) (screenWidth * mTabHeightScreenWidthFraction) + mTabShadowHeight;
+ int tabHeight = Math.round(screenWidth * mTabHeightScreenWidthFraction) + mTabShadowHeight;
// 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 * tabWidth, MeasureSpec.EXACTLY),
+
+ // add 1 dip of seperation between the tabs
+ final int seperatorPixels =
+ (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
+ getResources().getDisplayMetrics()) + 0.5f);
+
+ child.measure(
+ MeasureSpec.makeMeasureSpec(
+ TAB_COUNT * tabWidth +
+ (TAB_COUNT - 1) * seperatorPixels, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(tabHeight, MeasureSpec.EXACTLY));
}
- mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight;
+ mAllowedVerticalScrollLength = tabHeight - mTabDisplayLabelHeight - mTabShadowHeight;
setMeasuredDimension(
resolveSize(screenWidth, widthMeasureSpec),
resolveSize(tabHeight, heightMeasureSpec));
@@ -177,6 +199,105 @@
}
};
+ /**
+ * Does in "appear" animation to allow a seamless transition from
+ * the "No updates" mode.
+ * @param width Width of the container. As we haven't been layed out yet, we can't know
+ * @param scrollOffset The offset by how far we scrolled, where 0=not scrolled, -x=scrolled by
+ * x pixels, Integer.MIN_VALUE=scrolled so far that the image is not visible in "no updates"
+ * mode of this screen
+ */
+ public void animateAppear(int width, int scrollOffset) {
+ final float photoHeight = mTabHeightScreenWidthFraction * width;
+ final boolean animateZoomAndFade;
+ int pixelsToScrollVertically = 0;
+
+ // Depending on how far we are scrolled down, there is one of three animations:
+ // - Zoom and fade the picture (if it is still visible)
+ // - Scroll, zoom and fade (if the picture is mostly invisible and we now have a
+ // bigger visible region due to the pinning)
+ // - Just scroll if the picture is completely invisible. This time, no zoom is needed
+ if (scrollOffset == Integer.MIN_VALUE) {
+ // animate in completely by scrolling. no need for zooming here
+ pixelsToScrollVertically = mTabDisplayLabelHeight;
+ animateZoomAndFade = false;
+ } else {
+ final int pixelsOfPhotoLeft = Math.round(photoHeight) + scrollOffset;
+ if (pixelsOfPhotoLeft > mTabDisplayLabelHeight) {
+ // nothing to scroll
+ pixelsToScrollVertically = 0;
+ } else {
+ pixelsToScrollVertically = mTabDisplayLabelHeight - pixelsOfPhotoLeft;
+ }
+ animateZoomAndFade = true;
+ }
+
+ if (pixelsToScrollVertically != 0) {
+ // We can't animate ourselves here, because our own translation is needed for the user's
+ // scrolling. Instead, we use our only child. As we are transparent, that is just as
+ // good
+ mTabAndShadowContainer.setTranslationY(-pixelsToScrollVertically);
+ final ViewPropertyAnimator animator = mTabAndShadowContainer.animate();
+ animator.translationY(0.0f);
+ animator.setDuration(TRANSITION_MOVE_IN_TIME);
+ }
+
+ if (animateZoomAndFade) {
+ // Hack: We have two types of possible layouts:
+ // If the picture is square, it is square in both "with updates" and "without updates"
+ // --> no need for scale animation here
+ // example: 10inch tablet portrait
+ // If the picture is non-square, it is full-width in "without updates" and something
+ // arbitrary in "with updates"
+ // --> do animation with container
+ // example: 4.6inch phone portrait
+ final boolean squarePicture =
+ mTabWidthScreenWidthFraction == mTabHeightScreenWidthFraction;
+ final int firstTransitionTime;
+ if (squarePicture) {
+ firstTransitionTime = 0;
+ } else {
+ // For x, we need to scale our container so we'll animate the whole tab
+ // (unfortunately, we need to have the text invisible during this transition as it
+ // would also be stretched)
+ float revScale = 1.0f/mTabWidthScreenWidthFraction;
+ mAboutTab.setScaleX(revScale);
+ mAboutTab.setPivotX(0.0f);
+ final ViewPropertyAnimator aboutAnimator = mAboutTab.animate();
+ aboutAnimator.setDuration(TRANSITION_TIME);
+ aboutAnimator.scaleX(1.0f);
+
+ // For y, we need to scale only the picture itself because we want it to be cropped
+ mPhotoView.setScaleY(revScale);
+ mPhotoView.setPivotY(photoHeight * 0.5f);
+ final ViewPropertyAnimator photoAnimator = mPhotoView.animate();
+ photoAnimator.setDuration(TRANSITION_TIME);
+ photoAnimator.scaleY(1.0f);
+ firstTransitionTime = TRANSITION_TIME;
+ }
+
+ // Animate in the labels after the above transition is finished
+ mAboutTab.fadeInLabelViewAnimator(firstTransitionTime, true);
+ mUpdatesTab.fadeInLabelViewAnimator(firstTransitionTime, false);
+
+ final float pixelsToTranslate = (1.0f - mTabWidthScreenWidthFraction) * width;
+ // Views to translate
+ for (View view : new View[] { mUpdatesTab, mTabDivider }) {
+ view.setTranslationX(pixelsToTranslate);
+ final ViewPropertyAnimator translateAnimator = view.animate();
+ translateAnimator.translationX(0.0f);
+ translateAnimator.setDuration(TRANSITION_TIME);
+ }
+
+ // Another hack: If the picture is square, there is no shadow in "Without updates"
+ // --> fade it in after the translations are done
+ if (squarePicture) {
+ mShadow.setAlpha(0.0f);
+ mShadow.animate().setStartDelay(TRANSITION_TIME).alpha(1.0f);
+ }
+ }
+ }
+
private void updateAlphaLayers() {
mAboutTab.setAlphaLayerValue(mLastScrollPosition * MAX_ALPHA /
mAllowedHorizontalScrollLength);
@@ -290,7 +411,6 @@
if (contactData == null) {
return;
}
- mHasPhoto = contactData.getPhotoUri() != null;
// TODO: Move this into the {@link CarouselTab} class when the updates fragment code is more
// finalized
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
index 008aff8..ddccfe6 100644
--- a/src/com/android/contacts/detail/ContactLoaderFragment.java
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -407,4 +407,18 @@
mContext, mLookupUri, mCustomRingtone);
mContext.startService(intent);
}
+
+ /** Toggles whether to load stream items. Just for debugging */
+ public void toggleLoadStreamItems() {
+ Loader<ContactLoader.Result> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
+ ContactLoader loader = (ContactLoader) loaderObj;
+ loader.setLoadStreamItems(!loader.getLoadStreamItems());
+ }
+
+ /** Returns whether to load stream items. Just for debugging */
+ public boolean getLoadStreamItems() {
+ Loader<ContactLoader.Result> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
+ ContactLoader loader = (ContactLoader) loaderObj;
+ return loader != null && loader.getLoadStreamItems();
+ }
}