summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMakoto Onuki <omakoto@google.com>2010-11-18 15:42:00 -0800
committerMakoto Onuki <omakoto@google.com>2010-11-23 10:17:54 -0800
commitd2dac0fd6c7f8e9f6741e3b2d4cc333231a22082 (patch)
tree29cac79dfcc72d1ae84ed9dff529a75b1f4983e9
parent9cb0ad3cd62533bbdf1c6057d9f8f84611bac260 (diff)
downloadEmail-d2dac0fd6c7f8e9f6741e3b2d4cc333231a22082.tar.gz
Implement screen transition animation.
Bug 3137919 Change-Id: I077768bffb1eb246fdaa7d2def30c7b132566d69
-rw-r--r--proguard.flags5
-rw-r--r--res/layout-land/three_pane.xml5
-rw-r--r--res/layout-port/three_pane.xml5
-rw-r--r--res/values-xlarge-land/dimensions.xml7
-rw-r--r--res/values-xlarge-port/dimensions.xml10
-rw-r--r--src/com/android/email/activity/ThreePaneLayout.java365
6 files changed, 326 insertions, 71 deletions
diff --git a/proguard.flags b/proguard.flags
index 85b654581..747aed2be 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -23,11 +23,10 @@
-keep class * extends org.apache.james.mime4j.util.TempStorage
-# Keep names that are used only by unit tests
-
-# Any methods whose name is '*ForTest' are preserved.
+# Keep names that are used only by unit tests or by animators
-keep class ** {
*** *ForTest(...);
+ *** *Anim(...);
}
-keepclasseswithmembers class com.android.email.GroupMessagingListener {
diff --git a/res/layout-land/three_pane.xml b/res/layout-land/three_pane.xml
index 00dfa387b..596f0726e 100644
--- a/res/layout-land/three_pane.xml
+++ b/res/layout-land/three_pane.xml
@@ -17,7 +17,7 @@
<!-- ThreePaneLayout is based on LinearLayout with the orientation always horizontal -->
<!-- for landscape -->
-
+<!-- Note the width of each pane is set by code at runtime. -->
<com.android.email.activity.ThreePaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:splitMotionEvents="true"
@@ -29,20 +29,17 @@
android:id="@+id/left_pane"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="1"
/>
<fragment
android:name="com.android.email.activity.MessageListFragment"
android:id="@+id/middle_pane"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="3"
/>
<fragment
android:name="com.android.email.activity.MessageViewFragment"
android:id="@+id/right_pane"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="6"
/>
</com.android.email.activity.ThreePaneLayout>
diff --git a/res/layout-port/three_pane.xml b/res/layout-port/three_pane.xml
index a94cbf0b6..433e70990 100644
--- a/res/layout-port/three_pane.xml
+++ b/res/layout-port/three_pane.xml
@@ -17,7 +17,7 @@
<!-- ThreePaneLayout is based on LinearLayout with the orientation always horizontal -->
<!-- for portrait -->
-
+<!-- Note the width of each pane is set by code at runtime. -->
<com.android.email.activity.ThreePaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:splitMotionEvents="true"
@@ -29,7 +29,6 @@
android:id="@+id/left_pane"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="1"
/>
<fragment
@@ -37,13 +36,11 @@
android:id="@+id/middle_pane"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="2"
/>
<FrameLayout
android:id="@+id/right_pane_with_fog"
android:layout_width="0dip"
android:layout_height="match_parent"
- android:layout_weight="2"
>
<fragment
android:name="com.android.email.activity.MessageViewFragment"
diff --git a/res/values-xlarge-land/dimensions.xml b/res/values-xlarge-land/dimensions.xml
index aa0108323..cd25e73e7 100644
--- a/res/values-xlarge-land/dimensions.xml
+++ b/res/values-xlarge-land/dimensions.xml
@@ -19,4 +19,11 @@
<!-- message compose STOPSHIP not final -->
<dimen name="message_compose_paper_width">800dip</dimen>
<dimen name="message_compose_field_label_width">120dip</dimen>
+
+ <!-- XL activity dimensions -->
+
+ <!-- width of mailbox list -->
+ <dimen name="mailbox_list_width">312dip</dimen>
+ <!-- width of the message list, on the message list + message view mode. -->
+ <dimen name="message_list_width">466dip</dimen>
</resources>
diff --git a/res/values-xlarge-port/dimensions.xml b/res/values-xlarge-port/dimensions.xml
index 44b426a8e..5ad0211a4 100644
--- a/res/values-xlarge-port/dimensions.xml
+++ b/res/values-xlarge-port/dimensions.xml
@@ -19,4 +19,14 @@
<!-- message compose STOPSHIP not final -->
<dimen name="message_compose_paper_width">700dip</dimen>
<dimen name="message_compose_field_label_width">120dip</dimen>
+
+ <!-- XL activity dimensions -->
+
+ <!-- width of mailbox list -->
+ <dimen name="mailbox_list_width">216dip</dimen>
+ <!--
+ width of the message list, on the message list + message view mode.
+ (i.e. on portrait, it's the "expanded" message list.)
+ -->
+ <dimen name="message_list_width">440dip</dimen>
</resources>
diff --git a/src/com/android/email/activity/ThreePaneLayout.java b/src/com/android/email/activity/ThreePaneLayout.java
index e0c29c4a1..98e29630b 100644
--- a/src/com/android/email/activity/ThreePaneLayout.java
+++ b/src/com/android/email/activity/ThreePaneLayout.java
@@ -16,30 +16,40 @@
package com.android.email.activity;
+import com.android.email.Email;
import com.android.email.R;
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
import android.content.Context;
+import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
-// TODO Collapsing the middle pane should cancel the selection mode on message list
-// TODO Implement animation
-// TODO On STATE_PORTRAIT_MIDDLE_EXPANDED state, right pane should be pushed out, rather than
-// squished.
-// TODO Test SavedState too.
-
/**
* The "three pane" layout used on tablet.
*
* It'll encapsulate the behavioral differences between portrait mode and landscape mode.
+ *
+ * TODO Unit tests, when UX is settled.
*/
public class ThreePaneLayout extends LinearLayout implements View.OnClickListener {
+ private static final boolean ANIMATION_DEBUG = false; // DON'T SUBMIT WITH true
+
+ // STOPSHIP Make sure we're using the same parameters as gmail does
+ private static final int ANIMATION_DURATION = ANIMATION_DEBUG ? 1000 : 80;
+ private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.5f);
/** Uninitialized state -- {@link #changePaneState} hasn't been called yet. */
- private static final int STATE_LEFT_UNINITIALIZED = 0;
+ private static final int STATE_UNINITIALIZED = 0;
/** Mailbox list + message list */
private static final int STATE_LEFT_VISIBLE = 1;
@@ -55,7 +65,11 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
public static final int PANE_MIDDLE = 1 << 1;
public static final int PANE_RIGHT = 1 << 0;
- private int mPaneState = STATE_LEFT_UNINITIALIZED;
+ /** Current pane state. See {@link #changePaneState} */
+ private int mPaneState = STATE_UNINITIALIZED;
+
+ /** See {@link #changePaneState} and {@link #onFirstSizeChanged} */
+ private int mInitialPaneState = STATE_UNINITIALIZED;
private View mLeftPane;
private View mMiddlePane;
@@ -63,7 +77,34 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
// Views used only on portrait
private View mFoggedGlass;
- private View mRightWithFog;
+
+ private boolean mFirstSizeChangedDone;
+
+ /** Mailbox list width. Comes from resources. */
+ private int mMailboxListWidth;
+ /**
+ * Message list width, on:
+ * - the message list + message view mode, on landscape.
+ * - the message view + expanded message list mode, on portrait.
+ * Comes from resources.
+ */
+ private int mMessageListWidth;
+
+ /** Hold last animator to cancel. */
+ private Animator mLastAnimator;
+
+ /**
+ * Hold last animator listener to cancel. See {@link #startLayoutAnimation} for why
+ * we need both {@link #mLastAnimator} and {@link #mLastAnimatorListener}
+ */
+ private AnimatorListener mLastAnimatorListener;
+
+ // Arrays used in {@link #changePaneState}
+ private View[] mViewsLeft;
+ private View[] mViewsRight;
+ private View[] mViewsLeftMiddle;
+ private View[] mViewsMiddleRightFogged;
+ private View[] mViewsLeftMiddleFogged;
private Callback mCallback = EmptyCallback.INSTANCE;
@@ -102,33 +143,35 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
protected void onFinishInflate() {
super.onFinishInflate();
- getViews();
+ mLeftPane = findViewById(R.id.left_pane);
+ mMiddlePane = findViewById(R.id.middle_pane);
- if (!isLandscape()) {
+ mFoggedGlass = findViewById(R.id.fogged_glass);
+ if (mFoggedGlass != null) { // If it's around, it's portrait.
+ mRightPane = findViewById(R.id.right_pane_with_fog);
mFoggedGlass.setOnClickListener(this);
+ } else { // landscape
+ mRightPane = findViewById(R.id.right_pane);
}
-
- changePaneState(STATE_LEFT_VISIBLE, false);
+ mViewsLeft = new View[] {mLeftPane};
+ mViewsRight = new View[] {mRightPane};
+ mViewsLeftMiddle = new View[] {mLeftPane, mMiddlePane};
+ mViewsMiddleRightFogged = new View[] {mMiddlePane, mRightPane, mFoggedGlass};
+ mViewsLeftMiddleFogged = new View[] {mLeftPane, mMiddlePane, mFoggedGlass};
+
+ mInitialPaneState = STATE_LEFT_VISIBLE;
+
+ final Resources resources = getResources();
+ mMailboxListWidth = getResources().getDimensionPixelSize(
+ R.dimen.mailbox_list_width);
+ mMessageListWidth = getResources().getDimensionPixelSize(R.dimen.message_list_width);
}
+
public void setCallback(Callback callback) {
mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
}
- /**
- * Look for views and set to members. {@link #isLandscape} can be used after this method.
- */
- private void getViews() {
- mLeftPane = findViewById(R.id.left_pane);
- mMiddlePane = findViewById(R.id.middle_pane);
- mRightPane = findViewById(R.id.right_pane);
-
- mFoggedGlass = findViewById(R.id.fogged_glass);
- if (mFoggedGlass != null) { // If it's there, it's portrait.
- mRightWithFog = findViewById(R.id.right_pane_with_fog);
- }
- }
-
private boolean isLandscape() {
return mFoggedGlass == null;
}
@@ -145,7 +188,16 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
// Called after onFinishInflate()
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
- changePaneState(ss.mPaneState, false);
+ mInitialPaneState = ss.mPaneState;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (!mFirstSizeChangedDone) {
+ mFirstSizeChangedDone = true;
+ onFirstSizeChanged();
+ }
}
/**
@@ -188,6 +240,19 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
}
/**
+ * Before the first call to {@link #onSizeChanged}, we don't know the width of the view, so we
+ * can't layout properly. We just remember all the requests to {@link #changePaneState}
+ * until the first {@link #onSizeChanged}, at which point we actually change to the last
+ * requested state.
+ */
+ private void onFirstSizeChanged() {
+ if (mInitialPaneState != STATE_UNINITIALIZED) {
+ changePaneState(mInitialPaneState, false);
+ mInitialPaneState = STATE_UNINITIALIZED;
+ }
+ }
+
+ /**
* Show the right most pane. (i.e. message view)
*/
public void showRightPane() {
@@ -198,49 +263,105 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
if (isLandscape() && (newState == STATE_PORTRAIT_MIDDLE_EXPANDED)) {
newState = STATE_RIGHT_VISIBLE;
}
+ if (!mFirstSizeChangedDone) {
+ // Before first onSizeChanged(), we don't know the width of the view, so we can't
+ // layout properly.
+ // Just remember the new state and return.
+ mInitialPaneState = newState;
+ return;
+ }
if (newState == mPaneState) {
return;
}
+ // Just make sure the first transition doesn't animate.
+ if (mPaneState == STATE_UNINITIALIZED) {
+ animate = false;
+ }
+
final int previousVisiblePanes = getVisiblePanes();
mPaneState = newState;
- switch (mPaneState) {
- case STATE_LEFT_VISIBLE:
- mLeftPane.setVisibility(View.VISIBLE);
- if (isLandscape()) {
- mMiddlePane.setVisibility(View.VISIBLE);
- mRightPane.setVisibility(View.GONE);
- } else { // Portrait
- mMiddlePane.setVisibility(View.VISIBLE);
+ // Animate to the new state.
+ // (We still use animator even if animate == false; we just use 0 duration.)
+ final int totalWidth = getMeasuredWidth();
- mRightWithFog.setVisibility(View.GONE);
- }
- break;
- case STATE_RIGHT_VISIBLE:
- mLeftPane.setVisibility(View.GONE);
+ final int expectedMailboxLeft;
+ final int expectedMessageListWidth;
- if (isLandscape()) {
- mMiddlePane.setVisibility(View.VISIBLE);
- mRightPane.setVisibility(View.VISIBLE);
- } else { // Portrait
- mMiddlePane.setVisibility(View.GONE);
-
- mRightWithFog.setVisibility(View.VISIBLE);
- mRightPane.setVisibility(View.VISIBLE);
- mFoggedGlass.setVisibility(View.GONE);
- }
- break;
- case STATE_PORTRAIT_MIDDLE_EXPANDED:
- mLeftPane.setVisibility(View.GONE);
+ final String animatorLabel; // for debug purpose
- mMiddlePane.setVisibility(View.VISIBLE);
+ final View[] viewsToShow;
+ final View[] viewsToHide;
- mRightWithFog.setVisibility(View.VISIBLE);
- mRightPane.setVisibility(View.VISIBLE);
- mFoggedGlass.setVisibility(View.VISIBLE);
- break;
+ if (isLandscape()) { // Landscape
+ setViewWidth(mLeftPane, mMailboxListWidth);
+ setViewWidth(mRightPane, totalWidth - mMessageListWidth);
+
+ switch (mPaneState) {
+ case STATE_LEFT_VISIBLE:
+ // mailbox + message list
+ animatorLabel = "moving to [mailbox list + message list]";
+ expectedMailboxLeft = 0;
+ expectedMessageListWidth = totalWidth - mMailboxListWidth;
+ viewsToShow = mViewsLeft;
+ viewsToHide = mViewsRight;
+ break;
+ case STATE_RIGHT_VISIBLE:
+ // message list + message view
+ animatorLabel = "moving to [message list + message view]";
+ expectedMailboxLeft = -mMailboxListWidth;
+ expectedMessageListWidth = mMessageListWidth;
+ viewsToShow = mViewsRight;
+ viewsToHide = mViewsLeft;
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+
+ } else { // Portrait
+ setViewWidth(mLeftPane, mMailboxListWidth);
+ setViewWidth(mRightPane, totalWidth);
+
+ switch (mPaneState) {
+ case STATE_LEFT_VISIBLE:
+ // message list + Message view -> mailbox + message list
+ animatorLabel = "moving to [mailbox list + message list]";
+ expectedMailboxLeft = 0;
+ expectedMessageListWidth = totalWidth - mMailboxListWidth;
+ viewsToShow = mViewsLeftMiddle;
+ viewsToHide = mViewsRight;
+ break;
+ case STATE_PORTRAIT_MIDDLE_EXPANDED:
+ // mailbox + message list -> message list + message view
+ animatorLabel = "moving to [message list + message view]";
+ expectedMailboxLeft = -mMailboxListWidth;
+ expectedMessageListWidth = mMessageListWidth;
+ viewsToShow = mViewsMiddleRightFogged;
+ viewsToHide = mViewsLeft;
+ break;
+ case STATE_RIGHT_VISIBLE:
+ // message view only
+ animatorLabel = "moving to [message view]";
+ expectedMailboxLeft = -(mMailboxListWidth + mMessageListWidth);
+ expectedMessageListWidth = mMessageListWidth;
+ viewsToShow = mViewsRight;
+ viewsToHide = mViewsLeftMiddleFogged;
+ break;
+ default:
+ throw new IllegalStateException();
+ }
}
- mCallback.onVisiblePanesChanged(previousVisiblePanes);
+
+ final AnimatorListener listener = new AnimatorListener(animatorLabel, viewsToShow,
+ viewsToHide, previousVisiblePanes) ;
+
+ // Animation properties -- mailbox list left and message list width, at the same time.
+ startLayoutAnimation(animate ? ANIMATION_DURATION : 0, listener,
+ PropertyValuesHolder.ofInt(PROP_MAILBOX_LIST_LEFT,
+ getCurrentMailboxLeft(), expectedMailboxLeft),
+ PropertyValuesHolder.ofInt(PROP_MESSAGE_LIST_WIDTH,
+ getCurrentMessageListWidth(), expectedMessageListWidth)
+ );
}
/**
@@ -276,6 +397,130 @@ public class ThreePaneLayout extends LinearLayout implements View.OnClickListene
}
}
+ private void setViewWidth(View v, int value) {
+ v.getLayoutParams().width = value;
+ requestLayout();
+ }
+
+ private static final String PROP_MAILBOX_LIST_LEFT = "mailboxListLeftAnim";
+ private static final String PROP_MESSAGE_LIST_WIDTH = "messageListWidthAnim";
+
+ @SuppressWarnings("unused")
+ public void setMailboxListLeftAnim(int value) {
+ ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin = value;
+ requestLayout();
+ }
+
+ @SuppressWarnings("unused")
+ public void setMessageListWidthAnim(int value) {
+ setViewWidth(mMiddlePane, value);
+ }
+
+ private int getCurrentMailboxLeft() {
+ return ((ViewGroup.MarginLayoutParams) mLeftPane.getLayoutParams()).leftMargin;
+ }
+
+ private int getCurrentMessageListWidth() {
+ return mMiddlePane.getLayoutParams().width;
+ }
+
+ /**
+ * Helper method to start animation.
+ */
+ private void startLayoutAnimation(int duration, AnimatorListener listener,
+ PropertyValuesHolder... values) {
+ if (mLastAnimator != null) {
+ mLastAnimator.cancel();
+ }
+ if (mLastAnimatorListener != null) {
+ if (ANIMATION_DEBUG) {
+ Log.w(Email.LOG_TAG, "Anim: Cancelling last animation: " + mLastAnimator);
+ }
+ // Animator.cancel() doesn't call listener.cancel() immediately, so sometimes
+ // we end up cancelling the previous one *after* starting the next one.
+ // Directly tell the listener it's cancelled to avoid that.
+ mLastAnimatorListener.cancel();
+ }
+
+ final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
+ this, values).setDuration(duration);
+ animator.setInterpolator(INTERPOLATOR);
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+ mLastAnimator = animator;
+ mLastAnimatorListener = listener;
+ animator.start();
+ }
+
+ /**
+ * Animation listener.
+ *
+ * Update the visibility of each pane before/after an animation.
+ */
+ private class AnimatorListener implements Animator.AnimatorListener {
+ private final String mLogLabel;
+ private final View[] mViewsToShow;
+ private final View[] mViewsToHide;
+ private final int mPreviousVisiblePanes;
+
+ private boolean mCancelled;
+
+ public AnimatorListener(String logLabel, View[] viewsToShow, View[] viewsToHide,
+ int previousVisiblePanes) {
+ mLogLabel = logLabel;
+ mViewsToShow = viewsToShow;
+ mViewsToHide = viewsToHide;
+ mPreviousVisiblePanes = previousVisiblePanes;
+ }
+
+ private void log(String message) {
+ if (ANIMATION_DEBUG) {
+ Log.w(Email.LOG_TAG, "Anim: " + mLogLabel + "[" + this + "] " + message);
+ }
+ }
+
+ public void cancel() {
+ log("cancel");
+ mCancelled = true;
+ }
+
+ /**
+ * Show the about-to-become-visible panes before an animation.
+ */
+ @Override
+ public void onAnimationStart(Animator animation) {
+ log("start");
+ for (View v : mViewsToShow) {
+ v.setVisibility(View.VISIBLE);
+ }
+ mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ /**
+ * Hide the about-to-become-hidden panes after an animation.
+ */
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ return; // But they shouldn't be hidden when cancelled.
+ }
+ log("end");
+ for (View v : mViewsToHide) {
+ v.setVisibility(View.INVISIBLE);
+ }
+ mCallback.onVisiblePanesChanged(mPreviousVisiblePanes);
+ }
+ }
+
private static class SavedState extends BaseSavedState {
int mPaneState;