Create TwoPaneLayout from TriStateSplitLayout
Minor changes to the OnePane and TwoPane Controllers to hook in the layout.
Change-Id: Ie3fc73ed3fb63441d7ed9b79ce02c2f1e9f51e36
diff --git a/res/layout/two_pane_activity.xml b/res/layout/two_pane_activity.xml
new file mode 100644
index 0000000..fa6e943
--- /dev/null
+++ b/res/layout/two_pane_activity.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.mail.ui.TwoPaneLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/two_pane_activity"
+ android:orientation="horizontal"
+ android:splitMotionEvents="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <FrameLayout
+ android:id="@+id/labels_pane"
+ android:layout_width="@dimen/label_list_pane_width"
+ android:layout_height="match_parent"/>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:id="@+id/conversation_column_container">
+
+ <RelativeLayout
+ android:id="@+id/conversation_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/conversation_list_pane"
+ android:layout_alignParentTop="true" />
+
+ </RelativeLayout>
+
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/conversation_pane_container"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ android:background="@android:color/white">
+
+ <FrameLayout
+ android:id="@+id/conversation_pane"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <View
+ android:id="@+id/conversation_overlay"
+ android:background="@android:color/transparent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/conversation_error"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/no_conversations"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:visibility="gone"
+ android:background="@android:color/white"/>
+
+ </FrameLayout>
+</com.android.mail.ui.TwoPaneLayout>
+
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
index a50e6c8..3ed2bc1 100644
--- a/res/values/animation_constants.xml
+++ b/res/values/animation_constants.xml
@@ -16,6 +16,9 @@
limitations under the License.
-->
<resources>
+ <integer name="activity_slide_left_duration">475</integer>
+ <integer name="activity_slide_right_duration">525</integer>
+ <integer name="activity_collapse_duration">125</integer>
<integer name="expand_cc_bcc_dur">150</integer>
<integer name="fadein_cc_bcc_dur">150</integer>
<integer name="fade_duration">250</integer>
diff --git a/res/values/constants.xml b/res/values/constants.xml
index cac8683..3b1c677 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -16,7 +16,7 @@
limitations under the License.
-->
<resources>
- <!-- Boolean value indicating whether the table UI should be used. -->
+ <!-- Boolean value indicating whether the tablet UI should be used. -->
<integer name="use_tablet_ui">0</integer>
<integer name="conversation_list_header_mode">1</integer>
<integer name="conversation_header_mode">1</integer>
@@ -26,4 +26,17 @@
<!-- Frequency (in milliseconds) for the refresh of timestamps in conversation list fragments. -->
<integer name="timestamp_update_interval">60000</integer>
+
+ <!-- Boolean value indicating whether the conversation list be
+ removed from the UI. This is something only tablets can do,
+ so false in the default case. Set this true for tablets. -->
+ <integer name="conversation_list_collapsible">0</integer>
+ <!-- Relative weights of the different views. These three
+ determine how much of the screen width is taken by the
+ different views. For the default layout (phone), these values
+ are equal. These are only used in TwoPaneLayout, so they need
+ to be specified for sw600dp and sw720dp layouts. -->
+ <integer name="label_list_weight">1</integer>
+ <integer name="conversation_list_weight">1</integer>
+ <integer name="conversation_view_weight">1</integer>
</resources>
diff --git a/src/com/android/mail/ConversationListContext.java b/src/com/android/mail/ConversationListContext.java
index 6d27fbe..ce5e927 100644
--- a/src/com/android/mail/ConversationListContext.java
+++ b/src/com/android/mail/ConversationListContext.java
@@ -85,7 +85,7 @@
* Builds a context for a view to a Gmail folder. Note that folder may be null, in which case
* the context defaults to a view of the inbox.
*/
- private static ConversationListContext forFolder(
+ public static ConversationListContext forFolder(
Context context, Account account, String folder) {
// Mock stuff for now.
return new ConversationListContext(account, null, folder);
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index 9b32531..92b87cf 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -31,6 +31,7 @@
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
+import android.view.Window;
import android.widget.LinearLayout;
import com.android.mail.R;
@@ -58,12 +59,18 @@
private static final String SAVED_CONVERSATIONS = "saved-conversations";
// Keys for serialization of various information in Bundles.
private static final String SAVED_LIST_CONTEXT = "saved-list-context";
- private Account mAccount;
- private ActionBarView mActionBarView;
+ /**
+ * Are we on a tablet device or not.
+ */
+ public final boolean IS_TABLET_DEVICE;
+ protected Account mAccount;
+ protected ActionBarView mActionBarView;
protected final RestrictedActivity mActivity;
private ConversationSelectionSet mBatchConversations = new ConversationSelectionSet();
protected final Context mContext;
+ protected ConversationListContext mConversationListContext;
+
protected ConversationListFragment mConversationListFragment;
/**
* The current mode of the application. All changes in mode are initiated by the activity
@@ -72,16 +79,11 @@
*/
protected final ViewMode mViewMode;
- /**
- * Are we on a tablet device or not.
- */
- protected final boolean mTabletDevice;
-
public AbstractActivityController(MailActivity activity, ViewMode viewMode) {
mActivity = activity;
mViewMode = viewMode;
mContext = activity.getApplicationContext();
- mTabletDevice = Utils.useTabletUI(mContext);
+ IS_TABLET_DEVICE = Utils.useTabletUI(mContext);
}
@Override
@@ -143,6 +145,11 @@
}
@Override
+ public int getMode() {
+ return mViewMode.getMode();
+ }
+
+ @Override
public String getUnshownSubject(String subject) {
// Calculate how much of the subject is shown, and return the remaining.
return null;
@@ -242,12 +249,16 @@
}
@Override
- public void onCreate(Bundle savedState) {
+ public boolean onCreate(Bundle savedState) {
+ // Request opaque actionbar
+ mActivity.getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
+
// Initialize the action bar view.
initCustomActionBarView();
final Intent intent = mActivity.getIntent();
// TODO(viki) Choose an account here.
+ // If we cannot choose an account, we return false
// Allow shortcut keys to function for the ActionBar and menus.
mActivity.setDefaultKeyMode(Activity.DEFAULT_KEYS_SHORTCUT);
@@ -255,6 +266,7 @@
mViewMode.addListener(this);
restoreState(savedState);
+ return true;
}
@Override
@@ -487,5 +499,4 @@
// TODO(viki): Auto-generated method stub
}
-
}
diff --git a/src/com/android/mail/ui/ActivityController.java b/src/com/android/mail/ui/ActivityController.java
index 8bc1dec..8efc290 100644
--- a/src/com/android/mail/ui/ActivityController.java
+++ b/src/com/android/mail/ui/ActivityController.java
@@ -26,6 +26,7 @@
import android.view.MenuItem;
import android.view.MotionEvent;
+import com.android.mail.ConversationListContext;
import com.android.mail.ui.ViewMode.ModeChangeListener;
/**
@@ -67,6 +68,13 @@
ConversationSelectionSet getBatchConversations();
/**
+ * Return the current mode the activity is in. Values need to be matched against constants in
+ * {@link ViewMode}.
+ * @return
+ */
+ int getMode();
+
+ /**
*
*/
void handleConversationLoadError();
@@ -103,13 +111,16 @@
/**
* Called when the root activity calls onCreate. Any initialization needs to
- * be done here. This was called initialize in Gmail. Nothing. Otherwise
- * does nothing.
+ * be done here. Subclasses need to call their parents' onCreate method, since it performs
+ * valuable initialization common to all subclasses.
+ *
+ * This was called initialize in Gmail.
*
* @see android.app.Activity#onCreate
* @param savedState
+ * @return true if the controller was able to initialize successfully, false otherwise.
*/
- void onCreate(Bundle savedState);
+ boolean onCreate(Bundle savedState);
/**
* @see android.app.Activity#onCreateDialog(int, Bundle)
@@ -194,4 +205,17 @@
*/
void onWindowFocusChanged(boolean hasFocus);
+ /**
+ * Set the Action Bar icon according to the mode. The Action Bar icon can contain a back button
+ * or not. The individual controller is responsible for changing the icon based on the mode.
+ */
+ void resetActionBarIcon();
+
+ /**
+ * Show the conversation List with the list context provided here. On certain layouts, this
+ * might show more than just the conversation list. For instance, on tablets this might show
+ * the conversations along with the conversation list.
+ * @param listContext context providing information on what conversation list to display.
+ */
+ void showConversationList(ConversationListContext listContext);
}
diff --git a/src/com/android/mail/ui/ControllableActivity.java b/src/com/android/mail/ui/ControllableActivity.java
index 18b0d7b..067080a 100644
--- a/src/com/android/mail/ui/ControllableActivity.java
+++ b/src/com/android/mail/ui/ControllableActivity.java
@@ -17,6 +17,8 @@
package com.android.mail.ui;
+import com.android.mail.ui.ViewMode.ModeChangeListener;
+
/**
* A controllable activity is an Activity that has a Controller attached. This activity must be
@@ -24,6 +26,15 @@
*/
public interface ControllableActivity extends HelpCallback, RestrictedActivity {
/**
+ * Attaches the conversation list fragment to the activity controller. This callback is
+ * currently required because the Activity Controller directly calls methods on the conversation
+ * list fragment. This behavior should be modified to allow the controller to call a layout
+ * controller which knows about the fragments.
+ * @param conversationList
+ */
+ void attachConversationList(ConversationListFragment conversationList);
+
+ /**
* Returns the conversations selected by the user for performing a batch action like archive,
* delete, etc.
* @return conversations selected
@@ -35,15 +46,17 @@
* @see com.android.mail.ui.ViewMode
* @return the mode the activity is currently in.
*/
- ViewMode getViewMode();
+ int getViewMode();
/**
- * Attaches the conversation list fragment to the activity controller. This callback is
- * currently required because the Activity Controller directly calls methods on the conversation
- * list fragment. This behavior should be modified to allow the controller to call a layout
- * controller which knows about the fragments.
- * @param conversationList
+ * Sets the listener for receiving ViewMode changes.
+ * @param listener
*/
- void attachConversationList(ConversationListFragment conversationList);
+ void setViewModeListener(ModeChangeListener listener);
+ /**
+ * Removes the given listener from receiving ViewMode changes.
+ * @param listener
+ */
+ void unsetViewModeListener(ModeChangeListener listener);
}
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 1e29a61..3ec5bf4 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -56,6 +56,9 @@
private static final String LOG_TAG = new LogUtils().getLogTag();
+ // True if we are on a tablet device
+ private static boolean mTabletDevice;
+
/**
* Frequency of update of timestamps. Initialized in {@link #onCreate(Bundle)} and final
* afterwards.
@@ -65,16 +68,13 @@
private ControllableActivity mActivity;
private boolean mAnimateChanges;
-
// Control state.
private ConversationListCallbacks mCallbacks;
- private View mEmptyView;
+ private View mEmptyView;
private final Handler mHandler = new Handler();
// True if the view is in CAB (Contextual Action Bar: some conversations are selected) mode
private boolean mIsCabMode;
- // True if we are on a tablet device
- private static boolean mTabletDevice;
// List save state.
private Parcelable mListSavedState;
@@ -97,9 +97,6 @@
private ConversationListContext mViewContext;
- // Which mode is the underlying controller in?
- private ViewMode mViewMode;
-
/**
* Creates a new instance of {@link ConversationListFragment}, initialized to display
* conversation list context.
@@ -118,12 +115,8 @@
private void bindActivityInfo() {
final Activity activity = getActivity();
mCallbacks = (ConversationListCallbacks) activity;
- mViewMode = mActivity.getViewMode();
StarHandler starHandler = (StarHandler) activity;
- if (mViewMode != null) {
- mViewMode.addListener(this);
- }
-
+ mActivity.setViewModeListener(this);
mActivity.getBatchConversations().addObserver(this);
// TODO(mindyp): find some way to make the notification container more re-usable.
@@ -172,7 +165,7 @@
// The onViewModeChanged callback doesn't get called when the mode object is created, so
// force setting the mode manually this time around.
- onViewModeChanged(mViewMode.getMode());
+ onViewModeChanged(mActivity.getViewMode());
if (mActivity.isFinishing()) {
// Activity is finishing, just bail.
@@ -239,7 +232,7 @@
// Clear the adapter.
mListView.setAdapter(null);
- mViewMode.removeListener(this);
+ mActivity.unsetViewModeListener(this);
mActivity.attachConversationList(null);
mActivity.getBatchConversations().removeObserver(this);
@@ -315,7 +308,6 @@
@Override
public void onViewModeChanged(int newMode) {
// Change the divider based on view mode.
- Resources res = getView().getResources();
if (mTabletDevice) {
if (newMode == ViewMode.CONVERSATION) {
mListView.setBackgroundResource(R.drawable.panel_conversation_leftstroke);
@@ -329,7 +321,6 @@
}
}
-
/**
* Visually refresh the conversation list. This does not start a sync.
*/
@@ -355,7 +346,6 @@
}
}
-
/**
* View the message at the given position.
* @param position
diff --git a/src/com/android/mail/ui/FolderSettingsObservable.java b/src/com/android/mail/ui/FolderSettingsObservable.java
new file mode 100644
index 0000000..44983ab
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSettingsObservable.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+
+package com.android.mail.ui;
+
+import java.util.ArrayList;
+
+/**
+ * Represents folder settings and allows observers to listen for changes.
+ *
+ */
+public interface FolderSettingsObservable {
+ void registerObserver(FolderSettingsObserver observer);
+ void unregisterObserver(FolderSettingsObserver observer);
+ void notifyChanged();
+ ArrayList<String> getIncludedLabels();
+ ArrayList<String> getPartialLabels();
+ int getNumberOfSyncDays();
+ void setIncludedLabels(ArrayList<String> labels);
+ void setPartialLabels(ArrayList<String> labels);
+}
diff --git a/src/com/android/mail/ui/FolderSettingsObserver.java b/src/com/android/mail/ui/FolderSettingsObserver.java
new file mode 100644
index 0000000..df301cf
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSettingsObserver.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+
+package com.android.mail.ui;
+
+/**
+ * Observer to be notified when folder settings changed.
+ */
+public interface FolderSettingsObserver {
+ void onChanged();
+}
diff --git a/src/com/android/mail/ui/MailActivity.java b/src/com/android/mail/ui/MailActivity.java
index c2d912d..528b503 100644
--- a/src/com/android/mail/ui/MailActivity.java
+++ b/src/com/android/mail/ui/MailActivity.java
@@ -26,6 +26,7 @@
import android.view.MenuItem;
import android.view.MotionEvent;
+import com.android.mail.ui.ViewMode.ModeChangeListener;
import com.android.mail.utils.Utils;
@@ -50,9 +51,6 @@
*/
private boolean mLaunchedCleanly = false;
- /**
- * The specific view mode that we are in.
- */
private ViewMode mViewMode;
@Override
@@ -79,8 +77,8 @@
* Default implementation returns a null view mode.
*/
@Override
- public ViewMode getViewMode() {
- return mViewMode;
+ public int getViewMode() {
+ return mViewMode.getMode();
}
@Override
@@ -205,4 +203,14 @@
super.onWindowFocusChanged(hasFocus);
mController.onWindowFocusChanged(hasFocus);
}
+
+ @Override
+ public void setViewModeListener(ModeChangeListener listener) {
+ mViewMode.addListener(listener);
+ }
+
+ @Override
+ public void unsetViewModeListener(ModeChangeListener listener) {
+ mViewMode.removeListener(listener);
+ }
}
diff --git a/src/com/android/mail/ui/OnePaneController.java b/src/com/android/mail/ui/OnePaneController.java
index c096b5d..07625c2 100644
--- a/src/com/android/mail/ui/OnePaneController.java
+++ b/src/com/android/mail/ui/OnePaneController.java
@@ -17,18 +17,25 @@
package com.android.mail.ui;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.Window;
+import com.android.mail.ConversationListContext;
import com.android.mail.R;
/**
* Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
- * limited.
+ * limited. This controller also does the layout, since the layout is simpler in the one pane case.
*/
// Called OnePaneActivityController in Gmail.
public final class OnePaneController extends AbstractActivityController {
+
+ private ConversationListContext mConversationListContext;
+
/**
* @param activity
* @param viewMode
@@ -39,13 +46,22 @@
}
@Override
- public void onCreate(Bundle savedInstanceState) {
- // Request opaque actionbar
- mActivity.getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
+ public void resetActionBarIcon() {
+ final int mode = mViewMode.getMode();
+ if ((mode == ViewMode.CONVERSATION_LIST && mConversationListContext.isSearchResult())
+ || mode == ViewMode.CONVERSATION || mode == ViewMode.FOLDER_LIST) {
+ mActionBarView.setBackButton();
+ } else {
+ mActionBarView.removeBackButton();
+ }
+ }
+
+ @Override
+ public boolean onCreate(Bundle savedInstanceState) {
// Set 1-pane content view.
mActivity.setContentView(R.layout.one_pane_activity);
-
- super.onCreate(savedInstanceState);
+ // The parent class sets the correct viewmode and starts the application off.
+ return super.onCreate(savedInstanceState);
}
@Override
@@ -53,4 +69,42 @@
// TODO(viki): Auto-generated method stub
return false;
}
+
+ @Override
+ public void onViewModeChanged(int newMode) {
+ super.onViewModeChanged(newMode);
+ if (newMode == ViewMode.CONVERSATION_LIST) {
+ // TODO(viki): Get name of inbox from the Account
+ final String inboxFolder = "inbox";
+ ConversationListContext listContext = ConversationListContext.forFolder(mContext,
+ mAccount, inboxFolder);
+ showConversationList(listContext);
+ }
+
+ // We don't want to invalidate the options menu when switching to conversation
+ // mode, as it will happen when the conversation finishes loading.
+ if (newMode != ViewMode.CONVERSATION) {
+ mActivity.invalidateOptionsMenu();
+ }
+ }
+
+ @Override
+ public void showConversationList(ConversationListContext listContext) {
+ FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
+ fragmentTransaction.addToBackStack(null);
+ final boolean accountChanged = false;
+ // TODO(viki): This account transition looks strange in two pane mode. Revisit as the app
+ // is coming together and improve the look and feel.
+ final int transition = accountChanged ? FragmentTransaction.TRANSIT_FRAGMENT_FADE :
+ FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
+ fragmentTransaction.setTransition(transition);
+
+ Fragment conversationListFragment = ConversationListFragment
+ .newInstance(mConversationListContext);
+ fragmentTransaction.replace(R.id.content_pane, conversationListFragment);
+
+ fragmentTransaction.commitAllowingStateLoss();
+ resetActionBarIcon();
+ }
+
}
diff --git a/src/com/android/mail/ui/TwoPaneController.java b/src/com/android/mail/ui/TwoPaneController.java
index 56510ee..1874ed9 100644
--- a/src/com/android/mail/ui/TwoPaneController.java
+++ b/src/com/android/mail/ui/TwoPaneController.java
@@ -17,6 +17,14 @@
package com.android.mail.ui;
+import com.android.mail.ConversationListContext;
+import com.android.mail.R;
+
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.view.Window;
+
/**
* Controller for one-pane Mail activity. One Pane is used for phones, where screen real estate is
@@ -25,6 +33,8 @@
// Called OnePaneActivityController in Gmail.
public final class TwoPaneController extends AbstractActivityController {
+ private boolean mJumpToFirstConversation;
+ private TwoPaneLayout mLayout;
/**
* @param activity
@@ -35,10 +45,72 @@
// TODO(viki): Auto-generated constructor stub
}
+ /**
+ * Display the conversation list fragment.
+ * @param show
+ */
+ private void initializeConversationListFragment(boolean show) {
+ if (show) {
+ mViewMode.enterConversationListMode();
+ }
+ FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction();
+ // Use cross fading animation.
+ fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ Fragment conversationListFragment = ConversationListFragment
+ .newInstance(mConversationListContext);
+ fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment);
+ fragmentTransaction.commitAllowingStateLoss();
+ }
+
@Override
protected boolean isConversationListVisible() {
// TODO(viki): Auto-generated method stub
return false;
}
+ @Override
+ public void showConversationList(ConversationListContext context) {
+ initializeConversationListFragment(true);
+ }
+
+ @Override
+ public boolean onCreate(Bundle savedState) {
+ mActivity.setContentView(R.layout.two_pane_activity);
+
+ mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity);
+ mLayout.initializeLayout(mActivity.getApplicationContext());
+
+ // The tablet layout needs to refer to mode changes.
+ mViewMode.addListener(mLayout);
+ // The activity controller needs to listen to layout changes.
+ mLayout.setListener(this);
+
+ final boolean isParentInitialized = super.onCreate(savedState);
+ if (isParentInitialized && savedState == null) {
+ // Show a Label list, in the real application
+ // renderLabelList();
+ // In our case, we show a conversation list for everything.
+ }
+ return isParentInitialized;
+ }
+
+ @Override
+ public void onViewModeChanged(int newMode) {
+ super.onViewModeChanged(newMode);
+ if (newMode != ViewMode.CONVERSATION) {
+ // Clear this flag if the user jumps out of conversation mode
+ // before a load completes.
+ mJumpToFirstConversation = false;
+ }
+ }
+
+ @Override
+ public void resetActionBarIcon() {
+ if (mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
+ mActionBarView.removeBackButton();
+ } else {
+ mActionBarView.setBackButton();
+ }
+ }
+
}
diff --git a/src/com/android/mail/ui/TwoPaneLayout.java b/src/com/android/mail/ui/TwoPaneLayout.java
new file mode 100644
index 0000000..4529081
--- /dev/null
+++ b/src/com/android/mail/ui/TwoPaneLayout.java
@@ -0,0 +1,818 @@
+/*******************************************************************************
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *******************************************************************************/
+
+package com.android.mail.ui;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.LinearLayout;
+
+import com.android.mail.R;
+import com.android.mail.ui.ViewMode.ModeChangeListener;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+
+import java.util.ArrayList;
+
+/**
+ * This is a custom layout that manages the possible views of Gmail's large screen (read: tablet)
+ * activity, and the transitions between them.
+ *
+ * This is not intended to be a generic layout; it is specific to the {@code Fragment}s
+ * available in {@link MailActivity} and assumes their existence. It merely configures them
+ * according to the specific <i>modes</i> the {@link Activity} can be in.
+ *
+ * Currently, the layout differs in three dimensions: orientation, two aspects of view modes.
+ * This results in essentially three states: One where the labels are on the left and conversation
+ * list is on the right, and two states where the conversation list is on the left: one in which
+ * it's collapsed and another where it is not.
+ *
+ * In Label or conversation list view, conversations are hidden and labels and conversation lists
+ * are visible. This is the case in both portrait and landscape
+ *
+ * In Conversation List or Conversation View, labels are hidden, and conversation lists and
+ * conversation view is visible. This is the case in both portrait and landscape.
+ *
+ * In the Gmail source code, this was called TriStateSplitLayout
+ */
+final class TwoPaneLayout extends LinearLayout
+ implements ModeChangeListener, OnTouchListener {
+
+ /**
+ * Scaling modifier for sAnimationSlideRightDuration.
+ */
+ private static final double SLIDE_DURATION_SCALE = 2.0 / 3.0;
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+ private static final TimeInterpolator sCollapseInterpolator = new DecelerateInterpolator(2.5f);
+ private static final TimeInterpolator sLeftInterpolator = new DecelerateInterpolator(2.25f);
+ private static final TimeInterpolator sRightInterpolator = new DecelerateInterpolator(2.5f);
+
+ private static int sAnimationCollapseDuration;
+ private static int sAnimationSlideLeftDuration;
+ private static int sAnimationSlideRightDuration;
+ private static double sScaledConversationListWeight;
+ private static double sScaledLabelListWeight;
+ /**
+ * The current mode that the tablet layout is in. This is a constant integer that holds values
+ * that are {@link ViewMode} constants like {@link ViewMode#CONVERSATION}.
+ */
+ private int currentMode;
+ /**
+ * Whether or not the layout is currently in the middle of a cross-fade animation that requires
+ * custom rendering.
+ */
+ private boolean mAnimatingFade;
+
+ private Context mContext;
+ private int mConversationLeft;
+
+ private View mConversationListContainer;
+
+ private View mConversationView;
+ private View mConversationViewOverlay;
+ /** Left position of each fragment. */
+ private int mLabelsLeft;
+ private View mLabelsView;
+ private int mListAlpha;
+
+ /** Captured bitmap of each fragment. */
+ private Bitmap mListBitmap;
+ private int mListBitmapLeft;
+ /** Whether or not the conversation list can be collapsed all the way to hidden on the left.
+ * This is used only in portrait view*/
+ private boolean mListCollapsed;
+ private LayoutListener mListener;
+ private int mListLeft;
+ private Paint mListPaint;
+ private View mListView;
+ /**
+ * A handle to any out standing animations that are in progress.
+ */
+ private Animator mOutstandingAnimator;
+
+ /** Paint to be used for each fragment. */
+ private Paint mPaint;
+
+ private final AnimatorListener mConversationListListener =
+ new AnimatorListener(AnimatorListener.CONVERSATION_LIST);
+ private final AnimatorListener mCollapseListListener =
+ new AnimatorListener(AnimatorListener.COLLAPSE_LIST);
+ private final AnimatorListener mConversationListener =
+ new AnimatorListener(AnimatorListener.CONVERSATION);
+ private final AnimatorListener mUncollapseListListener =
+ new AnimatorListener(AnimatorListener.UNCOLLAPSE_LIST);
+
+ private class AnimatorListener implements Animator.AnimatorListener {
+ public static final int CONVERSATION_LIST = 1;
+ public static final int COLLAPSE_LIST = 2;
+ public static final int CONVERSATION = 3;
+ public static final int UNCOLLAPSE_LIST = 4;
+
+ /**
+ * Different animator listeners need to perform different actions on start and finish based
+ * on their type. The types are assigned at object creation using only the constants:
+ * {@link #CONVERSATION_LIST}, {@link #COLLAPSE_LIST}, {@link #CONVERSATION} or
+ * {@link #UNCOLLAPSE_LIST}
+ */
+ private final int listener_type;
+
+ /**
+ * Create an animator listener of a specific type. The types are created using the constants
+ * {@link #CONVERSATION_LIST}, {@link #COLLAPSE_LIST}, {@link #CONVERSATION} or
+ * {@link #UNCOLLAPSE_LIST}
+ * @param type
+ */
+ AnimatorListener(int type){
+ this.listener_type = type;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ LogUtils.d(LOG_TAG, "Cancelling animation (this=%s)", animation);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimatingFade = false;
+ mOutstandingAnimator = null;
+ destroyBitmaps();
+ // Now close the animation depending on the type of animator selected.
+ switch (listener_type) {
+ case CONVERSATION_LIST:
+ onFinishEnteringConversationListMode();
+ return;
+ case COLLAPSE_LIST:
+ onCollapseList();
+ return;
+ case CONVERSATION:
+ onFinishEnteringConversationMode();
+ return;
+ case UNCOLLAPSE_LIST:
+ onUncollapseList();
+ return;
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ switch (listener_type) {
+ case CONVERSATION_LIST:
+ mLabelsView.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ public TwoPaneLayout(Context context) {
+ super(context);
+ }
+
+ public TwoPaneLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TwoPaneLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Sets the {@link ViewMode} that this layout is synchronized to.
+ * @param viewMode The view mode object to listen to changes on.
+ */
+ // TODO(viki): Change this to have the ActivityController provide the viewMode only for adding
+ // as a listener.
+ public void attachToViewMode(ViewMode viewMode) {
+ viewMode.addListener(this);
+ currentMode = viewMode.getMode();
+ }
+
+ /**
+ * Captures list view.
+ */
+ private void captureListBitmaps() {
+ if (mListBitmap != null || mListView == null || mListView.getWidth() == 0
+ || mListView.getHeight() == 0) {
+ return;
+ }
+
+ try {
+ mListBitmap = Bitmap.createBitmap(mListView.getWidth(), mListView.getHeight(),
+ Config.ARGB_8888);
+ Canvas canvas = new Canvas(mListBitmap);
+ mListView.draw(canvas);
+ } catch (OutOfMemoryError e) {
+ LogUtils.e(LOG_TAG, e, "Could not create a bitmap due to OutOfMemoryError");
+ }
+ }
+
+ /**
+ * Collapses the conversation list to the left if it is in an expanded state.
+ * Only applies in portrait mode.
+ */
+ public boolean collapseList() {
+ if (mListCollapsed) {
+ return false;
+ }
+ mListCollapsed = true;
+
+ PropertyValuesHolder listLeftValues = PropertyValuesHolder.ofInt(
+ "conversationListLeft",
+ getConversationListLeft(),
+ computeConversationListLeft(computeConversationListWidth()));
+
+ startLayoutAnimation(sAnimationCollapseDuration, mCollapseListListener,
+ sCollapseInterpolator, listLeftValues);
+ return true;
+ }
+
+ /**
+ * Computes left position of the conversation list relative to its uncollapsed position.
+ * This is only relevant in a collapsible view, and will be 0 otherwise.
+ */
+ private int computeConversationListLeft(int width) {
+ if (isConversationListCollapsible()) {
+ return mListCollapsed ? -width : 0;
+
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Computes the width of the conversation list in stable state of the current mode.
+ */
+ private int computeConversationListWidth() {
+ return computeConversationListWidth(getMeasuredWidth());
+ }
+
+ /**
+ * Computes the width of the conversation list in stable state of the current mode.
+ */
+ private int computeConversationListWidth(int totalWidth) {
+ switch (currentMode) {
+ case ViewMode.CONVERSATION_LIST:
+ return totalWidth - computeLabelListWidth();
+ case ViewMode.CONVERSATION:
+ return (int) (totalWidth * sScaledConversationListWeight);
+ }
+ return 0;
+ }
+
+ /**
+ * Computes the width of the conversation pane in stable state of the current mode.
+ */
+ private int computeConversationWidth() {
+ return computeConversationWidth(getMeasuredWidth());
+ }
+
+ /**
+ * Computes the width of the conversation pane in stable state of the
+ * current mode.
+ */
+ private int computeConversationWidth(int totalWidth) {
+ switch (currentMode) {
+ case ViewMode.CONVERSATION:
+ // Fallthrough
+ case ViewMode.SEARCH_RESULTS:
+ if (isConversationListCollapsible()) {
+ return totalWidth;
+ }
+ return totalWidth - (int) (totalWidth * sScaledConversationListWeight);
+ }
+ return 0;
+ }
+
+ /**
+ * Computes the width of the label list in stable state of the current mode.
+ */
+ private int computeLabelListWidth() {
+ return computeLabelListWidth(getMeasuredWidth());
+ }
+
+ /**
+ * Computes the width of the label list in stable state of the current mode.
+ */
+ private int computeLabelListWidth(int totalWidth) {
+ return (int) (totalWidth * sScaledLabelListWeight);
+ }
+
+ /**
+ * Frees up the bitmaps.
+ */
+ private void destroyBitmaps() {
+ if (mListBitmap != null) {
+ mListBitmap.recycle();
+ mListBitmap = null;
+ }
+ }
+
+ private void dispatchConversationListVisibilityChange() {
+ if (mListener != null) {
+ // Post the visibility change using a handler, so other views
+ // will not be modified while we are performing a layout of the
+ // TriStateSplitLayout
+ final Handler handler = new Handler();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onConversationListVisibilityChanged(isConversationListVisible());
+ }
+ });
+ }
+ }
+
+ private void dispatchConversationVisibilityChanged(boolean visible) {
+ if (mListener != null) {
+ mListener.onConversationVisibilityChanged(visible);
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (!isAnimatingFade()) {
+ super.dispatchDraw(canvas);
+ return;
+ }
+
+ canvas.save();
+ canvas.translate(mLabelsLeft, 0);
+ mLabelsView.draw(canvas);
+ canvas.restore();
+
+ // The bitmap can be null if the view hasn't been drawn by the time we capture the bitmap.
+ if (mListBitmap != null) {
+ canvas.drawBitmap(mListBitmap, mListBitmapLeft, 0, mListPaint);
+ }
+
+ canvas.saveLayerAlpha(mListLeft, 0, mListLeft + mListView.getWidth(), getHeight(),
+ mListAlpha, Canvas.ALL_SAVE_FLAG);
+ canvas.translate(mListLeft, 0);
+ mListView.draw(canvas);
+ canvas.restore();
+
+ canvas.save();
+ canvas.translate(mConversationLeft, 0);
+ mConversationView.draw(canvas);
+ canvas.restore();
+ }
+
+ private void enterConversationListMode() {
+ mListView.setPadding(mListView.getPaddingLeft(), 0, mListView.getPaddingRight(),
+ mListView.getPaddingBottom());
+
+ // On the initial call, measurements may not have been done (i.e. this
+ // Layout has never been rendered), so no animation will be done.
+ if (getMeasuredWidth() == 0) {
+ mLabelsView.setVisibility(View.VISIBLE);
+ onFinishEnteringConversationListMode();
+ return;
+ }
+
+ // Slide labels in from the left.
+ setLabelListWidth(computeLabelListWidth());
+
+ // Prepare animation.
+ mAnimatingFade = true;
+ captureListBitmaps();
+ ArrayList<PropertyValuesHolder> values = Lists.newArrayList();
+
+ int labelsWidth = computeLabelListWidth();
+ values.add(PropertyValuesHolder.ofInt("labelsLeft", -labelsWidth, 0));
+
+ // Reset the relative left of the list view.
+ setConversationListLeft(0);
+
+ // Push conversation list out to fill remaining space.
+ setConversationListWidth(computeConversationListWidth());
+
+ // Fading out the conversation bitmap should finish before
+ // the final transition to the conversation list view.
+ ObjectAnimator animator = ObjectAnimator.ofInt(this, "listBitmapAlpha", 255, 0);
+ animator.setDuration((long) (sAnimationSlideRightDuration * SLIDE_DURATION_SCALE));
+ animator.setInterpolator(sRightInterpolator);
+
+ values.add(PropertyValuesHolder.ofInt("listBitmapLeft", 0, labelsWidth));
+ values.add(PropertyValuesHolder.ofInt("listLeft", 0, labelsWidth));
+ values.add(PropertyValuesHolder.ofInt("listAlpha", 0, 255));
+
+ // Slide conversation out to the right.
+ values.add(PropertyValuesHolder.ofInt("conversationLeft", mListView.getMeasuredWidth(),
+ getWidth()));
+ ObjectAnimator valuesAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
+ values.toArray(new PropertyValuesHolder[values.size()])).setDuration(
+ sAnimationSlideRightDuration);
+ valuesAnimator.setInterpolator(sRightInterpolator);
+ valuesAnimator.addListener(mConversationListListener);
+
+ mOutstandingAnimator = valuesAnimator;
+ AnimatorSet transitionSet = new AnimatorSet();
+ transitionSet.playTogether(animator, valuesAnimator);
+ transitionSet.start();
+ }
+
+ private void enterConversationMode() {
+ mConversationView.setVisibility(View.VISIBLE);
+
+ // On the initial call, measurements may not have been done (i.e. this Layout has never
+ // been rendered), so no animation will be done.
+ if (getMeasuredWidth() == 0) {
+ mListCollapsed = true;
+ onFinishEnteringConversationMode();
+ return;
+ }
+
+ // Prepare for animation.
+ mAnimatingFade = true;
+ captureListBitmaps();
+ ArrayList<PropertyValuesHolder> values = Lists.newArrayList();
+
+ // Slide labels out towards the left off screen.
+ int labelsWidth = mLabelsView.getMeasuredWidth();
+ values.add(PropertyValuesHolder.ofInt("labelsLeft", 0, -labelsWidth));
+
+ // Shrink the conversation list to make room for the conversation, and default
+ // it to collapsed in case it is collapsible.
+ mListCollapsed = true;
+ int targetWidth = computeConversationListWidth();
+ setConversationListWidth(targetWidth);
+
+ int currentListLeft = labelsWidth + getConversationListLeft();
+ int targetListLeft = computeConversationListLeft(targetWidth);
+ setConversationListLeft(targetListLeft);
+ if (currentListLeft != targetListLeft) {
+ values.add(
+ PropertyValuesHolder.ofInt("listBitmapLeft", currentListLeft, targetListLeft));
+ values.add(PropertyValuesHolder.ofInt("listBitmapAlpha", 255, 0));
+ values.add(
+ PropertyValuesHolder.ofInt("listLeft",
+ currentListLeft + mListView.getWidth() - targetWidth, targetListLeft));
+ values.add(PropertyValuesHolder.ofInt("listAlpha", 0, 255));
+ }
+
+ // Set up the conversation view.
+ // Performance note: do not animate the width of this, as it is very
+ // expensive to reflow in the WebView.
+ setConversationWidth(computeConversationWidth());
+ values.add(PropertyValuesHolder.ofInt(
+ "conversationLeft", getWidth(), targetListLeft + targetWidth));
+
+ startLayoutAnimation(sAnimationSlideLeftDuration, mConversationListener, sLeftInterpolator,
+ values.toArray(new PropertyValuesHolder[values.size()]));
+ }
+
+ /**
+ * @return The left position of the conversation list relative to its uncollapsed position.
+ * This is only relevant in a collapsible view, and will be 0 otherwise.
+ */
+ public int getConversationListLeft() {
+ return ((ViewGroup.MarginLayoutParams) mConversationListContainer.getLayoutParams())
+ .leftMargin;
+ }
+
+ /**
+ * Initializes the layout with a specific context.
+ */
+ @VisibleForTesting
+ public void initializeLayout(Context context) {
+ mContext = context;
+
+ Resources res = getResources();
+ mLabelsView = findViewById(R.id.labels_pane);
+ mConversationListContainer = findViewById(R.id.conversation_column_container);
+ mListView = findViewById(R.id.conversation_list);
+ mConversationView = findViewById(R.id.conversation_pane_container);
+ mConversationViewOverlay = findViewById(R.id.conversation_overlay);
+
+ mConversationViewOverlay.setOnTouchListener(this);
+
+ sAnimationSlideLeftDuration = res.getInteger(R.integer.activity_slide_left_duration);
+ sAnimationSlideRightDuration = res.getInteger(R.integer.activity_slide_right_duration);
+ sAnimationCollapseDuration = res.getInteger(R.integer.activity_collapse_duration);
+ final int sLabelListWeight = res.getInteger(R.integer.label_list_weight);
+ final int sConversationListWeight = res.getInteger(R.integer.conversation_list_weight);
+ final int sConversationViewWeight = res.getInteger(R.integer.conversation_view_weight);
+ sScaledLabelListWeight = (double) sLabelListWeight
+ / (sLabelListWeight + sConversationListWeight);
+ sScaledConversationListWeight = (double) sConversationListWeight
+ / (sConversationListWeight + sConversationViewWeight);
+
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setColor(android.R.color.white);
+ mListPaint = new Paint();
+ mListPaint.setAntiAlias(true);
+ }
+
+ private boolean isAnimatingFade() {
+ return mAnimatingFade;
+ }
+
+ /**
+ * @return whether the conversation list can be collapsed or not.
+ */
+ public boolean isConversationListCollapsible() {
+ return mContext.getResources().getInteger(R.integer.conversation_list_collapsible) != 0;
+ }
+
+ /**
+ * @return Whether or not the conversation list is visible on screen.
+ */
+ public boolean isConversationListVisible() {
+ return !isConversationListCollapsible() || !mListCollapsed;
+ }
+
+ /**
+ * Finalizes state after animations settle when collapsing the conversation list.
+ */
+ private void onCollapseList() {
+ mConversationViewOverlay.setVisibility(View.GONE);
+ dispatchConversationListVisibilityChange();
+ }
+
+ /**
+ * Finalizes state after animations settle when entering the conversation list mode.
+ */
+ private void onFinishEnteringConversationListMode() {
+ mListCollapsed = false;
+ mConversationView.setVisibility(View.GONE);
+ mConversationViewOverlay.setVisibility(View.GONE);
+
+ // Once animations settle, the conversation list always takes up the
+ // remaining space that is on the right, so avoid hard pixel values,
+ // since this avoids manual re-computations when the parent container
+ // size changes for any reason (e.g. orientation change).
+ mConversationListContainer.getLayoutParams().width =
+ ViewGroup.LayoutParams.MATCH_PARENT;
+
+ dispatchConversationListVisibilityChange();
+ dispatchConversationVisibilityChanged(false);
+ }
+
+ /**
+ * Finalizes state after animations settle when entering conversation mode.
+ */
+ private void onFinishEnteringConversationMode() {
+ mLabelsView.setVisibility(View.GONE);
+
+ setConversationListWidth(computeConversationListWidth());
+
+ if (isConversationListCollapsible()) {
+ onCollapseList();
+ }
+
+ dispatchConversationVisibilityChanged(true);
+ }
+
+ /**
+ * Handles a size change and sets layout parameters as necessary.
+ * Most of the time this occurs for an orientation change, but theoretically could occur
+ * if the Gmail layout was included in a larger ViewGroup.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ if (w == oldw) {
+ // Only width changes are relevant to our logic.
+ return;
+ }
+
+ switch (currentMode) {
+ case ViewMode.CONVERSATION_LIST:
+ setLabelListWidth(computeLabelListWidth());
+ break;
+
+ case ViewMode.CONVERSATION:
+ final int conversationListWidth = computeConversationListWidth(w);
+ setConversationListWidth(conversationListWidth);
+ setConversationWidth(computeConversationWidth(w));
+ setConversationListLeft(computeConversationListLeft(conversationListWidth));
+ break;
+ }
+
+ // Request a measure pass here so all children views can be measured correctly before
+ // layout.
+ int widthSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY);
+ measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ public boolean onTouch(View target, MotionEvent event) {
+ if (isConversationListCollapsible() && (target == mConversationViewOverlay)) {
+ collapseList();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Finalizes state after animations complete when expanding the conversation list.
+ */
+ private void onUncollapseList() {
+ if (isConversationListCollapsible()) {
+ mConversationViewOverlay.setVisibility(View.VISIBLE);
+ } else {
+ mConversationViewOverlay.setVisibility(View.GONE);
+ }
+ dispatchConversationListVisibilityChange();
+ }
+
+ @Override
+ public void onViewModeChanged(int newMode) {
+ currentMode = newMode;
+ // Finish the current animation before changing mode.
+ if (mOutstandingAnimator != null) {
+ mOutstandingAnimator.cancel();
+ }
+ switch (currentMode) {
+ case ViewMode.CONVERSATION:
+ enterConversationMode();
+ break;
+ case ViewMode.CONVERSATION_LIST:
+ enterConversationListMode();
+ break;
+ }
+ }
+
+ /**
+ * Sets the left position of the conversation fragment. Used by animators.
+ * Not to be used externally.
+ * @hide
+ */
+ private void setConversationLeft(int left) {
+ mConversationLeft = left;
+ invalidate();
+ }
+
+ /**
+ * Sets the relative left position of the conversation list.
+ */
+ private void setConversationListLeft(int left) {
+ ((ViewGroup.MarginLayoutParams) mConversationListContainer.getLayoutParams())
+ .leftMargin = left;
+ requestLayout();
+ }
+
+ /**
+ * Sets the width of the conversation list.
+ */
+ private void setConversationListWidth(int width) {
+ mConversationListContainer.getLayoutParams().width = width;
+ requestLayout();
+ }
+
+ /**
+ * Sets the width of the conversation pane.
+ */
+ private void setConversationWidth(int width) {
+ mConversationView.getLayoutParams().width = width;
+ requestLayout();
+ }
+
+ /**
+ * Sets the width of the label list pane.
+ * Used internally and by animators. Not to be used externally.
+ */
+ private void setLabelListWidth(int width) {
+ mLabelsView.getLayoutParams().width = width;
+ // Mindy points out that this is strange. Instead of requesting a layout for the labels
+ // view, we should be requesting a layout for the entire view.
+ // TODO(viki): Change to this.requestLayout() and see if there is any improvement or loss
+ mLabelsView.requestLayout();
+ }
+
+ // TODO(viki): I think most of the next methods aren't being used. Rather than removing them,
+ // I'm marking them private to remove once the application is complete.
+ /**
+ * Sets the left position of the labels fragment. Used by animators. Not to
+ * be used externally.
+ * @hide
+ */
+ private void setLabelsLeft(int left) {
+ mLabelsLeft = left;
+ invalidate();
+ }
+
+ /**
+ * Sets the alpha of the conversation list. Used by animators. Not to be used externally.
+ * @hide
+ */
+ private void setListAlpha(int alpha) {
+ mListAlpha = alpha;
+ invalidate();
+ }
+
+ /**
+ * Sets the alpha of the conversation list bitmap. Used by animators. Not to be used externally.
+ * @hide
+ */
+ private void setListBitmapAlpha(int alpha) {
+ mListPaint.setAlpha(alpha);
+ invalidate();
+ }
+
+ /**
+ * Sets the left position of the conversation list bitmap. Used by animators. Not to be used
+ * externally.
+ * @hide
+ */
+ private void setListBitmapLeft(int left) {
+ mListBitmapLeft = left;
+ invalidate();
+ }
+
+ /**
+ * Sets the {@link LayoutListener} for this object.
+ */
+ public void setListener(LayoutListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Sets the left position of the conversation list. Used by animators. Not to be used
+ * externally.
+ * @hide
+ */
+ private void setListLeft(int left) {
+ mListLeft = left;
+ invalidate();
+ }
+
+ /**
+ * Helper method to start animation.
+ */
+ private void startLayoutAnimation(
+ int duration, AnimatorListener listener, TimeInterpolator interpolator,
+ PropertyValuesHolder... values) {
+ ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
+ this, values).setDuration(duration);
+ animator.setInterpolator(interpolator);
+ if (listener != null) {
+ animator.addListener(listener);
+ }
+
+ mOutstandingAnimator = animator;
+ animator.start();
+ }
+
+ /**
+ * Expands the conversation list out from the left if it is in a collapsed state.
+ * Only applies in portrait mode.
+ */
+ public boolean uncollapseList() {
+ if (!mListCollapsed) {
+ return false;
+ }
+ mListCollapsed = false;
+
+ PropertyValuesHolder listLeftValues = PropertyValuesHolder.ofInt(
+ "conversationListLeft",
+ getConversationListLeft(), 0);
+
+ startLayoutAnimation(sAnimationCollapseDuration, mUncollapseListListener,
+ sCollapseInterpolator, listLeftValues);
+ return true;
+ }
+}
diff --git a/src/com/android/mail/ui/ViewMode.java b/src/com/android/mail/ui/ViewMode.java
index 96c42bd..6357b47 100644
--- a/src/com/android/mail/ui/ViewMode.java
+++ b/src/com/android/mail/ui/ViewMode.java
@@ -77,7 +77,7 @@
}
/**
- * Adds a listener from this view mode.
+ * Adds a listener from this view mode.
* Must happen in the UI thread.
*/
public void addListener(ModeChangeListener listener) {