blob: f70c9d5ae1e61e0d35227e5c09a5960b77bfbafe [file] [log] [blame]
/*******************************************************************************
* 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.support.v4.widget.DrawerLayout;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
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 com.android.mail.utils.ViewUtils;
import com.google.common.annotations.VisibleForTesting;
/**
* 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 folders 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 folder or conversation list view, conversations are hidden and folders and conversation lists
* are visible. This is the case in both portrait and landscape
*
* In Conversation List or Conversation View, folders 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 FrameLayout implements ModeChangeListener {
private static final String LOG_TAG = "TwoPaneLayout";
private static final long SLIDE_DURATION_MS = 300;
private final double mConversationListWeight;
private final double mFolderListWeight;
private final TimeInterpolator mSlideInterpolator;
/**
* True if and only if the conversation list is collapsible in the current device configuration.
* See {@link #isConversationListCollapsed()} to see whether it is currently collapsed
* (based on the current view mode).
*/
private final boolean mListCollapsible;
/**
* 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 mCurrentMode = ViewMode.UNKNOWN;
/**
* This mode represents the current positions of the three panes. This is split out from the
* current mode to give context to state transitions.
*/
private int mPositionedMode = ViewMode.UNKNOWN;
private AbstractActivityController mController;
private LayoutListener mListener;
private boolean mIsSearchResult;
private DrawerLayout mDrawerLayout;
private View mMiscellaneousView;
private View mConversationView;
private View mFoldersView;
private View mListView;
public static final int MISCELLANEOUS_VIEW_ID = R.id.miscellaneous_pane;
private final Runnable mTransitionCompleteRunnable = new Runnable() {
@Override
public void run() {
onTransitionComplete();
}
};
/**
* A special view used during animation of the conversation list.
* <p>
* The conversation list changes width when switching view modes, so to visually smooth out
* the transition, we cross-fade the old and new widths. During the transition, a bitmap of the
* old conversation list is kept here, and this view moves in tandem with the real list view,
* but its opacity gradually fades out to give way to the new width.
*/
private ConversationListCopy mListCopyView;
/**
* During a mode transition, this value is the final width for {@link #mListCopyView}. We want
* to avoid changing its width during the animation, as it should match the initial width of
* {@link #mListView}.
*/
private Integer mListCopyWidthOnComplete;
private final boolean mIsExpansiveLayout;
private boolean mDrawerInitialSetupComplete;
public TwoPaneLayout(Context context) {
this(context, null);
}
public TwoPaneLayout(Context context, AttributeSet attrs) {
super(context, attrs);
final Resources res = getResources();
// The conversation list might be visible now, depending on the layout: in portrait we
// don't show the conversation list, but in landscape we do. This information is stored
// in the constants
mListCollapsible = res.getBoolean(R.bool.list_collapsible);
mSlideInterpolator = AnimationUtils.loadInterpolator(context,
android.R.interpolator.decelerate_cubic);
final int folderListWeight = res.getInteger(R.integer.folder_list_weight);
final int convListWeight = res.getInteger(R.integer.conversation_list_weight);
final int convViewWeight = res.getInteger(R.integer.conversation_view_weight);
mFolderListWeight = (double) folderListWeight
/ (folderListWeight + convListWeight);
mConversationListWeight = (double) convListWeight
/ (convListWeight + convViewWeight);
mIsExpansiveLayout = res.getBoolean(R.bool.use_expansive_tablet_ui);
mDrawerInitialSetupComplete = false;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFoldersView = findViewById(R.id.content_pane);
mListView = findViewById(R.id.conversation_list_pane);
mListCopyView = (ConversationListCopy) findViewById(R.id.conversation_list_copy);
mConversationView = findViewById(R.id.conversation_pane);
mMiscellaneousView = findViewById(MISCELLANEOUS_VIEW_ID);
// all panes start GONE in initial UNKNOWN mode to avoid drawing misplaced panes
mCurrentMode = ViewMode.UNKNOWN;
mFoldersView.setVisibility(GONE);
mListView.setVisibility(GONE);
mListCopyView.setVisibility(GONE);
mConversationView.setVisibility(GONE);
mMiscellaneousView.setVisibility(GONE);
}
@VisibleForTesting
public void setController(AbstractActivityController controller, boolean isSearchResult) {
mController = controller;
mListener = controller;
mIsSearchResult = isSearchResult;
}
public void setDrawerLayout(DrawerLayout drawerLayout) {
mDrawerLayout = drawerLayout;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onMeasure()", this);
setupPaneWidths(MeasureSpec.getSize(widthMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
LogUtils.d(Utils.VIEW_DEBUGGING_TAG, "TPL(%s).onLayout()", this);
if (changed || mCurrentMode != mPositionedMode) {
positionPanes(getMeasuredWidth());
}
super.onLayout(changed, l, t, r, b);
}
/**
* Sizes up the three sliding panes. This method will ensure that the LayoutParams of the panes
* have the correct widths set for the current overall size and view mode.
*
* @param parentWidth this view's new width
*/
private void setupPaneWidths(int parentWidth) {
final int foldersWidth = computeFolderListWidth(parentWidth);
final int foldersFragmentWidth;
if (isDrawerView(mFoldersView)) {
foldersFragmentWidth = getResources().getDimensionPixelSize(R.dimen.drawer_width);
} else {
foldersFragmentWidth = foldersWidth;
}
final int convWidth = computeConversationWidth(parentWidth);
setPaneWidth(mFoldersView, foldersFragmentWidth);
// only adjust the fixed conversation view width when my width changes
if (parentWidth != getMeasuredWidth()) {
LogUtils.i(LOG_TAG, "setting up new TPL, w=%d fw=%d cv=%d", parentWidth,
foldersWidth, convWidth);
setPaneWidth(mMiscellaneousView, convWidth);
setPaneWidth(mConversationView, convWidth);
}
final int currListWidth = getPaneWidth(mListView);
int listWidth = currListWidth;
switch (mCurrentMode) {
case ViewMode.AD:
case ViewMode.CONVERSATION:
case ViewMode.SEARCH_RESULTS_CONVERSATION:
if (!mListCollapsible) {
listWidth = parentWidth - convWidth;
}
break;
case ViewMode.CONVERSATION_LIST:
case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
case ViewMode.SEARCH_RESULTS_LIST:
listWidth = parentWidth - foldersWidth;
break;
default:
break;
}
LogUtils.d(LOG_TAG, "conversation list width change, w=%d", listWidth);
setPaneWidth(mListView, listWidth);
if ((mCurrentMode != mPositionedMode && mPositionedMode != ViewMode.UNKNOWN)
|| mListCopyWidthOnComplete != null) {
mListCopyWidthOnComplete = listWidth;
} else {
setPaneWidth(mListCopyView, listWidth);
}
}
/**
* Positions the three sliding panes at the correct X offset (using {@link View#setX(float)}).
* When switching from list->conversation mode or vice versa, animate the change in X.
*
* @param width
*/
private void positionPanes(int width) {
if (mPositionedMode == mCurrentMode) {
return;
}
boolean hasPositions = false;
int convX = 0, listX = 0, foldersX = 0;
final boolean isRtl = ViewUtils.isViewRtl(this);
switch (mCurrentMode) {
case ViewMode.AD:
case ViewMode.CONVERSATION:
case ViewMode.SEARCH_RESULTS_CONVERSATION: {
final int foldersW = getPaneWidth(mFoldersView);
final int listW = getPaneWidth(mListView);
if (mListCollapsible) {
if (isRtl) {
convX = 0;
listX = width;
foldersX = width + listW;
} else {
convX = 0;
listX = -listW;
foldersX = listX - foldersW;
}
} else {
if (isRtl) {
convX = 0;
listX = getPaneWidth(mConversationView);
foldersX = width;
} else {
convX = listW;
listX = 0;
foldersX = -foldersW;
}
}
hasPositions = true;
LogUtils.i(LOG_TAG, "conversation mode layout, x=%d/%d/%d", foldersX, listX, convX);
break;
}
case ViewMode.CONVERSATION_LIST:
case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
case ViewMode.SEARCH_RESULTS_LIST: {
if (isRtl) {
convX = -getPaneWidth(mConversationView);
listX = 0;
foldersX = getPaneWidth(mListView);
} else {
convX = width;
listX = getPaneWidth(mFoldersView);
foldersX = 0;
}
hasPositions = true;
LogUtils.i(LOG_TAG, "conv-list mode layout, fX:%d/lX:%d/cX:%d",
foldersX, listX, convX);
break;
}
default:
break;
}
if (hasPositions) {
animatePanes(foldersX, listX, convX);
}
mPositionedMode = mCurrentMode;
}
private final AnimatorListenerAdapter mPaneAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mListCopyView.unbind();
useHardwareLayer(false);
fixupListCopyWidth();
onTransitionComplete();
}
@Override
public void onAnimationCancel(Animator animation) {
mListCopyView.unbind();
useHardwareLayer(false);
}
};
private void animatePanes(int foldersX, int listX, int convX) {
// If positioning has not yet happened, we don't need to animate panes into place.
// This happens on first layout, rotate, and when jumping straight to a conversation from
// a view intent.
if (mPositionedMode == ViewMode.UNKNOWN) {
mConversationView.setX(convX);
mMiscellaneousView.setX(convX);
mListView.setX(listX);
if (!isDrawerView(mFoldersView)) {
mFoldersView.setX(foldersX);
}
// listeners need to know that the "transition" is complete, even if one is not run.
// defer notifying listeners because we're in a layout pass, and they might do layout.
post(mTransitionCompleteRunnable);
return;
}
final boolean useListCopy = getPaneWidth(mListView) != getPaneWidth(mListCopyView);
if (useListCopy) {
// freeze the current list view before it gets redrawn
mListCopyView.bind(mListView);
mListCopyView.setX(mListView.getX());
mListCopyView.setAlpha(1.0f);
mListView.setAlpha(0.0f);
}
useHardwareLayer(true);
if (ViewMode.isAdMode(mCurrentMode)) {
mMiscellaneousView.animate().x(convX);
} else {
mConversationView.animate().x(convX);
}
if (!isDrawerView(mFoldersView)) {
mFoldersView.animate().x(foldersX);
}
if (useListCopy) {
mListCopyView.animate().x(listX).alpha(0.0f);
}
mListView.animate()
.x(listX)
.alpha(1.0f)
.setListener(mPaneAnimationListener);
configureAnimations(mConversationView, mFoldersView, mListView, mListCopyView,
mMiscellaneousView);
}
private void configureAnimations(View... views) {
for (View v : views) {
if (isDrawerView(v)) {
continue;
}
v.animate()
.setInterpolator(mSlideInterpolator)
.setDuration(SLIDE_DURATION_MS);
}
}
private void useHardwareLayer(boolean useHardware) {
final int layerType = useHardware ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE;
if (!isDrawerView(mFoldersView)) {
mFoldersView.setLayerType(layerType, null);
}
mListView.setLayerType(layerType, null);
mListCopyView.setLayerType(layerType, null);
mConversationView.setLayerType(layerType, null);
mMiscellaneousView.setLayerType(layerType, null);
if (useHardware) {
// these buildLayer calls are safe because layout is the only way we get here
// (i.e. these views must already be attached)
if (!isDrawerView(mFoldersView)) {
mFoldersView.buildLayer();
}
mListView.buildLayer();
mListCopyView.buildLayer();
mConversationView.buildLayer();
mMiscellaneousView.buildLayer();
}
}
private void fixupListCopyWidth() {
if (mListCopyWidthOnComplete == null ||
getPaneWidth(mListCopyView) == mListCopyWidthOnComplete) {
mListCopyWidthOnComplete = null;
return;
}
LogUtils.i(LOG_TAG, "onAnimationEnd of list view, setting copy width to %d",
mListCopyWidthOnComplete);
setPaneWidth(mListCopyView, mListCopyWidthOnComplete);
mListCopyWidthOnComplete = null;
}
private void onTransitionComplete() {
if (mController.isDestroyed()) {
// quit early if the hosting activity was destroyed before the animation finished
LogUtils.i(LOG_TAG, "IN TPL.onTransitionComplete, activity destroyed->quitting early");
return;
}
switch (mCurrentMode) {
case ViewMode.CONVERSATION:
case ViewMode.SEARCH_RESULTS_CONVERSATION:
dispatchConversationVisibilityChanged(true);
dispatchConversationListVisibilityChange(!isConversationListCollapsed());
break;
case ViewMode.CONVERSATION_LIST:
case ViewMode.SEARCH_RESULTS_LIST:
dispatchConversationVisibilityChanged(false);
dispatchConversationListVisibilityChange(true);
break;
case ViewMode.AD:
dispatchConversationVisibilityChanged(false);
dispatchConversationListVisibilityChange(!isConversationListCollapsed());
break;
default:
break;
}
}
/**
* Computes the width of the conversation list in stable state of the current mode.
*/
public 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 (mCurrentMode) {
case ViewMode.CONVERSATION_LIST:
case ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION:
case ViewMode.SEARCH_RESULTS_LIST:
return totalWidth - computeFolderListWidth(totalWidth);
case ViewMode.AD:
case ViewMode.CONVERSATION:
case ViewMode.SEARCH_RESULTS_CONVERSATION:
return (int) (totalWidth * mConversationListWeight);
}
return 0;
}
public int computeConversationWidth() {
return computeConversationWidth(getMeasuredWidth());
}
/**
* Computes the width of the conversation pane in stable state of the
* current mode.
*/
private int computeConversationWidth(int totalWidth) {
if (mListCollapsible) {
return totalWidth;
} else {
return totalWidth - (int) (totalWidth * mConversationListWeight);
}
}
/**
* Computes the width of the folder list in stable state of the current mode.
*/
private int computeFolderListWidth(int parentWidth) {
if (mIsSearchResult) {
return 0;
} else if (isDrawerView(mFoldersView)) {
return 0;
} else {
return (int) (parentWidth * mFolderListWeight);
}
}
private void dispatchConversationListVisibilityChange(boolean visible) {
if (mListener != null) {
mListener.onConversationListVisibilityChanged(visible);
}
}
private void dispatchConversationVisibilityChanged(boolean visible) {
if (mListener != null) {
mListener.onConversationVisibilityChanged(visible);
}
}
// does not apply to drawer children. will return zero for those.
private int getPaneWidth(View pane) {
return isDrawerView(pane) ? 0 : pane.getLayoutParams().width;
}
private boolean isDrawerView(View child) {
return child != null && child.getParent() == mDrawerLayout;
}
/**
* @return Whether or not the conversation list is visible on screen.
*/
public boolean isConversationListCollapsed() {
return !ViewMode.isListMode(mCurrentMode) && mListCollapsible;
}
@Override
public void onViewModeChanged(int newMode) {
// make all initially GONE panes visible only when the view mode is first determined
if (mCurrentMode == ViewMode.UNKNOWN) {
mFoldersView.setVisibility(VISIBLE);
mListView.setVisibility(VISIBLE);
mListCopyView.setVisibility(VISIBLE);
}
if (ViewMode.isAdMode(newMode)) {
mMiscellaneousView.setVisibility(VISIBLE);
mConversationView.setVisibility(GONE);
} else {
mConversationView.setVisibility(VISIBLE);
mMiscellaneousView.setVisibility(GONE);
}
// set up the drawer as appropriate for the configuration
final ViewParent foldersParent = mFoldersView.getParent();
if (mIsExpansiveLayout && foldersParent != this) {
if (foldersParent != mDrawerLayout) {
throw new IllegalStateException("invalid Folders fragment parent: " +
foldersParent);
}
mDrawerLayout.removeView(mFoldersView);
addView(mFoldersView, 0);
mFoldersView.findViewById(R.id.folders_pane_edge).setVisibility(VISIBLE);
mFoldersView.setBackgroundDrawable(null);
} else if (!mIsExpansiveLayout && foldersParent == this) {
removeView(mFoldersView);
mDrawerLayout.addView(mFoldersView);
final DrawerLayout.LayoutParams lp =
(DrawerLayout.LayoutParams) mFoldersView.getLayoutParams();
lp.gravity = Gravity.START;
mFoldersView.setLayoutParams(lp);
mFoldersView.findViewById(R.id.folders_pane_edge).setVisibility(GONE);
mFoldersView.setBackgroundResource(R.color.list_background_color);
}
// detach the pager immediately from its data source (to prevent processing updates)
if (ViewMode.isConversationMode(mCurrentMode)) {
mController.disablePagerUpdates();
}
mDrawerInitialSetupComplete = true;
mCurrentMode = newMode;
LogUtils.i(LOG_TAG, "onViewModeChanged(%d)", newMode);
// do all the real work in onMeasure/onLayout, when panes are sized and positioned for the
// current width/height anyway
requestLayout();
}
public boolean isModeChangePending() {
return mPositionedMode != mCurrentMode;
}
private void setPaneWidth(View pane, int w) {
final ViewGroup.LayoutParams lp = pane.getLayoutParams();
if (lp.width == w) {
return;
}
lp.width = w;
pane.setLayoutParams(lp);
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
final String s;
if (pane == mFoldersView) {
s = "folders";
} else if (pane == mListView) {
s = "conv-list";
} else if (pane == mConversationView) {
s = "conv-view";
} else if (pane == mMiscellaneousView) {
s = "misc-view";
} else {
s = "???:" + pane;
}
LogUtils.d(LOG_TAG, "TPL: setPaneWidth, w=%spx pane=%s", w, s);
}
}
public boolean isDrawerEnabled() {
return !mIsExpansiveLayout && mDrawerInitialSetupComplete;
}
public boolean isExpansiveLayout() {
return mIsExpansiveLayout;
}
}